@seanyao/roll 2026.522.2 → 2026.523.2
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.
- package/CHANGELOG.md +46 -0
- package/bin/dream-test-quality-scan +110 -0
- package/bin/roll +761 -82
- package/lib/__pycache__/model_prices.cpython-314.pyc +0 -0
- package/lib/__pycache__/prices_fetcher.cpython-314.pyc +0 -0
- package/lib/__pycache__/roll-loop-status.cpython-314.pyc +0 -0
- package/lib/__pycache__/roll_render.cpython-314.pyc +0 -0
- package/lib/i18n.sh +113 -0
- package/lib/loop-fmt.py +62 -3
- package/lib/model_prices.py +78 -38
- package/lib/prices/snapshot-2026-05-22.json +20 -0
- package/lib/prices_fetcher.py +285 -0
- package/lib/roll-loop-status.py +88 -48
- package/lib/roll_render.py +20 -8
- package/package.json +1 -1
- package/skills/roll-.dream/SKILL.md +59 -0
- package/skills/roll-design/SKILL.md +4 -3
- package/skills/roll-notes/SKILL.md +6 -3
package/lib/roll-loop-status.py
CHANGED
|
@@ -153,7 +153,11 @@ def load_backlog(project_root: Optional[Path] = None) -> Dict[str, str]:
|
|
|
153
153
|
# ════════════════════════════════════════════════════════════════════════════
|
|
154
154
|
# Cycle aggregation — group events by cycle label; attach cron + story id
|
|
155
155
|
# ════════════════════════════════════════════════════════════════════════════
|
|
156
|
-
|
|
156
|
+
# FIX-108: each segment was [A-Z]+ (letters only), so alphanumeric segments
|
|
157
|
+
# like I18N / K8S / D2 / S3 / 2FA failed to match — dashboard silently dropped
|
|
158
|
+
# any story id with a mixed-letter-digit segment (US-I18N-001 etc.). First
|
|
159
|
+
# char must still be a letter so "001-002" doesn't false-positive as an id.
|
|
160
|
+
_STORY_ID_PAT = re.compile(r"\b([A-Z][A-Z0-9]*(?:-[A-Z][A-Z0-9]*)*-\d+)\b")
|
|
157
161
|
_PR_NUM_PAT = re.compile(r"/pull/(\d+)")
|
|
158
162
|
|
|
159
163
|
def _extract_story_id(ev_detail: str) -> Optional[str]:
|
|
@@ -356,21 +360,29 @@ def backfill_usage_from_claude_sessions(cycles: List[Dict[str, Any]], slug: str)
|
|
|
356
360
|
# Path 1: usage event written by loop-fmt at result time.
|
|
357
361
|
ue = cy.get("usage_event")
|
|
358
362
|
if isinstance(ue, dict) and (ue.get("input_tokens") or ue.get("output_tokens")):
|
|
359
|
-
cy["input_tokens"]
|
|
360
|
-
cy["output_tokens"]
|
|
363
|
+
cy["input_tokens"] = int(ue.get("input_tokens") or 0)
|
|
364
|
+
cy["output_tokens"] = int(ue.get("output_tokens") or 0)
|
|
365
|
+
cy["cache_creation_tokens"] = int(ue.get("cache_creation_tokens") or 0)
|
|
366
|
+
cy["cache_read_tokens"] = int(ue.get("cache_read_tokens") or 0)
|
|
361
367
|
cy["model"] = ue.get("model")
|
|
362
|
-
# US-VIEW-
|
|
363
|
-
#
|
|
364
|
-
#
|
|
365
|
-
#
|
|
366
|
-
#
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
368
|
+
# US-VIEW-014: prefer the cost frozen at cycle_end so a later
|
|
369
|
+
# prices refresh never rewrites a historical cycle's cost. Only
|
|
370
|
+
# legacy events (pre-US-VIEW-014) fall back to recomputing — and
|
|
371
|
+
# the row gets a muted [legacy] tag so it can't be mistaken for
|
|
372
|
+
# the authoritative value.
|
|
373
|
+
persisted = ue.get("cost_list_usd")
|
|
374
|
+
if persisted is not None:
|
|
375
|
+
cy["cost_list"] = float(persisted)
|
|
376
|
+
cy["cost_list_legacy"] = False
|
|
377
|
+
else:
|
|
378
|
+
cy["cost_list"] = mp.compute_list_cost(
|
|
379
|
+
ue.get("model"),
|
|
380
|
+
input_tokens=ue.get("input_tokens", 0),
|
|
381
|
+
output_tokens=ue.get("output_tokens", 0),
|
|
382
|
+
cache_creation_tokens=ue.get("cache_creation_tokens", 0),
|
|
383
|
+
cache_read_tokens=ue.get("cache_read_tokens", 0),
|
|
384
|
+
)
|
|
385
|
+
cy["cost_list_legacy"] = True
|
|
374
386
|
if ue.get("duration_ms") and not cy.get("duration_s"):
|
|
375
387
|
cy["duration_s"] = int(ue["duration_ms"] / 1000)
|
|
376
388
|
continue
|
|
@@ -380,8 +392,10 @@ def backfill_usage_from_claude_sessions(cycles: List[Dict[str, Any]], slug: str)
|
|
|
380
392
|
u = load_claude_session_usage(cy.get("label", ""), slug)
|
|
381
393
|
if not u:
|
|
382
394
|
continue
|
|
383
|
-
cy["input_tokens"]
|
|
384
|
-
cy["output_tokens"]
|
|
395
|
+
cy["input_tokens"] = int(u.get("input_tokens") or 0)
|
|
396
|
+
cy["output_tokens"] = int(u.get("output_tokens") or 0)
|
|
397
|
+
cy["cache_creation_tokens"] = int(u.get("cache_creation_tokens") or 0)
|
|
398
|
+
cy["cache_read_tokens"] = int(u.get("cache_read_tokens") or 0)
|
|
385
399
|
cy["model"] = u["model"]
|
|
386
400
|
cy["cost_list"] = mp.compute_list_cost(
|
|
387
401
|
u["model"],
|
|
@@ -390,25 +404,39 @@ def backfill_usage_from_claude_sessions(cycles: List[Dict[str, Any]], slug: str)
|
|
|
390
404
|
cache_creation_tokens=u["cache_creation_tokens"],
|
|
391
405
|
cache_read_tokens=u["cache_read_tokens"],
|
|
392
406
|
)
|
|
407
|
+
# US-VIEW-014: session salvage never has a frozen cycle_end cost, so
|
|
408
|
+
# this path is always legacy.
|
|
409
|
+
cy["cost_list_legacy"] = True
|
|
393
410
|
if u.get("duration_ms") and not cy.get("duration_s"):
|
|
394
411
|
cy["duration_s"] = int(u["duration_ms"] / 1000)
|
|
395
412
|
|
|
396
413
|
def load_pr_merges_from_git(days: int) -> Dict[str, Dict[str, Any]]:
|
|
397
414
|
"""Repair fallback: when events.ndjson dropped the pr / cycle_end events
|
|
398
|
-
for a cycle (events writer regressions
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
415
|
+
for a cycle (events writer regressions, or cycle_end fired before PR
|
|
416
|
+
merged), git log still has the merge commit. Two known subject formats:
|
|
417
|
+
|
|
418
|
+
- Branch-named (Merge commit / older squash): "Merge pull request #N
|
|
419
|
+
from seanyao/loop/cycle-LABEL" — the branch name carries the label.
|
|
420
|
+
- Squash with default-title (newer GitHub UI / `gh pr merge --squash`):
|
|
421
|
+
"loop cycle LABEL (#N)" — space-separated, no slash.
|
|
422
|
+
|
|
423
|
+
FIX-107: the old --grep="loop/cycle-" + label_re missed the squash
|
|
424
|
+
subject entirely, so PRs merged AFTER cycle_end never got their
|
|
425
|
+
pr_outcome promoted to 'merged' on the dashboard.
|
|
426
|
+
"""
|
|
402
427
|
try:
|
|
403
428
|
out = subprocess.check_output(
|
|
404
429
|
["git", "log", f"--since={days + 1} days ago",
|
|
405
|
-
"--grep=loop/cycle
|
|
430
|
+
"--grep=loop[ /]cycle", "--extended-regexp",
|
|
431
|
+
"--format=%H|||%s|||%b<<<END>>>"],
|
|
406
432
|
text=True, errors="ignore"
|
|
407
433
|
)
|
|
408
434
|
except Exception:
|
|
409
435
|
return {}
|
|
410
436
|
result: Dict[str, Dict[str, Any]] = {}
|
|
411
|
-
|
|
437
|
+
# Accept both `loop/cycle-LABEL` and `loop cycle LABEL` (with or without
|
|
438
|
+
# the leading `-` separator after `cycle`). LABEL = YYYYMMDD-HHMMSS-PID.
|
|
439
|
+
label_re = re.compile(r"loop[ /]cycle[-\s](\d{8}-\d+-\d+)")
|
|
412
440
|
pr_re = re.compile(r"#(\d+)")
|
|
413
441
|
story_re = re.compile(r"\b([A-Z]+(?:-[A-Z]+)*-\d+)\b")
|
|
414
442
|
for chunk in out.split("<<<END>>>"):
|
|
@@ -557,7 +585,8 @@ def rollup_for_day(day_cycles: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
|
557
585
|
# reads all 4 fields), but they don't represent the model's actual work.
|
|
558
586
|
r = {"cycles": len(day_cycles), "prs": 0, "failed": 0,
|
|
559
587
|
"duration_s": 0, "cost": 0.0,
|
|
560
|
-
"input_tokens": 0, "output_tokens": 0
|
|
588
|
+
"input_tokens": 0, "output_tokens": 0,
|
|
589
|
+
"cache_creation_tokens": 0, "cache_read_tokens": 0}
|
|
561
590
|
for cy in day_cycles:
|
|
562
591
|
if cy.get("outcome") == "fail":
|
|
563
592
|
r["failed"] += 1
|
|
@@ -567,6 +596,10 @@ def rollup_for_day(day_cycles: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
|
567
596
|
r["input_tokens"] += cy["input_tokens"]
|
|
568
597
|
if cy.get("output_tokens"):
|
|
569
598
|
r["output_tokens"] += cy["output_tokens"]
|
|
599
|
+
if cy.get("cache_creation_tokens"):
|
|
600
|
+
r["cache_creation_tokens"] += cy["cache_creation_tokens"]
|
|
601
|
+
if cy.get("cache_read_tokens"):
|
|
602
|
+
r["cache_read_tokens"] += cy["cache_read_tokens"]
|
|
570
603
|
# US-VIEW-011: rollup only counts cycles whose PR actually merged.
|
|
571
604
|
# Backward compat: rows where pr_outcome is missing but pr URL exists
|
|
572
605
|
# (no `pr` event after the writer upgrade ran for that cycle) are
|
|
@@ -634,10 +667,13 @@ def render(events, cron, state, backlog, *, days=3, lang="both", now=None,
|
|
|
634
667
|
c("dim", "run ") + c("fg", "roll loop on", bold=True) +
|
|
635
668
|
c("dim", " to enable"))
|
|
636
669
|
eb_zh = c("dim", " 未安装 · 运行 ") + c("fg", "roll loop on") + c("dim", " 启用")
|
|
637
|
-
elif install_state
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
670
|
+
elif install_state in ("stale", "disabled"):
|
|
671
|
+
# FIX-098: 'stale' = plist on disk but agent not registered in launchd.
|
|
672
|
+
# 'disabled' kept for back-compat (old install_state values). Both mean
|
|
673
|
+
# the user needs to run 'roll loop on' to bootstrap the agent.
|
|
674
|
+
eb_l = (c("amber", "◌ STALE — plist present, not loaded", bold=True) + c("muted", " ") +
|
|
675
|
+
c("dim", "run ") + c("fg", "roll loop on", bold=True) + c("dim", " to repair"))
|
|
676
|
+
eb_zh = c("dim", " Plist 存在但未加载 · 运行 ") + c("fg", "roll loop on") + c("dim", " 修复")
|
|
641
677
|
else:
|
|
642
678
|
eb_l = (c("blue", "● IDLE", bold=True) + c("muted", " · ") +
|
|
643
679
|
c("dim", "enabled · next run ") + c("fg", _next_cron_hint(state), bold=True))
|
|
@@ -723,11 +759,12 @@ def render(events, cron, state, backlog, *, days=3, lang="both", now=None,
|
|
|
723
759
|
yest_color="amber" if yest["failed"] > 0 else "dim",
|
|
724
760
|
yest_suffix="⚠" if yest["failed"] > 0 else "")
|
|
725
761
|
metric_dur("duration", today["duration_s"], yest["duration_s"], d2["duration_s"], partial=is_partial)
|
|
726
|
-
# US-VIEW-
|
|
727
|
-
#
|
|
728
|
-
#
|
|
729
|
-
# processed and generated for this cycle.
|
|
762
|
+
# US-VIEW-017: show all 4 token components so the cost is explainable.
|
|
763
|
+
# cache_creation (↑) and cache_read (↓) typically account for 80-90% of
|
|
764
|
+
# cost — hiding them makes the cost line incomprehensible.
|
|
730
765
|
metric_tokens("input tokens", today["input_tokens"], yest["input_tokens"], d2["input_tokens"], partial=is_partial)
|
|
766
|
+
metric_tokens("cache writes", today["cache_creation_tokens"], yest["cache_creation_tokens"], d2["cache_creation_tokens"], partial=is_partial)
|
|
767
|
+
metric_tokens("cache reads", today["cache_read_tokens"], yest["cache_read_tokens"], d2["cache_read_tokens"], partial=is_partial)
|
|
731
768
|
metric_tokens("output tokens", today["output_tokens"], yest["output_tokens"], d2["output_tokens"], partial=is_partial)
|
|
732
769
|
metric_dollar("cost", today["cost"], yest["cost"], d2["cost"], partial=is_partial)
|
|
733
770
|
|
|
@@ -784,15 +821,18 @@ def _read_plist_loop_minute() -> int:
|
|
|
784
821
|
|
|
785
822
|
|
|
786
823
|
def _detect_install_state() -> str:
|
|
787
|
-
"""FIX-095: classify the launchd install state of the loop service.
|
|
824
|
+
"""FIX-095 / FIX-098: classify the launchd install state of the loop service.
|
|
788
825
|
|
|
789
826
|
Returns one of:
|
|
790
827
|
'not-installed' — no plist for com.roll.loop.<slug> in ~/Library/LaunchAgents/
|
|
791
|
-
'
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
828
|
+
'stale' — plist on disk but agent NOT registered in launchd
|
|
829
|
+
(happens after roll loop off + roll update without roll loop on)
|
|
830
|
+
'enabled' — plist on disk AND registered in launchd
|
|
831
|
+
|
|
832
|
+
FIX-098: switched from `launchctl print-disabled` (disabled-overrides DB) to
|
|
833
|
+
`launchctl print gui/<uid>/<label>` which probes the actual launchd registry.
|
|
834
|
+
The old approach returned false-positive 'enabled' when the disabled-overrides
|
|
835
|
+
DB had no entry for the label (empty = not explicitly disabled, not loaded).
|
|
796
836
|
"""
|
|
797
837
|
slug = project_slug()
|
|
798
838
|
label = f"com.roll.loop.{slug}"
|
|
@@ -801,17 +841,17 @@ def _detect_install_state() -> str:
|
|
|
801
841
|
return "not-installed"
|
|
802
842
|
try:
|
|
803
843
|
uid = os.getuid()
|
|
804
|
-
|
|
805
|
-
["launchctl", "print
|
|
806
|
-
capture_output=True,
|
|
807
|
-
)
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
844
|
+
result = subprocess.run(
|
|
845
|
+
["launchctl", "print", f"gui/{uid}/{label}"],
|
|
846
|
+
capture_output=True, timeout=2,
|
|
847
|
+
)
|
|
848
|
+
if result.returncode == 0:
|
|
849
|
+
return "enabled"
|
|
850
|
+
return "stale"
|
|
811
851
|
except Exception:
|
|
812
|
-
# launchctl missing or timed out —
|
|
813
|
-
|
|
814
|
-
|
|
852
|
+
# launchctl missing or timed out — assume stale (safe: user sees STALE
|
|
853
|
+
# banner and is told to run 'roll loop on' to repair).
|
|
854
|
+
return "stale"
|
|
815
855
|
|
|
816
856
|
|
|
817
857
|
def _next_cron_hint(state: Dict[str, str], zh: bool = False) -> str:
|
package/lib/roll_render.py
CHANGED
|
@@ -298,12 +298,19 @@ def cycle_row(cy: Dict[str, Any], backlog: Dict[str, str]) -> None:
|
|
|
298
298
|
from datetime import datetime as _dt, timezone as _tz
|
|
299
299
|
dur_s = int((_dt.now(_tz.utc) - cy["start"]).total_seconds())
|
|
300
300
|
dur = fmt_dur(dur_s) if dur_s else "—"
|
|
301
|
-
# US-VIEW-
|
|
302
|
-
#
|
|
303
|
-
#
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
301
|
+
# US-VIEW-017: show all 4 token components when cache data is available.
|
|
302
|
+
# Format: "in/cw↑ cr↓/out" (cache writes ↑, cache reads ↓).
|
|
303
|
+
# Falls back to "in/out" for cycles that predate cache tracking.
|
|
304
|
+
inp = cy.get('input_tokens') or 0
|
|
305
|
+
out_tok = cy.get('output_tokens') or 0
|
|
306
|
+
cw = cy.get('cache_creation_tokens') or 0
|
|
307
|
+
cr = cy.get('cache_read_tokens') or 0
|
|
308
|
+
if cw or cr:
|
|
309
|
+
tok = (f"{fmt_tokens(inp)}"
|
|
310
|
+
f"/{fmt_tokens(cw)}↑ {fmt_tokens(cr)}↓"
|
|
311
|
+
f"/{fmt_tokens(out_tok)}")
|
|
312
|
+
else:
|
|
313
|
+
tok = f"{fmt_tokens(inp)}/{fmt_tokens(out_tok)}"
|
|
307
314
|
# cost prefers the backfilled list-price; falls back to cron.log when
|
|
308
315
|
# the claude session log isn't available (only the latest cycle).
|
|
309
316
|
if cy.get("cost_list") is not None:
|
|
@@ -343,14 +350,19 @@ def cycle_row(cy: Dict[str, Any], backlog: Dict[str, str]) -> None:
|
|
|
343
350
|
"open": ("dim", "…"),
|
|
344
351
|
}.get(pr_outcome, ("dim", "…"))
|
|
345
352
|
pr_marker = " " + c(mark_c, f"#{pr_num} {mark_sym}")
|
|
353
|
+
# US-VIEW-014: pre-US-VIEW-014 events (no frozen cost_list_usd at
|
|
354
|
+
# cycle_end) get a muted [legacy] suffix — the number is recomputed on
|
|
355
|
+
# the fly and can shift with future price changes, unlike the frozen
|
|
356
|
+
# values written by current loop-fmt.
|
|
357
|
+
legacy_marker = " " + c("muted", "[legacy]") if cy.get("cost_list_legacy") else ""
|
|
346
358
|
inner = (
|
|
347
359
|
" " + c(glyph_c, glyph, bold=True) + " " +
|
|
348
360
|
c(time_c, pad(time_str, 5), bold=(outcome == "fail")) + " " +
|
|
349
361
|
c("muted", pad(dur, 4, "r")) + " " +
|
|
350
|
-
c("muted", pad(tok,
|
|
362
|
+
c("muted", pad(tok, 26)) + " " +
|
|
351
363
|
model_seg +
|
|
352
364
|
c("muted", pad(cost, 7, "r")) + " " +
|
|
353
|
-
c(sid_c, ids_str, bold=True) + pr_marker
|
|
365
|
+
c(sid_c, ids_str, bold=True) + pr_marker + legacy_marker
|
|
354
366
|
)
|
|
355
367
|
# Subtle red bg on failure rows so a fail can't be missed at a glance.
|
|
356
368
|
if outcome == "fail" and USE_COLOR:
|
package/package.json
CHANGED
|
@@ -224,6 +224,65 @@ Add after `## 文档覆盖度` section:
|
|
|
224
224
|
{发现内容列表 或 "文档新鲜度良好,无滞后或缺失项。"}
|
|
225
225
|
```
|
|
226
226
|
|
|
227
|
+
### Scan 7 — Test Quality (rubric-driven)
|
|
228
|
+
|
|
229
|
+
Apply the test-quality rubric at [guide/en/testing/quality-rubric.md](../../guide/en/testing/quality-rubric.md)
|
|
230
|
+
(Chinese: [quality-rubric.zh.md](../../guide/zh/testing/quality-rubric.md)) against every file under
|
|
231
|
+
`tests/`. The rubric publishes six anti-pattern categories (❶..❻); each has a
|
|
232
|
+
**Signals** subsection that lists the matching heuristics. Scan 7 is purely a
|
|
233
|
+
mechanical apply-the-rubric step — no new logic.
|
|
234
|
+
|
|
235
|
+
**Per-category signals** — read from the rubric, summarized here:
|
|
236
|
+
|
|
237
|
+
| Marker | Anti-pattern | Cheapest signal |
|
|
238
|
+
|--------|--------------|-----------------|
|
|
239
|
+
| ❶ | Hardcoded business data | Bare numeric / version / pricing literal inside `[[ "$output" == *"..."*` that matches a value also defined in `lib/` |
|
|
240
|
+
| ❷ | Over-mocking real boundaries | `function git() {` / `function gh() {` overrides at the top of a unit test |
|
|
241
|
+
| ❸ | Asserting implementation details | `grep '_internal_helper'` against output; assertions on `.roll/internal/*` paths |
|
|
242
|
+
| ❹ | Fixture order coupling | `setup_file` writes shared mutable state without per-test reset |
|
|
243
|
+
| ❺ | Testing private functions | Test sources a `lib/` file and calls a `_underscore_prefixed` helper directly |
|
|
244
|
+
| ❻ | Asserting framework behavior | References to `$BATS_TEST_NUMBER`, `$BATS_SUITE_NAME` in assertions |
|
|
245
|
+
|
|
246
|
+
**Rate cap — 每轮 ≤ 5 条 test-quality REFACTOR entries**. Same dream cycle may
|
|
247
|
+
emit more than 5 findings; the dream scan must rank by severity (❶ > ❷ > ❸ > ❹ > ❺ > ❻
|
|
248
|
+
and within a class, by occurrence count) and only persist the top 5 to BACKLOG.
|
|
249
|
+
Remaining findings go into the dream log under `## 测试质量` but are not made
|
|
250
|
+
into REFACTOR rows — this prevents the backlog from being drowned in test-debt
|
|
251
|
+
on the first scan after rubric publication.
|
|
252
|
+
|
|
253
|
+
**REFACTOR entry format** — same as other scans, but tagged with category:
|
|
254
|
+
|
|
255
|
+
```markdown
|
|
256
|
+
| REFACTOR-XXX | docs: <one-line description> [test-quality:❶] — flagged by dream YYYY-MM-DD | 📋 Todo |
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
The `[test-quality:❶]` (through `❻`) tag is **required** so downstream filtering
|
|
260
|
+
(e.g. "show me all ❶ items still open") is mechanical. The marker character must
|
|
261
|
+
match the rubric exactly.
|
|
262
|
+
|
|
263
|
+
**Optional helper** — `bin/dream-test-quality-scan` is a thin shell script
|
|
264
|
+
maintainers can invoke ad-hoc to dry-run the ❶ detector against a single file
|
|
265
|
+
or directory (see `bin/dream-test-quality-scan --help`). The dream skill itself
|
|
266
|
+
does **not** depend on the helper — Scan 7 is the AI agent applying the rubric.
|
|
267
|
+
The helper just exists so a maintainer (or this skill's smoke test) can confirm
|
|
268
|
+
the ❶ heuristic still finds known instances.
|
|
269
|
+
|
|
270
|
+
#### Dream Log Section (Scan 7)
|
|
271
|
+
|
|
272
|
+
Add after `## 文档新鲜度` section:
|
|
273
|
+
|
|
274
|
+
```markdown
|
|
275
|
+
## 测试质量
|
|
276
|
+
- 本轮发现 {N} 项(写入 BACKLOG 的前 5 项见下;剩余 {M} 项仅记录于本日志)
|
|
277
|
+
- ❶ 硬编码业务数据:{count}
|
|
278
|
+
- ❷ 过度 mock:{count}
|
|
279
|
+
- ❸ 断言实现细节:{count}
|
|
280
|
+
- ❹ Fixture 顺序耦合:{count}
|
|
281
|
+
- ❺ 测私有函数:{count}
|
|
282
|
+
- ❻ 断言框架行为:{count}
|
|
283
|
+
{命中文件列表 或 "未发现可治理的测试反模式。"}
|
|
284
|
+
```
|
|
285
|
+
|
|
227
286
|
## Output
|
|
228
287
|
|
|
229
288
|
### REFACTOR Entry (.roll/backlog.md)
|
|
@@ -118,9 +118,10 @@ Document structure (two-layer separation):
|
|
|
118
118
|
**Important rules:**
|
|
119
119
|
1. Plan files go in `.roll/features/<feature>-plan.md` (**no longer using** `docs/plans/`)
|
|
120
120
|
2. US details go in the corresponding `.roll/features/<feature>.md`
|
|
121
|
-
3. .roll/
|
|
122
|
-
4.
|
|
123
|
-
5.
|
|
121
|
+
3. **FIX / IDEA detail files use ID-prefixed filenames**: `.roll/features/<epic>/FIX-097.md`, not `.roll/features/<epic>/some-descriptive-slug.md`. Reason: a single FIX is one card, not a long-lived feature; the ID is the most stable handle, descriptive slugs date quickly and break links. US can keep feature-slug naming (US lives inside a multi-Story feature file). Quick lookup: `ls .roll/features/<epic>/FIX-*.md` finds all bugs in that area without grepping content.
|
|
122
|
+
4. .roll/backlog.md only contains index rows (one row per US), **do not write** AC / Files / Notes
|
|
123
|
+
5. Domain model files go in `.roll/domain/` — create on first greenfield design, update incrementally
|
|
124
|
+
6. **Do not** write to `~/.kimi/` or any global config directory
|
|
124
125
|
|
|
125
126
|
**File path resolution order:**
|
|
126
127
|
1. Determine Feature ownership (based on the requirement domain: compiler / ingest / qa / ...)
|
|
@@ -29,7 +29,7 @@ $roll-notes 今天的 code review 给了很好的反馈
|
|
|
29
29
|
|
|
30
30
|
## Behavior
|
|
31
31
|
|
|
32
|
-
1. **Determine file path**:
|
|
32
|
+
1. **Determine file path**: `.roll/notes/YYYY-MM-DD.md` relative to project root (parallel to `.roll/dream/` and `.roll/briefs/` — notes is project metadata, not source)
|
|
33
33
|
2. **Get current time**: Use `Asia/Shanghai` timezone (`TZ=Asia/Shanghai date`)
|
|
34
34
|
3. **Read existing entries for style**: Before writing, read the last 2–3 entries
|
|
35
35
|
in the same file. Analyze their style: heading format, voice/tone,
|
|
@@ -95,6 +95,9 @@ $roll-notes 今天的 code review 给了很好的反馈
|
|
|
95
95
|
## File location
|
|
96
96
|
|
|
97
97
|
```
|
|
98
|
-
|
|
99
|
-
└──
|
|
98
|
+
.roll/
|
|
99
|
+
└── notes/
|
|
100
|
+
└── YYYY-MM-DD.md
|
|
100
101
|
```
|
|
102
|
+
|
|
103
|
+
注:notes 是项目元数据(与 `.roll/dream/` / `.roll/briefs/` 同级),不入 git;由 dream/brief 等下游 skill 跨日聚合。
|