optiquant Claude Opus 4.8 (1M context) commited on
Commit
866bea8
Β·
1 Parent(s): f92e318

Speed + UX: trim scored schema, cache prompt, lighter ctx, live status

Browse files

App-only deploy to the Space. Same app changes as origin commit 6408b42 but
without the demo/ video tooling (and its font binary, which HF rejects and the
running app never uses).

- PARSE schema trimmed to ranker-only fields (~20-35% fewer output tokens)
- Why column derived from score.explain (deterministic, zero tokens)
- cache_prompt: reuse system-prompt KV between calls
- llama-server ctx 8192 -> 4096
- prioritize() streams an instant status so CPU inference isn't a frozen wait

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files changed (6) hide show
  1. app.py +61 -13
  2. llm.py +4 -0
  3. prompts.py +1 -4
  4. score.py +30 -55
  5. start.sh +1 -1
  6. test_score.py +0 -13
app.py CHANGED
@@ -37,6 +37,35 @@ Two scores compete and the higher one shows:
37
  DF_HEADERS = ["#", "Task", "Score", "Due", "Flag", "Why", "I", "R", "EffΒ·h"]
38
  DF_DATATYPES = ["number", "str", "str", "str", "str", "str", "number", "number", "number"]
39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
  def _label(t: dict) -> str:
42
  return f"{t['id']} Β· {t['title']}"
@@ -59,19 +88,30 @@ def render(tasks: list[dict]):
59
  score.format_score(c["display"]),
60
  score.due_label(t.get("due_date"), now) if t.get("due_date") else "β€”",
61
  flag,
62
- t.get("reason", "") or "",
63
  round(c["I"]),
64
  round(c["R"]),
65
  round(t["effort_hours"], 2),
66
  ])
67
  top = ranked[0]
68
- header = f"### β–Ά Do this first: **{top['title']}** Β· score {score.format_score(score.priority(top))}"
 
 
 
69
  return header, rows, [_label(t) for t in ranked]
70
 
71
 
72
  def prioritize(text, image, tasks):
 
 
73
  if not llm.is_ready():
74
- return tasks, "### ⏳ The local model is still loading β€” give it a moment and try again.", [], gr.update()
 
 
 
 
 
 
75
  collected = []
76
  if text and text.strip():
77
  collected += llm.parse_braindump(text)
@@ -80,9 +120,10 @@ def prioritize(text, image, tasks):
80
  for i, t in enumerate(collected):
81
  t["id"] = f"t{i}"
82
  if not collected:
83
- return collected, "### Nothing actionable found β€” try a different dump or photo.", [], gr.update(choices=[], value=None)
 
84
  header, rows, choices = render(collected)
85
- return collected, header, rows, gr.update(choices=choices, value=(choices[0] if choices else None))
86
 
87
 
88
  def load_task(sel, tasks):
@@ -98,7 +139,7 @@ def apply_edit(sel, impact, readiness, effort, tasks):
98
  t["impact"] = int(impact)
99
  t["readiness"] = int(readiness)
100
  t["effort_hours"] = float(effort)
101
- t["reason"] = (t.get("reason") or "").split(" Β· edited")[0] + " Β· edited"
102
  header, rows, choices = render(tasks)
103
  keep = sel if sel in choices else (choices[0] if choices else None)
104
  return tasks, header, rows, gr.update(choices=choices, value=keep)
@@ -121,12 +162,16 @@ def _find(sel, tasks):
121
  return next((x for x in tasks if x["id"] == tid), None)
122
 
123
 
124
- with gr.Blocks(title="whatfirst-small", theme=gr.themes.Soft()) as demo:
125
- gr.Markdown(
126
- "# whatfirst Β· small\n"
127
- "Dump everything on your mind β€” or snap a photo of your to-do list β€” and a "
128
- "**small local model** turns it into structured tasks. A **transparent scoring "
129
- "engine** then tells you what to do *first*, and shows its work. No cloud, no API keys."
 
 
 
 
130
  )
131
  tasks_state = gr.State([])
132
 
@@ -141,7 +186,10 @@ with gr.Blocks(title="whatfirst-small", theme=gr.themes.Soft()) as demo:
141
  go = gr.Button("Prioritize", variant="primary")
142
  sample = gr.Button("Try a sample")
143
  with gr.Column(scale=3):
144
- header = gr.Markdown("### Add a brain-dump or a photo, then hit **Prioritize**.")
 
 
 
