excavate 是做什麼的?
在第一次功能開發盲目進行之前,恢復舊版儲存庫自身已知的資訊。輸出是一個脫離目錄樹的檔案(包含熱點、耦合關係、還原的決策、詢問對象地圖,以及一個說明歷史能告訴你和不能告訴你什麼的覆蓋率區塊),外加一個可選的docs/solutions/條目的PR,採用/compound的格式,以便/research、/write-a-prd和/execute能透過它們已經運行的關鍵字grep來讀取這些條目。
有兩件事保持這項工作的誠實性。Tornhill的方法將版本控制作為行為記錄來挖掘:熱點是變更頻率和複雜度的交點,而不是單獨的任何一個軸,並限定在一個固定的時間窗口內,它是一個需要檢查的機率,而不是一個需要採取行動的判決。而「基礎規則」(references/visual-rendering-core.md § 1)則分割每個人工製品:SHA、PR編號、路徑、計數和引用的理由都是直接從git和gh輸出中複製,從不重新輸入;模型只撰寫圍繞它們的說明文字。一個其「為什麼」是模型猜測的決策不是被還原的,而是被發明的,它不會被包含在儲存庫中。
調用位置
側路由,僅由使用者調用。 它在人類輸入/excavate時觸發;disable-model-invocation: true 將其描述排除在所有其他會話的上下文之外,因為它在每個儲存庫導入時僅輸入一次,且從未有理由自行觸發。使用規劃模式,全程保持人在迴路中(HITL);沒有無人值守路徑,因為每個與提交綁定的項目都需要人類閱讀其所依據的引用。
將其作為單獨的訊息發送,而不是作為新工作區的第一條訊息。一個隱藏技能如果被輸入到主機預先附加了序言的第一條訊息中,將會被當作散文閱讀且永遠不會載入;/lfg的調用位置記錄了這個現場事件(chrislacey89/skills#383)。請將其重新輸入為第二條訊息。
/help 的步驟2說明了開始使用它的信號。請勿將其用於管道已經複合的儲存庫(在那裡執行/compound),勿作為對任何人的績效評估(參見§5),也勿作為缺陷查找工具(/qa和/triage-issue負責此項)。
它與主管道並列,並在/shape處重新連接:第一個功能在擁有檔案和條目後進行塑形。它也可以將工作交接給/request-refactor-plan(當首要熱點就是工作本身時),或交接給/qa(當歷史顯示存在一個帶有修復標籤但從未關閉的叢集時)。
方法規則
以下是下方流程所操作化的規則。每一條都指向其來源;此文件中不再重複說明。
- 按修訂次數排序,按程式碼行數進行資格審查,絕不單獨使用一個軸。 沒有大小的頻率會標記出配置檔案;沒有頻率的大小則會標記出那些多年前除錯至正確後就再也沒有被碰過的程式碼(Tornhill, 第1-4章)。使用
wc -l的程式碼行數 (LOC) 作為複雜度代理:不假設存在cloc,且 LOC 是該書預設使用的代理指標。 - 限定在一個固定的時間窗口內。 原始的修訂次數若無參考期間是沒有意義的。預設值為12個月;對於高變動率的儲存庫則縮短它。列印實際的窗口。
- 預期冪律分佈並在曲線膝蓋處截斷。 少數檔案承擔了大部分成本。在曲線趨於平緩處停止,絕不在一個整數處停止。
- 一個趨勢需要複雜度和大小。 「惡化」表示縮排複雜度的上升速度快於 LOC;「已重構」表示兩者都下降;「穩定」表示兩者都平穩(Tornhill, 第5-6章)。僅靠 LOC 無法得出這些標籤。
- 在耦合分析前過濾非領域檔案;在報告每個耦合之前對其進行分類。 鎖定檔案、清單檔案、生成的程式碼和供應商程式碼都是誤報。與其主題耦合的測試是預期行為,應予排除;複製貼上、非支持性邊界和生產者-消費者耦合則應保留並標記(Tornhill, 第8-9章)。被壓縮的歷史會破壞提交層級的耦合;改用PR層級的變更集,或說明該部分不可用。
- 熱點是機率。 在命名一個機制之前先打開該檔案,並且不要為你未打開的檔案提供任何條目。
- 擁有權數據僅有四種合法用途:尋找審查者、導入新成員、決定測試放置位置,以及判斷某個架構是否適合團隊。它絕不能用於對人員進行排名或評估,並且應透過
.mailmap來讀取,排除機器人和初始導入(Tornhill, 第12-14章)。 - 現有文檔僅是理想化的陳述。 ADR、README 或 CHANGELOG 中的聲明在記錄為當前有效的決策之前,必須與當前程式碼樹進行驗證(Spinellis, Code Reading)。
- 對熱點的第一步行動是在一個關鍵點進行特徵化測試,而不是重構(Feathers, Working Effectively with Legacy Code)。檔案中的「建議第一步行動」部分會為每個熱點說明這一點。
- 一個熱點因其極端性而被選中,因此其排名中的一部分是雜訊。 回歸均值是虛無假設:在一個時間窗口內排名第一的檔案,在未經任何處理的情況下,下一個窗口可能只是平均水準;而一個因重構而冷卻的檔案,即使不重構也會自然冷卻(Galton; Kahneman)。根據在窗口的前後半段中的持續性進行排名,將每個熱點標記為
persistent或single-window,並僅將持續性熱點路由到第一步行動清單。排名是診斷工具,絕非目標;設定目標的團隊會得到操縱行為,而非品質(Goodhart)。 - 在決策旁邊記錄其假設。 理由是最容易被遺忘的,而未記錄的假設會固化為沒人敢改變的數字(Leveson, Engineering a Safer World, 第10章)。每個被還原的決策都攜帶著其引用所陳述的約束條件(「因為Y而選擇X,假設Z」),以及Z在當前程式碼樹中是否仍然成立,後者即為反轉檢測器。
- 一個決策條目應具有ADR的形式和合規性檢查線。 包含標題、狀態、上下文、決策及其原因、後果,以及如何驗證;缺少最後一項,記錄就只是考古學而非治理(Richards & Ford, Fundamentals of Software Architecture, 第19章)。此處的驗證是針對當前程式碼樹執行的grep或路徑檢查。
- 檔案是由一個處於「知識詛咒」下的新進者閱讀的。 以簡短的詞彙表和膝蓋處的熱點開頭,將完整表格放在它們後面,並使第一個歡迎任務是理解並總結一個熱點的已還原決策,而不是修復它(Hermans, The Programmer's Brain)。
流程
以下每個圍欄程式碼塊都按原樣執行。每個 Bash 工具呼叫都是一個全新的 shell,因此沒有變數可以從一個呼叫存活到下一個。步驟0的變數區塊執行一次;其後的區塊將每個解析後的值寫入 $WORK/vars.sh,而之後的每個 shell(無論是你的還是挖掘工具的)都以 . "$WORK/vars.sh" 開頭,而不是重新執行步驟0,因為重新執行會將 SINCE 重置為預設值,並丟棄 --since 或 NONDOMAIN 的擴展。跳過該檔案的 shell 會在不報錯的情況下失敗:git log --since="" 不返回任何提交,grep -Ev "" 不列印任何內容,而空輸出會被解讀為有效的答案「無還原」。WORK 是一個派生自儲存庫的路徑,因此一個新的 shell 會使用步驟0中的 WORK= 表達式重新派生它,並讀取其下的檔案;挖掘工具在它們的簡報中接收的是 vars.sh 的絕對路徑,而不是繼承任何變數。<path> 是唯一的佔位符,按每個熱點進行替換。
標誌語義是其工具自身的文檔
(git-log、
git-shortlog、
git-rev-parse、
gh pr list);此文件不加以解釋。scripts/test-documented-git-commands.sh 針對一個預先準備好的測試環境運行每個 git 區塊,因此一個不再有效的區塊調用將在 CI 中失敗。
0. 預檢
解析參數:--since <date>(預設為 12 months ago;接受 git log --since 接受的任何形式)和 --paths <pathspec>...(替換下面所有區塊中的 .)。然後設定共享變數:
SINCE='12 months ago'
WORK="${TMPDIR:-/tmp}/excavate-$(git rev-parse --show-toplevel | cksum | cut -d' ' -f1)"
mkdir -p -m 700 "$WORK"
START=$(git log --since="$SINCE" --no-merges --format=%cs HEAD </dev/null | tail -1)
NONDOMAIN=(':(exclude)*.lock' ':(exclude)*package-lock.json' ':(exclude)*pnpm-lock.yaml' ':(exclude)*yarn.lock' ':(exclude)*.min.*' ':(exclude)*.snap' ':(exclude)*.generated.*' ':(exclude)CHANGELOG.md' ':(exclude)*/vendor/*' ':(exclude)*/node_modules/*')
BOTS='[[]bot[]]|dependabot|renovate'
MAXFILES=30
export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.quotePath GIT_CONFIG_VALUE_0=false
GIT_CONFIG_* 匯出為本次運行中的每個 git 呼叫設定 core.quotePath=false(參見 git-config, 環境),因此包含非 ASCII 位元組的路徑會以其自身形式列印,而不是以 wc 和後續的聯接操作無法打開或匹配的轉義形式。
START 是窗口內第一個提交的日期,從 git 派生而非 date,因為 date -d 是 GNU 專有,而 date -v 是 BSD 專有。NONDOMAIN 是一個起始清單:在步驟1之前,使用此儲存庫中生成的、供應商的、快照的和鏡像的路徑來擴展它(對任何可疑項目檢查 git check-attr linguist-generated linguist-vendored)。MAXFILES 是變更集大小的上限,高於此值的提交將被視為批量掃蕩(如格式化、導入、大量重新命名),並從頻率和耦合計數中排除。步驟2中的共享日誌在構建日誌時應用此值一次;後續區塊不會重複。
WORK 基於儲存庫的頂層路徑,而不是 HEAD,因為步驟8會將 HEAD 移動到文檔分支;它在不同 shell 之間保持有效,並在步驟9中被移除。如果中斷運行的目錄已存在,請在步驟2的第一個區塊之前刪除它,以免過時檔案被誤認為本次運行的輸出。
一旦 NONDOMAIN 被擴展(以及在步驟1閘門之後,如果使用者更改了窗口:讀取檔案,設定 SINCE,重新運行步驟0的 START= 行,然後再次運行此區塊)後,持久化解析後的值:
{
declare -p SINCE START NONDOMAIN BOTS MAXFILES WORK
declare -px GIT_CONFIG_COUNT GIT_CONFIG_KEY_0 GIT_CONFIG_VALUE_0
} > "$WORK/vars.sh"
然後在一個以 . "$WORK/vars.sh" 開頭的 shell 中測量覆蓋率:
git rev-parse --is-shallow-repository
git rev-list --count --since="$SINCE" --no-merges HEAD
git log --max-parents=0 --format=%cs HEAD </dev/null | sort | head -1
git log --since="$SINCE" --no-merges --format=%s HEAD </dev/null | awk '{ t++ } /\(#[0-9]+\)$/ { s++ } END { printf "%d of %d subjects end in (#N)\n", s, t }'
這四行分別是:是否為淺克隆、窗口內的提交數、根提交日期(如果是淺克隆則為邊界,所以顯示「unknown」)、以及壓縮比率。高的壓縮比率意味著每個提交就是一個完整的 PR,因此提交層級的耦合已經等同於 PR 層級的耦合;低比率且在其他地方有 squash 合併的 PR 是需要關注的情況。
然後統計那些落地了一個已在窗口內的變更的提交數量,因為透過發布分支發布的儲存庫會以原始、揀選和發布/同步彙總的形式攜帶相同的變更,而下面的每個計數都會將每個副本視為一個獨立的修訂:
git log --since="$SINCE" --no-merges --reverse -p --format='commit %H' HEAD -- . "${NONDOMAIN[@]}" </dev/null \
| git patch-id --stable | awk 'seen[$(1)]++ { print $(2) }' > "$WORK/dups.txt"
wc -l < "$WORK/dups.txt" | tr -d ' '
dups.txt 列出了每個提交,其修補程式 ID (git-patch-id) 已經被窗口內更早的提交擁有,因此清單中最早的那個副本被排除。它讀取與步驟2中共享日誌相同的窗口和 pathspec,因此它統計了排名所讀取的提交中的副本;一個合併的分支,其變更已經在主線上,會被 git log 的歷史簡化跳過。這個計數會放入覆蓋率區塊,步驟3會在排除這些提交後重新排名膝蓋處的熱點。這些提交被計數,而非排除:重新提交的變更仍然是某人所做的工作,一個彙總提交可能是它所包含的變更的唯一記錄,並且後續每個區塊都透過自己的 git log 或 git rev-list 讀取歷史,因此僅應用於共享日誌的過濾器會導致步驟之間不一致。在範圍閘門處,如果你在提交主題中看到任何發布或同步彙總,請指出它們的名稱;它們也不會被排除。
探測 gh auth status 以及遠端是否為 GitHub;
記錄 gh: yes 或 gh: no。使用 bash "$SC" path research 解析檔案目錄,其中 SC 是此技能捆綁的解析器 references/sidecar.sh 的已安裝絕對路徑;切勿自己計算 sidecar 路徑。如果收到 SIDECAR MISS,解析器將退出並返回 1,並列印一個 init 命令:向使用者顯示該命令,並在使用者執行後繼續,因為創建 sidecar 是一個人類行為。在該目錄中查找先前的 excavation-*.md 以及現有的 docs/solutions/;重新運行會更新條目而不是重複它們,並根據新的窗口對先前檔案的 persistent 標籤進行評分(有多少仍然保持熱點),以便覆蓋率區塊攜帶其自身預測的命中率。
如果儲存庫是淺克隆,說明缺失了多少歷史,並提供 git fetch --unshallow;切勿運行它,因為它會改寫 .git。
1. 範圍閘門 (HITL)
精確地列印覆蓋率區塊,就像它將作為檔案的標題一樣:
Window: <START> → today (<SINCE>)
Commits: <N> non-merge (<M> excluded as bulk > MAXFILES files)
Shallow: yes | no
Squash ratio: <s>/<t>
Duplicates: <d> commits repeat an earlier commit's patch ID (counted, not excluded)
gh: yes (<fetched> of <total> merged PRs; <closed> closed unmerged) | no
Fix labels: <k> of <N> commits match the fix pattern (a lower bound; filled in at Step 3)
Excluded: <NONDOMAIN list, one line>
Not visible: closed-unmerged PRs, force-pushed or squashed history, deleted files, departed contributors
Not visible 行不是模板。下面的所有排名均基於可見的歷史;此區塊明確記錄了限制。在檔案末尾,再次列印此區塊,並補充一個 Stability: 行,顯示先前的 persistent 熱點中有多少在當前窗口保持熱點。