scripts/shallow_work_auditor.py
8.4 KB| 1 | #!/usr/bin/env python3 |
| 2 | """shallow_work_auditor.py — Classify tasks deep vs shallow and audit the shallow share against a budget. |
| 3 | |
| 4 | Encodes Cal Newport's shallow-work budget discipline: |
| 5 | |
| 6 | 1. Every task is classified DEEP or SHALLOW by keyword heuristics on its name: |
| 7 | shallow signals: email, inbox, slack, status, standup, meeting, expense, invoice, |
| 8 | scheduling, calendar, admin, paperwork, timesheet, errand, triage |
| 9 | deep signals: write, draft, design, code, build, research, analyze, study, |
| 10 | architect, prototype, prove, strategy, model |
| 11 | A tie (or no signal at all) counts as SHALLOW — depth must be claimed deliberately. |
| 12 | Override any task with an explicit :deep or :shallow suffix; the suffix always wins. |
| 13 | 2. The shallow share of total hours is compared against --budget percent (default 50 — |
| 14 | the top of Newport's 30-50 percent band for most knowledge roles). |
| 15 | 3. For every shallow item, the forcing question is printed: |
| 16 | "How long would it take to train a smart recent graduate to do this?" |
| 17 | Months of training -> it is probably deep; days or weeks -> it is shallow. Batch it, |
| 18 | delegate it, or say no to it. |
| 19 | |
| 20 | Verdicts: |
| 21 | WITHIN-BUDGET (exit 0) shallow share <= budget |
| 22 | OVER-BUDGET (exit 2) shallow share > budget — cut, batch, or delegate before planning the day |
| 23 | |
| 24 | NO LLM CALLS. Deterministic keyword scoring. Identical inputs, identical verdict. |
| 25 | |
| 26 | Exit codes: |
| 27 | 0 WITHIN-BUDGET |
| 28 | 1 usage / input error (bad task spec) |
| 29 | 2 OVER-BUDGET |
| 30 | |
| 31 | Usage: |
| 32 | python shallow_work_auditor.py --task "Write investor update:60" \\ |
| 33 | --task "Email triage:45" --task "Analyze churn cohort:90:deep" --budget 50 |
| 34 | python shallow_work_auditor.py --sample |
| 35 | python shallow_work_auditor.py --sample --json |
| 36 | """ |
| 37 | |
| 38 | import argparse |
| 39 | import json |
| 40 | import re |
| 41 | import sys |
| 42 | from typing import Any, Dict, List |
| 43 | |
| 44 | SHALLOW_KEYWORDS = [ |
| 45 | "email", "inbox", "slack", "status", "standup", "meeting", "expense", "invoice", |
| 46 | "scheduling", "schedule", "calendar", "admin", "paperwork", "timesheet", "errand", |
| 47 | "triage", |
| 48 | ] |
| 49 | DEEP_KEYWORDS = [ |
| 50 | "write", "writing", "draft", "design", "code", "coding", "program", "build", |
| 51 | "research", "analyze", "analysis", "study", "architect", "prototype", "prove", |
| 52 | "strategy", "model", |
| 53 | ] |
| 54 | |
| 55 | FORCING_QUESTION = ('How long would it take to train a smart recent graduate to do "{name}"? ' |
| 56 | "Days or weeks -> shallow: batch it, delegate it, or say no.") |
| 57 | |
| 58 | |
| 59 | def parse_task(spec: str) -> Dict[str, Any]: |
| 60 | """Accepts "name:minutes" (heuristic classifies) or "name:minutes:deep|shallow" (override).""" |
| 61 | parts = spec.rsplit(":", 2) |
| 62 | override = None |
| 63 | if len(parts) == 3 and parts[2].strip().lower() in ("deep", "shallow"): |
| 64 | name, minutes_s, override = parts[0], parts[1], parts[2].strip().lower() |
| 65 | else: |
| 66 | parts = spec.rsplit(":", 1) |
| 67 | if len(parts) != 2: |
| 68 | raise ValueError(f'task must be "name:minutes[:deep|shallow]", got: {spec!r}') |
| 69 | name, minutes_s = parts |
| 70 | name, minutes_s = name.strip(), minutes_s.strip() |
| 71 | if not name: |
| 72 | raise ValueError(f"task name is empty in: {spec!r}") |
| 73 | if not minutes_s.isdigit() or int(minutes_s) <= 0: |
| 74 | raise ValueError(f"task minutes must be a positive integer in: {spec!r}") |
| 75 | return {"name": name, "minutes": int(minutes_s), "override": override} |
| 76 | |
| 77 | |
| 78 | def classify(name: str) -> Dict[str, Any]: |
| 79 | lowered = name.lower() |
| 80 | deep_hits = [k for k in DEEP_KEYWORDS if re.search(rf"\b{re.escape(k)}\b", lowered)] |
| 81 | shallow_hits = [k for k in SHALLOW_KEYWORDS if re.search(rf"\b{re.escape(k)}\b", lowered)] |
| 82 | if len(deep_hits) > len(shallow_hits): |
| 83 | mode, why = "deep", f"deep signals: {', '.join(deep_hits)}" |
| 84 | elif shallow_hits: |
| 85 | mode, why = "shallow", f"shallow signals: {', '.join(shallow_hits)}" |
| 86 | else: |
| 87 | mode, why = "shallow", "no signal — unclassified counts as shallow; claim :deep explicitly if wrong" |
| 88 | return {"mode": mode, "why": why} |
| 89 | |
| 90 | |
| 91 | def audit(tasks: List[Dict[str, Any]], budget_pct: float) -> Dict[str, Any]: |
| 92 | items = [] |
| 93 | for t in tasks: |
| 94 | if t["override"]: |
| 95 | mode, why = t["override"], f"explicit :{t['override']} override" |
| 96 | else: |
| 97 | c = classify(t["name"]) |
| 98 | mode, why = c["mode"], c["why"] |
| 99 | item = {"name": t["name"], "minutes": t["minutes"], "mode": mode, "why": why} |
| 100 | if mode == "shallow": |
| 101 | item["forcing_question"] = FORCING_QUESTION.format(name=t["name"]) |
| 102 | items.append(item) |
| 103 | |
| 104 | total = sum(i["minutes"] for i in items) |
| 105 | shallow_min = sum(i["minutes"] for i in items if i["mode"] == "shallow") |
| 106 | deep_min = total - shallow_min |
| 107 | share = round(100.0 * shallow_min / total, 1) if total else 0.0 |
| 108 | verdict = "WITHIN-BUDGET" if share <= budget_pct else "OVER-BUDGET" |
| 109 | return { |
| 110 | "items": items, |
| 111 | "total_minutes": total, |
| 112 | "deep_minutes": deep_min, |
| 113 | "shallow_minutes": shallow_min, |
| 114 | "shallow_share_pct": share, |
| 115 | "budget_pct": budget_pct, |
| 116 | "verdict": verdict, |
| 117 | "headline": ( |
| 118 | f"Shallow work is {share}% of the day against a {budget_pct:.0f}% budget. " |
| 119 | + ("Within budget — protect the deep hours you just earned." |
| 120 | if verdict == "WITHIN-BUDGET" |
| 121 | else "Over budget — cut, batch, or delegate shallow items before planning the day.") |
| 122 | ), |
| 123 | } |
| 124 | |
| 125 | |
| 126 | def render_human(r: Dict[str, Any]) -> str: |
| 127 | out: List[str] = [] |
| 128 | out.append("Shallow-Work Audit (deep vs shallow, share vs budget)") |
| 129 | out.append("=" * 64) |
| 130 | out.append("") |
| 131 | out.append("| Task | Min | Mode | Basis |") |
| 132 | out.append("|------|-----|------|-------|") |
| 133 | for i in r["items"]: |
| 134 | out.append(f"| {i['name']} | {i['minutes']} | {i['mode'].upper()} | {i['why']} |") |
| 135 | out.append("") |
| 136 | out.append(f" Deep {r['deep_minutes']} min · Shallow {r['shallow_minutes']} min · " |
| 137 | f"Shallow share {r['shallow_share_pct']}% vs budget {r['budget_pct']:.0f}%") |
| 138 | out.append("") |
| 139 | out.append(f" VERDICT: {r['verdict']}") |
| 140 | out.append(f" {r['headline']}") |
| 141 | shallow_items = [i for i in r["items"] if i["mode"] == "shallow"] |
| 142 | if shallow_items: |
| 143 | out.append("") |
| 144 | out.append(" The forcing question, per shallow item:") |
| 145 | for i in shallow_items: |
| 146 | out.append(f" - {i['forcing_question']}") |
| 147 | return "\n".join(out) |
| 148 | |
| 149 | |
| 150 | SAMPLE_TASKS = [ |
| 151 | "Write investor update:60", |
| 152 | "Analyze churn cohort:90:deep", |
| 153 | "Email triage:45", |
| 154 | "Slack catch-up:30", |
| 155 | "Expense report:20", |
| 156 | "Team scheduling:15", |
| 157 | ] |
| 158 | |
| 159 | |
| 160 | def main(argv: List[str]) -> int: |
| 161 | p = argparse.ArgumentParser( |
| 162 | description="Classify tasks deep vs shallow and audit the shallow share against a budget.", |
| 163 | formatter_class=argparse.RawDescriptionHelpFormatter, |
| 164 | epilog=("Exit codes:\n" |
| 165 | " 0 WITHIN-BUDGET — shallow share <= --budget\n" |
| 166 | " 1 usage / input error\n" |
| 167 | " 2 OVER-BUDGET — shallow share > --budget; cut, batch, or delegate first"), |
| 168 | ) |
| 169 | p.add_argument("--task", action="append", default=[], |
| 170 | help='Repeatable: "name:minutes" (heuristic) or "name:minutes:deep|shallow" (override)') |
| 171 | p.add_argument("--budget", type=float, default=50.0, |
| 172 | help="Shallow budget as percent of total time (default 50 — top of the 30-50 band)") |
| 173 | p.add_argument("--json", action="store_true", help="Emit JSON instead of the table") |
| 174 | p.add_argument("--sample", action="store_true", help="Run the embedded sample task list") |
| 175 | args = p.parse_args(argv) |
| 176 | |
| 177 | if args.sample: |
| 178 | args.task = SAMPLE_TASKS |
| 179 | if not args.task: |
| 180 | p.print_help() |
| 181 | print("\nerror: at least one --task is required (or --sample)", file=sys.stderr) |
| 182 | return 1 |
| 183 | if not 0 < args.budget <= 100: |
| 184 | print("error: --budget must be in (0, 100]", file=sys.stderr) |
| 185 | return 1 |
| 186 | try: |
| 187 | tasks = [parse_task(s) for s in args.task] |
| 188 | except ValueError as exc: |
| 189 | print(f"error: {exc}", file=sys.stderr) |
| 190 | return 1 |
| 191 | |
| 192 | result = audit(tasks, args.budget) |
| 193 | if args.json: |
| 194 | print(json.dumps(result, indent=2)) |
| 195 | else: |
| 196 | print(render_human(result)) |
| 197 | return 0 if result["verdict"] == "WITHIN-BUDGET" else 2 |
| 198 | |
| 199 | |
| 200 | if __name__ == "__main__": |
| 201 | sys.exit(main(sys.argv[1:])) |
文件内容是平台检查时读出的文本,只供查看。平台不会运行包里的任何脚本或程序。