145
  table = gr.Dataframe(
146
  headers=DF_HEADERS, datatype=DF_DATATYPES, interactive=False,
147
  wrap=True, label="Ranked",
 
37
  DF_HEADERS = ["#", "Task", "Score", "Due", "Flag", "Why", "I", "R", "EffΒ·h"]
38
  DF_DATATYPES = ["number", "str", "str", "str", "str", "str", "number", "number", "number"]
39
 
40
+ # ── Branding, borrowed from the full what-first.com app ───────────────────────
41
+ # Indigo β†’ blue gradient, slate/ink text, Roboto β€” the palette the marketing
42
+ # site and the demo video (demo/compose.py) share.
43
+ INDIGO, BLUE = "#4F46E5", "#2563EB"
44
+ SLATE, INK, MUTED = "#1E293B", "#111827", "#5A6473"
45
+
46
+ THEME = gr.themes.Soft(
47
+ primary_hue=gr.themes.colors.indigo,
48
+ secondary_hue=gr.themes.colors.blue,
49
+ neutral_hue=gr.themes.colors.slate,
50
+ font=[gr.themes.GoogleFont("Roboto"), "system-ui", "sans-serif"],
51
+ ).set(
52
+ button_primary_background_fill=f"linear-gradient(90deg, {INDIGO}, {BLUE})",
53
+ button_primary_background_fill_hover=f"linear-gradient(90deg, {BLUE}, {INDIGO})",
54
+ button_primary_text_color="white",
55
+ block_title_text_weight="600",
56
+ )
57
+
58
+ CSS = f"""
59
+ #wf-hero {{
60
+ background: linear-gradient(120deg, {INDIGO}, {BLUE});
61
+ color: #fff; border-radius: 16px; padding: 26px 30px; margin-bottom: 8px;
62
+ }}
63
+ #wf-hero h1 {{ margin: 0 0 6px; font-weight: 700; letter-spacing: -0.5px; }}
64
+ #wf-hero p {{ margin: 0; opacity: 0.92; font-size: 1.02em; line-height: 1.5; }}
65
+ #wf-hero a {{ color: #fff; text-decoration: underline; }}
66
+ #wf-first {{ color: {SLATE}; font-weight: 600; }}
67
+ """
68
+
69
 
70
  def _label(t: dict) -> str:
71
  return f"{t['id']} Β· {t['title']}"
 
88
  score.format_score(c["display"]),
89
  score.due_label(t.get("due_date"), now) if t.get("due_date") else "β€”",
90
  flag,
91
+ score.explain(t, now) + (" Β· edited" if t.get("edited") else ""),
92
  round(c["I"]),
93
  round(c["R"]),
94
  round(t["effort_hours"], 2),
95
  ])
96
  top = ranked[0]
97
+ header = (
98
+ f"### <span id='wf-first'>β–Ά Do this first: {top['title']}</span>"
99
+ f" Β· score {score.format_score(score.priority(top))}"
100
+ )
101
  return header, rows, [_label(t) for t in ranked]
102
 
103
 
104
  def prioritize(text, image, tasks):
105
+ # A generator: the first yield lands instantly so the ~60s CPU inference
106
+ # doesn't look like a frozen app. Gradio streams each yield to the UI.
107
  if not llm.is_ready():
108
+ yield tasks, "### ⏳ The local model is still loading β€” give it a moment and try again.", [], gr.update()
109
+ return
110
+ hint = ""
111
+ if text and text.strip():
112
+ n = len([ln for ln in text.splitlines() if ln.strip()])
113
+ hint = f" ~{n} item{'s' if n != 1 else ''}"
114
+ yield tasks, f"### ⏳ Reading and scoring{hint} on the local model… (~60s on free CPU)", [], gr.update()
115
  collected = []
116
  if text and text.strip():
117
  collected += llm.parse_braindump(text)
 
120
  for i, t in enumerate(collected):
121
  t["id"] = f"t{i}"
122
  if not collected:
123
+ yield collected, "### Nothing actionable found β€” try a different dump or photo.", [], gr.update(choices=[], value=None)
124
+ return
125
  header, rows, choices = render(collected)
126
+ yield collected, header, rows, gr.update(choices=choices, value=(choices[0] if choices else None))
127
 
128
 
129
  def load_task(sel, tasks):
 
139
  t["impact"] = int(impact)
140
  t["readiness"] = int(readiness)
141
  t["effort_hours"] = float(effort)
