diff --git a/skills/hourlog/SKILL.md b/skills/hourlog/SKILL.md index ebc8b45..4800510 100644 --- a/skills/hourlog/SKILL.md +++ b/skills/hourlog/SKILL.md @@ -48,8 +48,13 @@ time that project was actually active, plus wall-clock `total`, the day's `window`, and `flags`. That table is the deliverable — paste it as-is rather than restating it in prose. -`total` is the union of active slots, so on a day with parallel sessions it is -less than the row sum. Both numbers are true; they answer different questions. +Overlap is divided, not double-counted: five minutes with two projects open is +five minutes of the day, half to each. The project columns therefore add up to +`total`, which is wall-clock presence. + +That division is also the ratio to submit with. The user gives the day's real +total; you split it by these proportions. Never submit the measured numbers as +the hours unless the user says they are right. Paste it as plain markdown in the reply. Never wrap it in a code fence: a fence shows the raw pipes and dashes instead of a rendered table. diff --git a/skills/hourlog/scripts/scan-activity.py b/skills/hourlog/scripts/scan-activity.py index fdb1d7f..73de70a 100755 --- a/skills/hourlog/scripts/scan-activity.py +++ b/skills/hourlog/scripts/scan-activity.py @@ -22,8 +22,9 @@ a guessed number is worse than a small true one. Measured time is a floor. Meetings, review and thinking leave no transcript, so the user adds those back; the script never does. -The 'total' column is wall-clock presence, the union of active slots across -projects, so on days with parallel sessions it is less than the row sum. +Overlap is divided, not double-counted: five minutes with two projects open is +five minutes of the day, half to each. So the project columns add up to the +'total' column, which is wall-clock presence. """ import argparse import collections @@ -114,6 +115,21 @@ def read_session(path): return cwd, stamps +def attribute(slots_of, owners, mins): + """Divide each slot's minutes evenly among the projects live in it. + + Five minutes with two projects open is five minutes of the user's day, not + ten, so each project gets half. Largest remainder over whole minutes keeps + the parts summing to wall clock exactly. + """ + exact = {p: sum(mins / owners[s] for s in ss) for p, ss in slots_of.items()} + floors = {p: int(v) for p, v in exact.items()} + left = round(sum(exact.values())) - sum(floors.values()) + for p in sorted(exact, key=lambda p: -(exact[p] - floors[p]))[:left]: + floors[p] += 1 + return floors + + def fmt_dur(minutes): """45 -> '45min', 155 -> '2h35', 180 -> '3h', 0 -> '—'.""" if not minutes: @@ -216,22 +232,27 @@ def main(): d = since while d <= until: per = grid.get(d.isoformat(), {}) - totals = {p: sum(len(s) for s in hrs.values()) for p, hrs in per.items()} - # Wall clock, not the row sum: a slot with two projects live is one - # minute of the user's day, counted once here and once per project. - union = len({s for hrs in per.values() for ss in hrs.values() - for s in ss}) + slots_of = {p: {s for ss in hrs.values() for s in ss} + for p, hrs in per.items() + if not p.startswith("unmapped:")} + owners = collections.Counter() + for ss in slots_of.values(): + owners.update(ss) + share = attribute(slots_of, owners, mins) entry = { "date": d.isoformat(), "weekday": d.strftime("%a"), - "active_minutes": union * mins, + "active_minutes": len(owners) * mins, "projects": [], } - for proj, n in sorted(totals.items(), key=lambda kv: -kv[1]): + raw = {p: sum(len(s) for s in hrs.values()) * mins + for p, hrs in per.items()} + for proj in sorted(per, key=lambda p: -raw[p]): hrs = sorted(per[proj]) entry["projects"].append({ "project": proj, - "active_minutes": n * mins, + "active_minutes": share.get(proj, raw[proj]), + "raw_minutes": raw[proj], "first_hour": hrs[0], "last_hour": hrs[-1], "outside_workday": [ @@ -256,7 +277,7 @@ def main(): print(f"{since} .. {until} — {nfiles} sessions, {mins}-min slots, " f"{cfg['timezone']}") print("measured active time, nothing extrapolated; " - "total is wall clock, so it can be less than the row\n") + "overlapping minutes split evenly between projects\n") cols = [p for p in dict.fromkeys( x["project"] for e in report for x in e["projects"])