Theme / v4.9.0

Ponytail

讓 AI 編碼代理像最懶的資深工程師

實戰範例

實戰範例 010:用 /ponytail-audit 掃描一個微型 Python CLI,整理整份過度設計負債表

示範如何對一個剛接手的微型 CLI 專案執行 /ponytail-audit,讀懂分類報告,並依「刪除、簡化、換成標準庫」三種動作排優先順序。

實戰範例 010:用 /ponytail-audit 掃描一個微型 Python CLI,整理整份過度設計負債表

/ponytail-review 是針對「變更或單一檔案」的審查;/ponytail-audit 則是針對「整個 codebase」的過度設計總盤點,一次掃完、產出排序後的清單,告訴你哪些檔案可以刪、可以簡化、或可以用標準庫取代。這個範例用一個剛接手、約 6 個檔案的 Python CLI 專案當目標,跑完整流程,對應 7 階階梯的第 3、5、6 階(標準庫優先、既有依賴項、單行優化)。


場景:剛接手的微型 CLI 專案

專案結構如下:

mini-todo/
├── pyproject.toml
├── src/mini_todo/
│   ├── __init__.py
│   ├── cli.py
│   ├── commands.py
│   ├── config.py
│   ├── storage.py
│   └── validators.py
└── tests/

pyproject.toml 裡裝了 clickpydanticpydantic-settingsrichpython-dotenvtomlistructlog。第一眼覺得有點超載,但沒人知道哪些真的用得到。


第 1 步:確認等級並執行稽核

/ponytail full
/ponytail-audit

/ponytail-audit 不接檔案路徑,它掃整個專案。回報格式是「分類排序清單」,每一項附帶位置、可刪內容、可用什麼取代。下面是這次典型的報告摘要:

== Ponytail Audit: mini-todo ==
Categories (ranked by impact):

[DELETE - reinvented stdlib]
1. storage.py:38  read_json(): 手寫 JSON 讀取,用 json.load() 即可
2. validators.py:12  is_email(): 30 行 regex,改用標準庫 email.utils

[DELETE - unneeded dependency]
3. tomli (pyproject.toml): Python 3.11+ 已內建 tomllib → 移除 tomli
4. python-dotenv: 此專案從未讀 .env → 移除

[SIMPLIFY - speculative abstraction]
5. config.py:7  TodoConfig(BaseSettings): pydantic 模型只用於 1 個欄位 → 改 dataclass
6. commands.py:45  CommandProtocol(Protocol): 只有一個實作 → 刪除

[SIMPLIFY - dead flexibility]
7. storage.py:80  save_json(): 預留 4 種 serialization backend → 留 1 種
8. cli.py:15  setup_logging(): 3 個 handler 分支,實際只用 stream → 留 1 行

Total findings: 8
Est. LOC reduction: ~120 (約 35%)
Safety preserved: 0 items touched (all findings are non-critical paths)

報告頭部的 [DELETE ...][SIMPLIFY ...] 是 Ponytail 對每一項的處置建議,從最激進(整段刪除)排到最保守(保留但簡化)。


第 2 步:分類解讀,決定優先順序

2.1 先做 DELETE 類(風險最低、效益最高)

storage.py:38 read_json() 是經典的「重新發明標準庫」:

# 改之前:手寫檔案讀取、手動剝離 BOM、處理空字串
def read_json(path: str) -> dict:
    with open(path, 'r', encoding='utf-8') as f:
        content = f.read()
    if not content.strip():
        return {}
    return json.loads(content)

# 改之後:json.load 一行搞定,utf-8-sig 自動處理 BOM
import json
def read_json(path: str) -> dict:
    with open(path, encoding='utf-8-sig') as f:
        return json.load(f) or {}

validators.py:12 is_email() 那 30 行 regex 同理,標準庫 email.utils.parseaddr 已涵蓋 RFC 5322 大多情境。

2.2 再做「移除多餘依賴」

# pyproject.toml(修改前)
dependencies = ["click", "pydantic", "pydantic-settings", "rich", "python-dotenv", "tomli", "structlog"]

# 修改後
dependencies = ["click", "rich"]
# tomli → tomllib(Python 3.11+ 內建)
# python-dotenv → 從未使用,整支移除
# pydantic / pydantic-settings → 改用 dataclasses
# structlog → 改用 logging 標準庫

7 個依賴砍到 2 個。structlogpydantic 在這個微型 CLI 完全用不到那麼重的抽象。

2.3 最後做 SIMPLIFY 類(抽象退場)

config.py 那個 TodoConfig(BaseSettings) 只有 1 個欄位,改用 dataclass

# 改之前:pydantic-settings 為 1 個欄位引入整套
from pydantic_settings import BaseSettings
class TodoConfig(BaseSettings):
    storage_path: str = "~/.mini-todo.json"

# 改之後:標準庫 dataclass
from dataclasses import dataclass
@dataclass
class TodoConfig:
    storage_path: str = "~/.mini-todo.json"

commands.py 那個只有 1 個實作的 Protocol 整支刪除,讓呼叫端直接依賴具體型別。storage.py:80 save_json() 預留的 4 種 serialization backend 砍到 1 種;cli.py:15 setup_logging() 3 個 handler 分支壓成 1 行。


第 3 步:分批套用並跑測試

Ponytail 不會自動改碼,它只給報告。建議分 3 個 commit 套用:

  1. commit A:DELETE 類(第 1、2 項)→ 跑 pytest
  2. commit B:移除多餘依賴(第 3、4 項)→ 更新 pyproject.toml → 跑 pytest
  3. commit C:SIMPLIFY 類(第 5 至 8 項)→ 跑 pytest

每批都跑測試,任何一批失敗就回滾該批。如果某項的簡化被測試擋下,Ponytail 的規則很硬:「從不刪除或削弱測試來讓它通過」。這時請改根因,不要硬改測試。


第 4 步:沒清完的留下追蹤

如果你沒辦法一次清完,可以搭配 /ponytail-debt

/ponytail-debt

它會把這次稽核中你刻意延後的項目,記成一份可追蹤的負債表,避免「先擱著」變成「永遠不處理」。每一筆都會附上位置與升級路徑。/ponytail-debt 不會自動改碼,只是把欠債寫下來。


報告分類對照 Ponytail 7 階階梯

報告分類 對應階梯 範例
DELETE - reinvented stdlib 第 3 階 標準庫優先 手寫 JSON / email regex
DELETE - unneeded dependency 第 5 階 既有依賴項 tomli / python-dotenv
SIMPLIFY - speculative abstraction 第 1 階 YAGNI 單一實作的 Protocol
SIMPLIFY - dead flexibility 第 6 階 單行優化 4 種 serialization backend

關鍵學習點

  • /ponytail-audit 是整個 codebase 的盤點,不接檔案路徑,輸出是排序後的「刪除、簡化、換標準庫」清單,每一項都附位置與替代方案。
  • 套用順序建議 DELETE、移除依賴、SIMPLIFY,每批一個 commit 並跑測試,任何一批失敗就回滾該批,絕不刪測試來硬過。
  • 報告分類直接對應 7 階階梯reinvented stdlib 對應第 3 階,unneeded dependency 對應第 5 階,speculative abstraction 對應第 1 階 YAGNI。
  • 沒清完的項目用 /ponytail-debt 記成負債表,每筆附位置與升級路徑,避免「先擱著」變成永遠不處理。