142
+ t["edited"] = True
143
  header, rows, choices = render(tasks)
144
  keep = sel if sel in choices else (choices[0] if choices else None)
145
  return tasks, header, rows, gr.update(choices=choices, value=keep)
 
162
  return next((x for x in tasks if x["id"] == tid), None)
163
 
164
 
165
+ with gr.Blocks(title="whatfirst-small", theme=THEME, css=CSS) as demo:
166
+ gr.HTML(
167
+ "<div id='wf-hero'>"
168
+ "<h1>whatfirst Β· small</h1>"
169
+ "<p>Dump everything on your mind β€” or snap a photo of your to-do list β€” and a "
170
+ "<strong>small local model</strong> turns it into structured tasks. A "
171
+ "<strong>transparent scoring engine</strong> then tells you what to do <em>first</em>, "
172
+ "and shows its work. No cloud, no API keys. &nbsp;The full app lives at "
173
+ "<a href='https://what-first.com'>what-first.com</a>.</p>"
174
+ "</div>"
175
  )
176
  tasks_state = gr.State([])
177
 
 
186
  go = gr.Button("Prioritize", variant="primary")
187
  sample = gr.Button("Try a sample")
188
  with gr.Column(scale=3):
189
+ header = gr.Markdown(
190
+ "### Add a brain-dump or a photo, then hit **Prioritize**.",
191
+ sanitize_html=False,
192
+ )
193
  table = gr.Dataframe(
194
  headers=DF_HEADERS, datatype=DF_DATATYPES, interactive=False,
195
  wrap=True, label="Ranked",
llm.py CHANGED
@@ -63,6 +63,10 @@ def _chat(system: str, user_content, max_tokens: int) -> str:
63
  "top_p": 0.9,
64
  "stream": False,
65
  "response_format": {"type": "json_object"},
 
 
 
 
66
  }
67
  r = requests.post(CHAT_URL, json=payload, timeout=REQUEST_TIMEOUT)
68
  r.raise_for_status()
 
63
  "top_p": 0.9,
64
  "stream": False,
65
  "response_format": {"type": "json_object"},
66
+ # Keep the (identical, ~400-token) system-prompt prefix in the server's
67
+ # KV cache between calls, so repeated requests β€” the image flow's two
68
+ # passes, per-task re-suggest, back-to-back demos β€” skip re-processing it.
69
+ "cache_prompt": True,
70
  }
71
  r = requests.post(CHAT_URL, json=payload, timeout=REQUEST_TIMEOUT)
72
  r.raise_for_status()
prompts.py CHANGED
@@ -23,18 +23,15 @@ effort_hours: realistic focused time. 0.25 = 15 minutes, 1 = an hour, 8 = a day.
23
 
24
  PARSE_SYSTEM_PROMPT = f"""You turn a messy brain-dump into a prioritized task list for a personal productivity app. The text is one person listing things they need to do; it may ramble, run several tasks together, or include filler.
25
 
26
- Output a JSON object: {{ "items": [ {{ "title": string, "category": string | null, "notes": string | null, "due_date": string | null, "due_time": string | null, "impact": integer, "readiness": integer, "effort_hours": number, "reason": string }} ] }}
27
 
28
  {SCALE_GUIDE}
29
 
30
  Rules:
31
  - title is a short imperative phrase, sentence case, no trailing period, under 90 characters. Prefer the person's own words.
32
- - category is a 1-2 word project name if obvious (e.g. "Work", "Home", "Errands"); otherwise null.
33
- - notes is a clarifying detail that won't fit in the title; otherwise null.
34
  - due_date: if a day, date, or deadline is named ("Friday", "tomorrow", "by the 5th", "end of week"), resolve it to YYYY-MM-DD relative to the current date given below. Otherwise null.
35
  - due_time: if a clock time is named ("at 11pm", "by 9am", "noon"), resolve it to 24-hour HH:MM. If a day but no clock time, null. Never invent a time.
36
  - impact, readiness, effort_hours: always score every task using the scale above. If importance is stated ("really important", "no rush"), let it guide impact.
37
- - reason: one short lowercase sentence, no exclamations, under 14 words, explaining the scores.
38
  - Split distinct tasks into separate items. Skip greetings, filler, and self-talk that isn't a task.
39
  - Return at most 25 items. Drop near-duplicates.
40
  - If there are no actionable tasks, return {{ "items": [] }}.
 
23
 
24
  PARSE_SYSTEM_PROMPT = f"""You turn a messy brain-dump into a prioritized task list for a personal productivity app. The text is one person listing things they need to do; it may ramble, run several tasks together, or include filler.
