跳到主要内容

每周复盘与待办检查

收藏0

按收集、检查、规划三个阶段复盘一周,找出遗漏待办、停滞项目和没有下一步的承诺。

让 AI 安装

github最新版本 0.1.1更新于 2026-10-21下载 2 次2 个版本

scripts/commitment_auditor.py

7.5 KB
1#!/usr/bin/env python3
2"""commitment_auditor.py — Audit a commitment portfolio: flags, health score, one honest verdict.
3
4Step 8 of the weekly review (review project lists) in numbers. Reads a JSON list of commitments:
5
6 [{"name": "Website relaunch", "days_since_touched": 3, "has_next_action": true}, ...]
7
8and flags each one:
9
10 STALLED days_since_touched > 14 (a week-and-a-half of silence is drift, not rest)
11 NO-NEXT-ACTION has_next_action is false (a project with no next action is a wish)
12 SOMEDAY-CANDIDATE days_since_touched > 45 (be honest: activate it or move it to someday/maybe)
13
14Then computes a 0-100 commitment-health score. The formula is printed with the output so the
15number is auditable, never mystical:
16
17 score = 100 - 30*(stalled/total) - 40*(no_next_action/total) - 30*(someday/total)
18
19(SOMEDAY-CANDIDATEs also count as STALLED — the double penalty is deliberate escalation.)
20
21Verdict: HEALTHY (>= 80) · DRIFTING (50-79) · OVERCOMMITTED (< 50)
22
23Deterministic logic. No LLM calls, no network. Stdlib only.
24
25Usage:
26 python commitment_auditor.py --input commitments.json
27 python commitment_auditor.py --input commitments.json --json
28 python commitment_auditor.py --sample
29
30Exit codes:
31 0 audit complete (any verdict) — also --sample / --help
32 2 input file missing, unreadable, or not the expected JSON shape
33"""
34
35import argparse
36import json
37import sys
38from typing import Any, Dict, List
39
40STALLED_DAYS = 14
41SOMEDAY_DAYS = 45
42
43FORMULA = ("score = 100 - 30*(stalled/total) - 40*(no_next_action/total) "
44 "- 30*(someday_candidates/total)")
45
46SAMPLE_COMMITMENTS = [
47 {"name": "Website relaunch", "days_since_touched": 3, "has_next_action": True},
48 {"name": "Q3 budget draft", "days_since_touched": 9, "has_next_action": True},
49 {"name": "Hire a designer", "days_since_touched": 21, "has_next_action": False},
50 {"name": "Learn Spanish", "days_since_touched": 41, "has_next_action": True},
51 {"name": "Write a novel", "days_since_touched": 63, "has_next_action": False},
52 {"name": "Renew passports", "days_since_touched": 2, "has_next_action": True},
53]
54
55
56def validate(data: Any) -> List[Dict[str, Any]]:
57 if not isinstance(data, list) or not data:
58 raise ValueError("input must be a non-empty JSON list of commitment objects")
59 for i, c in enumerate(data):
60 if not isinstance(c, dict):
61 raise ValueError(f"entry {i} is not an object")
62 for key in ("name", "days_since_touched", "has_next_action"):
63 if key not in c:
64 raise ValueError(f"entry {i} ({c.get('name', '?')!r}) missing key: {key!r}")
65 if not isinstance(c["days_since_touched"], (int, float)) or c["days_since_touched"] < 0:
66 raise ValueError(f"entry {i}: days_since_touched must be a non-negative number")
67 if not isinstance(c["has_next_action"], bool):
68 raise ValueError(f"entry {i}: has_next_action must be true or false")
69 return data
70
71
72def audit(commitments: List[Dict[str, Any]]) -> Dict[str, Any]:
73 total = len(commitments)
74 rows = []
75 n_stalled = n_no_action = n_someday = 0
76 for c in commitments:
77 flags = []
78 if c["days_since_touched"] > STALLED_DAYS:
79 flags.append("STALLED")
80 n_stalled += 1
81 if not c["has_next_action"]:
82 flags.append("NO-NEXT-ACTION")
83 n_no_action += 1
84 if c["days_since_touched"] > SOMEDAY_DAYS:
85 flags.append("SOMEDAY-CANDIDATE")
86 n_someday += 1
87 rows.append({"name": c["name"],
88 "days_since_touched": c["days_since_touched"],
89 "has_next_action": c["has_next_action"],
90 "flags": flags})
91
92 score = 100.0 - 30.0 * (n_stalled / total) - 40.0 * (n_no_action / total) \
93 - 30.0 * (n_someday / total)
94 score = round(max(0.0, min(100.0, score)), 1)
95
96 if score >= 80:
97 verdict = "HEALTHY"
98 headline = ("The portfolio is honest: most commitments are moving and have a next "
99 "action. Keep the cadence.")
100 elif score >= 50:
101 verdict = "DRIFTING"
102 headline = ("Several commitments are coasting on good intentions. Give each flagged "
103 "item a next action, a waiting-for, or a someday/maybe home this review.")
104 else:
105 verdict = "OVERCOMMITTED"
106 headline = ("More commitments than attention. Kill or park the SOMEDAY-CANDIDATEs, "
107 "then rebuild next actions for what survives — fewer, moving projects "
108 "beat many, stalled ones.")
109
110 return {
111 "total_commitments": total,
112 "flag_counts": {"STALLED": n_stalled, "NO-NEXT-ACTION": n_no_action,
113 "SOMEDAY-CANDIDATE": n_someday},
114 "thresholds": {"stalled_days": STALLED_DAYS, "someday_days": SOMEDAY_DAYS},
115 "commitments": rows,
116 "formula": FORMULA,
117 "health_score": score,
118 "verdict": verdict,
119 "headline": headline,
120 }
121
122
123def render_human(r: Dict[str, Any]) -> str:
124 out = ["Commitment Auditor (project lists in numbers)", "=" * 64]
125 out.append(f" Commitments: {r['total_commitments']} "
126 f"Stalled > {r['thresholds']['stalled_days']}d · "
127 f"Someday-candidate > {r['thresholds']['someday_days']}d")
128 out.append("")
129 for c in r["commitments"]:
130 mark = "OK " if not c["flags"] else "!! "
131 flags = f" [{', '.join(c['flags'])}]" if c["flags"] else ""
132 out.append(f" {mark}{c['name']} — {c['days_since_touched']}d since touched, "
133 f"next action: {'yes' if c['has_next_action'] else 'NO'}{flags}")
134 fc = r["flag_counts"]
135 out.append("")
136 out.append(f" Flags: STALLED {fc['STALLED']} · NO-NEXT-ACTION {fc['NO-NEXT-ACTION']} · "
137 f"SOMEDAY-CANDIDATE {fc['SOMEDAY-CANDIDATE']}")
138 out.append(f" Formula: {r['formula']}")
139 out.append(f" COMMITMENT HEALTH: {r['health_score']}/100 VERDICT: {r['verdict']}")
140 out.append("")
141 out.append(f" {r['headline']}")
142 return "\n".join(out)
143
144
145def main(argv: List[str]) -> int:
146 p = argparse.ArgumentParser(
147 description="Audit commitments: STALLED / NO-NEXT-ACTION / SOMEDAY-CANDIDATE flags + 0-100 health score.",
148 formatter_class=argparse.RawDescriptionHelpFormatter,
149 epilog=__doc__,
150 )
151 p.add_argument("--input", help="Path to a JSON list of "
152 '{"name", "days_since_touched", "has_next_action"} objects')
153 p.add_argument("--sample", action="store_true", help="Run on embedded example data and exit 0")
154 p.add_argument("--json", action="store_true", help="Emit JSON instead of text")
155 args = p.parse_args(argv)
156
157 try:
158 if args.sample:
159 commitments = validate(SAMPLE_COMMITMENTS)
160 elif args.input:
161 with open(args.input, "r", encoding="utf-8") as f:
162 commitments = validate(json.load(f))
163 else:
164 p.print_help()
165 print("\nerror: provide --input <file.json> or --sample", file=sys.stderr)
166 return 2
167 except (OSError, ValueError, json.JSONDecodeError) as e:
168 print(f"error: {e}", file=sys.stderr)
169 return 2
170
171 result = audit(commitments)
172 if args.json:
173 print(json.dumps(result, indent=2))
174 else:
175 print(render_human(result))
176 return 0
177
178
179if __name__ == "__main__":
180 sys.exit(main(sys.argv[1:]))

文件内容是平台检查时读出的文本,只供查看。平台不会运行包里的任何脚本或程序。