@rm0nroe/coach-claw 1.0.7 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -383,12 +383,16 @@ def test_celebrate_block_includes_verbatim_instruction(cup, now):
383
383
  def test_celebrate_combines_all_event_kinds(cup, now):
384
384
  """Regression + streak + graduation + level-up — verify ordering and
385
385
  that all four sections are present without bleeding into each other."""
386
+ # Use slug-form ids whose humanized fallback (`-` → space) matches
387
+ # the asserted display text. display_name() is now the single
388
+ # source of truth — marker `name` is ignored, so synthetic ids
389
+ # without an override render via humanized-slug fallback.
386
390
  block = cup._assemble_celebrate_block(
387
- grads=[{"id": "g1", "name": "pattern g", "direction": "positive",
391
+ grads=[{"id": "pattern-g", "name": "pattern g", "direction": "positive",
388
392
  "graduated_reason": "present-5-runs"}],
389
- regs=[{"id": "r1", "name": "pattern r",
393
+ regs=[{"id": "pattern-r", "name": "pattern r",
390
394
  "originally_graduated_at": "2026-04-01"}],
391
- streak_rewards=[{"id": "s1", "name": "pattern s",
395
+ streak_rewards=[{"id": "pattern-s", "name": "pattern s",
392
396
  "direction": "negative", "streak": 2, "target": 5,
393
397
  "xp_awarded": 1}],
394
398
  levelup={"from": "L3 X", "to": "Y", "to_idx": 3, "xp_at_levelup": 100},
@@ -146,9 +146,10 @@ def test_regression_terminal_uses_blockquote(cup):
146
146
  "originally_graduated_at": "2026-04-01"}],
147
147
  env="terminal",
148
148
  )
149
- # Verbatim banner: name (not slug) appears in the heading; the
150
- # graduation date is interpolated into the body sentence.
151
- assert "> ⚠️ **Regressed: Edits without testing**" in block
149
+ # Verbatim banner: display_name override wins over marker name, so
150
+ # the heading shows the curated wording. Graduation date is
151
+ # interpolated into the body sentence.
152
+ assert "> ⚠️ **Regressed: edits without testing**" in block
152
153
  assert "(was graduated 2026-04-01)" in block
153
154
  assert "edits-without-testing" not in block # slug must not leak
154
155
  assert "---" not in block
@@ -160,7 +161,7 @@ def test_regression_ide_uses_hr_frame(cup):
160
161
  "originally_graduated_at": "2026-04-01"}],
161
162
  env="ide",
162
163
  )
163
- assert "⚠️ **Regressed** — `Edits without testing`" in block
164
+ assert "⚠️ **Regressed** — `edits without testing`" in block
164
165
  assert "edits-without-testing" not in block # slug must not leak
165
166
  assert block.startswith("---\n")
166
167
  assert block.endswith("\n---")
@@ -292,6 +293,46 @@ def test_graduation_ide_positive(cup):
292
293
  # attribution — graduation ceremonies get bespoke colors.
293
294
  # -----------------------------------------------------------------------------
294
295
 
296
+ def test_curated_override_wins_over_marker_name(cup):
297
+ """Regression: when a marker carries a `name` that differs from the
298
+ curated WORDING_OVERRIDES entry for that slug, the override MUST win.
299
+ The teammate-found bug was the milestone renderers preferring marker
300
+ name over display_name's resolution chain — defeating the natural-
301
+ language override contract for known awkward phrases."""
302
+ # `commit-without-testing` carries marker name "commit without
303
+ # testing" (analyze.py:350), but the curated override is the richer
304
+ # "committing without testing".
305
+ streak_block = cup._streak_reward_block(
306
+ [{"id": "commit-without-testing", "name": "commit without testing",
307
+ "direction": "negative", "streak": 3, "target": 5, "xp_awarded": 1}],
308
+ env="terminal",
309
+ )
310
+ assert "committing without testing" in streak_block, (
311
+ "streak reward banner ignored the curated override"
312
+ )
313
+ assert "`commit without testing`" not in streak_block, (
314
+ "marker name leaked through despite override match"
315
+ )
316
+
317
+ grad_block = cup._graduation_block(
318
+ [{"id": "commit-without-testing", "name": "commit without testing",
319
+ "direction": "negative", "graduated_reason": "absent-5-runs"}],
320
+ env="terminal",
321
+ )
322
+ assert "GRADUATED: committing without testing" in grad_block, (
323
+ "graduation banner ignored the curated override"
324
+ )
325
+
326
+ reg_block = cup._regression_block(
327
+ [{"id": "commit-without-testing", "name": "commit without testing",
328
+ "originally_graduated_at": "2026-04-01"}],
329
+ env="terminal",
330
+ )
331
+ assert "Regressed: committing without testing" in reg_block, (
332
+ "regression banner ignored the curated override"
333
+ )
334
+
335
+
295
336
  def test_graduation_negative_full_bar_is_yellow(cup):
296
337
  block = cup._graduation_block(
297
338
  [{"id": "edits-without-testing", "name": "edits without testing",
@@ -574,12 +574,11 @@ def _regression_block(
574
574
  if not isinstance(r, dict):
575
575
  continue
576
576
  rid = r.get("id", "?")
577
+ # display_name is the single source of truth: curated override →
578
+ # profile.name → humanized slug. Marker name is intentionally
579
+ # ignored here so a curated WORDING_OVERRIDES entry always wins
580
+ # over whatever wording the marker happened to carry at write time.
577
581
  rname = _display_name(rid, profile) if rid != "?" else rid
578
- # Marker may carry richer wording than display_name resolves;
579
- # prefer the explicit name field when present and non-slug.
580
- marker_name = r.get("name")
581
- if marker_name and marker_name != rid:
582
- rname = marker_name
583
582
  originally_at = r.get("originally_graduated_at", "?")
584
583
  sentence = (
585
584
  f"Re-detected this run, so it's off the mastered list "
@@ -608,10 +607,10 @@ def _streak_reward_block(
608
607
  if not isinstance(r, dict):
609
608
  continue
610
609
  rid = r.get("id", "?")
610
+ # See _regression_block: display_name is authoritative; the
611
+ # marker's `name` field is intentionally ignored so curated
612
+ # overrides win over whatever the marker carried at write time.
611
613
  rname = _display_name(rid, profile) if rid != "?" else rid
612
- marker_name = r.get("name")
613
- if marker_name and marker_name != rid:
614
- rname = marker_name
615
614
  streak = int(r.get("streak", 0))
616
615
  target = int(r.get("target", 5))
617
616
  xp = int(r.get("xp_awarded", 1))
@@ -652,10 +651,10 @@ def _graduation_block(
652
651
  if not isinstance(g, dict):
653
652
  continue
654
653
  gid = g.get("id", "?")
654
+ # See _regression_block: display_name is authoritative; the
655
+ # marker's `name` field is intentionally ignored so curated
656
+ # overrides win over whatever the marker carried at write time.
655
657
  gname = _display_name(gid, profile) if gid != "?" else gid
656
- marker_name = g.get("name")
657
- if marker_name and marker_name != gid:
658
- gname = marker_name
659
658
  direction = g.get("direction", "negative")
660
659
  if direction == "positive":
661
660
  sentence = positive_sentence
@@ -749,21 +748,17 @@ def _assemble_celebrate_block(
749
748
  graduated_ids = {g.get("id") for g in (grads or []) if isinstance(g, dict) and g.get("id")}
750
749
  streak_rewards = [s for s in streak_rewards if s.get("id") not in graduated_ids]
751
750
 
752
- # Pass C: normalize each reward's `name` field via display_name so both
753
- # the default-theme and bespoke-theme render paths see user-facing
754
- # wording (override → profile.name → humanized slug). The bespoke path
755
- # in banner_themes.py reads `r["name"]` directly without any
756
- # display-name lookup; without this pass it would leak slugs (e.g.,
757
- # `under-planning` if profile.name is broken). We rebuild dicts to
758
- # avoid mutating the marker payload.
751
+ # Pass C: normalize each reward's `name` field via display_name so
752
+ # both the default-theme and bespoke-theme render paths see the same
753
+ # user-facing wording (override → profile.name → humanized slug).
754
+ # display_name is authoritative — the marker's own `name` field is
755
+ # ignored so a curated WORDING_OVERRIDES entry always wins over
756
+ # whatever wording the marker carried at write time. We rebuild
757
+ # dicts to avoid mutating the marker payload.
759
758
  normalized: list[dict] = []
760
759
  for s in streak_rewards:
761
760
  sid = s.get("id", "?")
762
- marker_name = s.get("name")
763
- if marker_name and marker_name != sid:
764
- display = marker_name
765
- else:
766
- display = _display_name(sid, profile) if sid != "?" else sid
761
+ display = _display_name(sid, profile) if sid != "?" else sid
767
762
  normalized.append({**s, "name": display})
768
763
  streak_rewards = normalized
769
764
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rm0nroe/coach-claw",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "A self-evolving coaching layer for Claude Code.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://rm0nroe.github.io/coach-claw/",