25
 
26
+ Output a JSON object: {{ "items": [ {{ "title": string, "due_date": string | null, "due_time": string | null, "impact": integer, "readiness": integer, "effort_hours": number }} ] }}
27
 
28
  {SCALE_GUIDE}
29
 
30
  Rules:
31
  - title is a short imperative phrase, sentence case, no trailing period, under 90 characters. Prefer the person's own words.
 
 
32
  - due_date: if a day, date, or deadline is named ("Friday", "tomorrow", "by the 5th", "end of week"), resolve it to YYYY-MM-DD relative to the current date given below. Otherwise null.
33
  - due_time: if a clock time is named ("at 11pm", "by 9am", "noon"), resolve it to 24-hour HH:MM. If a day but no clock time, null. Never invent a time.
34
  - impact, readiness, effort_hours: always score every task using the scale above. If importance is stated ("really important", "no rush"), let it guide impact.
 
35
  - Split distinct tasks into separate items. Skip greetings, filler, and self-talk that isn't a task.
36
  - Return at most 25 items. Drop near-duplicates.
37
  - If there are no actionable tasks, return {{ "items": [] }}.
score.py CHANGED
@@ -22,10 +22,6 @@ PREP_BIAS = 0.7 # action bias on the prep branch (<1)
22
  SCORE_CAP = 1000 # backstop guard, above the formula's natural max
23
  TIE_BAND = 0.10 # value-closeness band (log-bucketed for transitivity)
24
 
25
- # Quick-win lens: a short, ready-to-start task for a spare moment.
26
- QUICK_WIN_MAX_EFFORT_HOURS = 0.25 # 15 minutes
27
- QUICK_WIN_MIN_READINESS = 7
28
-
29
 
30
  def clamp(x: float, lo: float, hi: float) -> float:
31
  return max(lo, min(hi, x))
@@ -66,13 +62,6 @@ def comp_urgency(d: float, e_hours: float) -> float:
66
  return min(30, base + accel)
67
 
68
 
69
- def urgency(due_date, effort_hours) -> float:
70
- if not due_date:
71
- return 2
72
- d = days_to_due_raw(due_date)
73
- return max(cal_urgency(d), comp_urgency(d, effort_hours or 0.05))
74
-
75
-
76
  # -- Core score ---------------------------------------------------------------
77
 
78
  def score_components(t: dict) -> dict:
@@ -115,24 +104,6 @@ def value_bucket(v: float) -> int:
115
  return math.floor(math.log(max(v, 0.1)) / math.log(1 + TIE_BAND))
116
 
117
 
118
- # -- Assignment (collapses to "always mine" with no teams) --------------------
119
-
120
- def assignee_ids(task: dict) -> list:
121
- ids = task.get("assignee_user_ids")
122
- if isinstance(ids, list):
123
- return ids
124
- if task.get("assignee_user_id") is not None:
125
- return [task["assignee_user_id"]]
126
- return []
127
-
128
-
129
- def mine_to_do(task: dict, current_user_id=None) -> bool:
130
- if current_user_id is None:
131
- return True
132
- ids = assignee_ids(task)
133
- return len(ids) == 0 or current_user_id in ids
134
-
135
-
136
  # -- Dates --------------------------------------------------------------------
137
 
138
  def normalize_iso(due_date):
@@ -180,7 +151,7 @@ def deadline_status(task: dict, now: datetime | None = None):
180
 
181
  # -- Ranking ------------------------------------------------------------------
182
 
183
- def deadline_risk_map(sorted_active: list, now: datetime | None = None, current_user_id=None) -> dict:
184
  """Walk the queue in priority order, accumulate effort, and flag any task
185
  whose projected wall-clock finish lands past its deadline. Returns
186
  {id: 'at-risk' | 'tight'}."""
@@ -190,8 +161,6 @@ def deadline_risk_map(sorted_active: list, now: datetime | None = None, current_
190
  for t in sorted_active:
191
  if not t or t.get("completed"):
192
  continue
193
- if not mine_to_do(t, current_user_id):
194
- continue
195
  E = max(t.get("effort_hours") or 0, 0)
196
  acc += E
197
  if not t.get("due_date"):
@@ -210,7 +179,7 @@ def deadline_risk_map(sorted_active: list, now: datetime | None = None, current_
210
  return out
211
 
212
 
213
- def rank_active(tasks: list, now: datetime | None = None, current_user_id=None) -> list:
214
  """Rank with deadlines as a constraint, not a blended term. Three tiers:
215
  0 overdue (EDF), 1 binding/at-risk (EDF, lifted above the value pack),
216
  2 value pack (value bucket, then sooner due breaks near-ties)."""
@@ -219,11 +188,9 @@ def rank_active(tasks: list, now: datetime | None = None, current_user_id=None)
219
  V = {t["id"]: priority(t) for t in active}
220
  D = {t["id"]: days_to_due_raw(t.get("due_date"), now) for t in active}
221
  by_value = sorted(active, key=lambda t: V[t["id"]], reverse=True)
222
- risk = deadline_risk_map(by_value, now, current_user_id)
223
 
224
  def tier_of(t):
225
- if not mine_to_do(t, current_user_id):
226
- return 2
227
  if t.get("due_date") and D[t["id"]] <= 0:
228
  return 0
229
  return 1 if risk.get(t["id"]) == "at-risk" else 2
@@ -239,25 +206,6 @@ def rank_active(tasks: list, now: datetime | None = None, current_user_id=None)
239
  return sorted(by_value, key=sort_key)
240
 
241
 
242
- # -- Quick-win lens -----------------------------------------------------------
243
-
244
- def is_quick_win(task: dict) -> bool:
245
- if not task or task.get("completed"):
246
- return False
247
- e = task.get("effort_hours")
248
- if not isinstance(e, (int, float)) or not math.isfinite(e) or e <= 0 or e > QUICK_WIN_MAX_EFFORT_HOURS:
249
- return False
250
- return readiness_of(task) >= QUICK_WIN_MIN_READINESS or bool(task.get("execute_anyway"))
251
-
252
-
253
- def quick_wins(tasks: list) -> list:
254
- """Quick wins, shortest first, ties by priority desc."""
255
- return sorted(
256
- (t for t in tasks if is_quick_win(t)),
257
- key=lambda t: (t["effort_hours"], -priority(t)),
258
- )
259
-
260
-
261
  # -- Display helpers ----------------------------------------------------------
262
 
263
  def format_score(n: float) -> str:
@@ -265,6 +213,33 @@ def format_score(n: float) -> str:
265
  return str(int(rounded)) if rounded == int(rounded) else f"{rounded:.1f}"
266
 
267
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
  def due_label(due_date, now: datetime | None = None) -> str:
269
  """Compact relative deadline label ('now', '3h', 'tomorrow', '2d', '5d late')."""
270
  if not due_date:
 
22
  SCORE_CAP = 1000 # backstop guard, above the formula's natural max
23
  TIE_BAND = 0.10 # value-closeness band (log-bucketed for transitivity)
24
 
 
 
 
 
25
 
26
  def clamp(x: float, lo: float, hi: float) -> float:
27
  return max(lo, min(hi, x))
 
62
  return min(30, base + accel)
63
 
64
 
 
 
 
 
 
 
 
65
  # -- Core score ---------------------------------------------------------------
66
 
67
  def score_components(t: dict) -> dict:
 
104
  return math.floor(math.log(max(v, 0.1)) / math.log(1 + TIE_BAND))
105
 
106
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  # -- Dates --------------------------------------------------------------------
108
 
109
  def normalize_iso(due_date):
 
151
 
152
  # -- Ranking ------------------------------------------------------------------
153
 
154
+ def deadline_risk_map(sorted_active: list, now: datetime | None = None) -> dict:
155
  """Walk the queue in priority order, accumulate effort, and flag any task
156
  whose projected wall-clock finish lands past its deadline. Returns
157
  {id: 'at-risk' | 'tight'}."""
 
161
  for t in sorted_active:
162
  if not t or t.get("completed"):
163
  continue
 
 
164
  E = max(t.get("effort_hours") or 0, 0)
165
  acc += E
166
  if not t.get("due_date"):
 
179
  return out
180
 
181
 
182
+ def rank_active(tasks: list, now: datetime | None = None) -> list:
183
  """Rank with deadlines as a constraint, not a blended term. Three tiers:
184
  0 overdue (EDF), 1 binding/at-risk (EDF, lifted above the value pack),
185
  2 value pack (value bucket, then sooner due breaks near-ties)."""
 
188
  V = {t["id"]: priority(t) for t in active}
189
  D = {t["id"]: days_to_due_raw(t.get("due_date"), now) for t in active}
190
  by_value = sorted(active, key=lambda t: V[t["id"]], reverse=True)
191
+ risk = deadline_risk_map(by_value, now)
192
 
193
  def tier_of(t):
 
 
194
  if t.get("due_date") and D[t["id"]] <= 0:
195
  return 0
196
  return 1 if risk.get(t["id"]) == "at-risk" else 2
 
206
  return sorted(by_value, key=sort_key)
207
 
208
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  # -- Display helpers ----------------------------------------------------------
210
 
211
  def format_score(n: float) -> str:
 
213
  return str(int(rounded)) if rounded == int(rounded) else f"{rounded:.1f}"
214
 
215
 
216
+ def explain(t: dict, now: datetime | None = None) -> str:
217
+ """A short, deterministic 'why it ranks here' phrase, read off the same
218
+ components that drive the score. This replaces a model-written reason: it
219
+ costs the model no tokens to produce and can never drift from the math the
220
+ way an LLM sentence can."""
221
+ now = now or datetime.now()
222
+ c = score_components(t)
223
+ I, R, E = c["I"], c["R"], c["E"]
224
+ status = deadline_status(t, now)
225
+ if status == "overdue":
226
+ return "overdue β€” start now"
227
+ if c["prep_wins"]:
228
+ return ("high-value but not ready β€” de-risk first" if I >= 5
229
+ else "not ready β€” needs a first step")
230
+ if status in ("today", "tomorrow"):
231
+ return f"due {status} β€” time-sensitive"
232
+ if E <= 1 and R >= 7:
233
+ return "quick win β€” short and ready"
234
+ if R <= 3:
235
+ return "blocked β€” next step unclear"
236
+ if I >= 8:
237
+ return "high-impact work"
238
+ if I <= 3:
239
+ return "minor β€” low impact"
240
+ return "steady priority"
241
+
242
+
243
  def due_label(due_date, now: datetime | None = None) -> str:
244
  """Compact relative deadline label ('now', '3h', 'tomorrow', '2d', '5d late')."""
245
  if not due_date:
start.sh CHANGED
@@ -14,7 +14,7 @@ echo "[start] launching llama-server"
14
  --model "${MODEL_DIR}/${MODEL_FILE}" \
15
  --mmproj "${MODEL_DIR}/${MMPROJ_FILE}" \
16
  --host 127.0.0.1 --port 8080 \
17
- --ctx-size 8192 \
18
  --threads "$(nproc)" &
19
 
20
  # 3. Start the Gradio app (foreground). is_ready() polls the server's /health,
 
14
  --model "${MODEL_DIR}/${MODEL_FILE}" \
15
  --mmproj "${MODEL_DIR}/${MMPROJ_FILE}" \
16
  --host 127.0.0.1 --port 8080 \
17
+ --ctx-size 4096 \
18
  --threads "$(nproc)" &
19
 
20
  # 3. Start the Gradio app (foreground). is_ready() polls the server's /health,
test_score.py CHANGED
@@ -123,19 +123,6 @@ def test_completed_tasks_excluded():
123
  assert order == ["live"]
124
 
125
 
126
- # -- Quick wins ---------------------------------------------------------------
127
-
128
- def test_quick_win_filter_and_order():
129
- tasks = [
130
- {"id": "a", "impact": 5, "readiness": 9, "effort_hours": 0.1}, # qualifies
131
- {"id": "b", "impact": 9, "readiness": 9, "effort_hours": 0.05}, # qualifies, shorter
132
- {"id": "c", "impact": 5, "readiness": 3, "effort_hours": 0.1}, # too unready
133
- {"id": "d", "impact": 5, "readiness": 9, "effort_hours": 2}, # too long
134
- ]
135
- order = [t["id"] for t in s.quick_wins(tasks)]
136
- assert order == ["b", "a"], order # shortest first
137
-
138
-
139
  def test_due_label():
140
  assert s.due_label("2026-06-08T12:00:00", NOW) == "tomorrow"
141
  assert s.due_label("2026-06-12T12:00:00", NOW) == "5d"
 
123
  assert order == ["live"]
124
 
125
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  def test_due_label():
127
  assert s.due_label("2026-06-08T12:00:00", NOW) == "tomorrow"
128
  assert s.due_label("2026-06-12T12:00:00", NOW) == "5d"