fix(工作日誌暫存): 收編舊名目錄,並掃出定址不到的孤兒

雜湊規則從 8 碼改成完整 40 碼那一刻,還沒寫進 wiki 的暫存留在舊名底下。
現行程式拿 40 碼去查,查不到那些目錄,裡面的條目就再也寫不出去。

沒人發現是因為兩種狀態長得一樣:「真的沒有待寫」與「待寫卡在舊名底下」
對呼叫端都是成功。技能於是照常回報完成,日誌卻一筆都沒有。

兩道處置。推得出對映的(舊名等於 H 加新名前 7 碼)由 list、cat、merge
一併收編,clear 與 commit 也連舊目錄一起清——commit 不放行的話,已經寫進
wiki 的暫存清不掉,下一輪會整批重複寫一次。推不出對映的交給新的 orphans
子命令掃出來回報,要有人看到才處理得掉。
This commit is contained in:
2026-09-02 15:27:03 +08:00
parent 7ff79005c2
commit e762cf986e
3 changed files with 98 additions and 31 deletions
+89 -27
View File
@@ -29,9 +29,22 @@
# 合併檔: $JSC_HOME/worklog-pending/.merge/{hash}-{UTC 時間}-{pid}.md
# 併入清單: 同名換副檔名 .claim,一行一個被併走的暫存檔路徑
#
# 結束碼: 0=成功 1=讀寫失敗 2=用法錯誤 3=該 hash 沒有暫存內容
# 結束碼: 0=成功 1=讀寫失敗 2=用法錯誤 3=該 hash 沒有暫存內容 4=orphans 掃到孤兒目錄
# merge 沒有暫存內容時仍然 exit 0:合併檔至少有本次條目,呼叫端不必分兩條路走。
#
# 舊目錄自動收編:
# 拿 40 碼來查時,一併收「H 加前 7 碼」那個舊目錄。雜湊規則從 8 碼改成 40 碼那一刻,
# 還沒寫進 wiki 的暫存會留在舊名底下;不收的話它查不到,而且沒有人會說查不到——
# 「沒有暫存」與「暫存卡在舊名底下」對呼叫端長得一模一樣,兩種都是成功。日誌就這樣消失。
# 只認「舊名等於 H 加新名前 7 碼」這一種對映,推導唯一,不會收到別的專案。
# list、cat、merge 讀兩個目錄,clear 清兩個,commit 也放行舊目錄的路徑——
# 放行漏掉的話,已經寫進 wiki 的暫存清不掉,下一輪會整批重複寫一次。
#
# orphans 子命令(不吃 hash):
# 掃暫存區根目錄,印出每一個不是 40 碼的目錄:{目錄名}<TAB>{檔數}<TAB>{路徑}。
# 沒有就 exit 0,有就 exit 4。上面那條自動收編只救得到推得出對映的舊目錄,
# 推不出來的要有人看到才處理得掉,所以另外給一支掃描。
#
# 陷阱:
# - clear 與 commit 都只在日誌確實寫進 wiki 之後才呼叫。先清再寫,寫失敗就兩邊都沒有。
# - commit 只認 merge 產出的併入清單,且清單上的路徑必須落在該 hash 的暫存目錄底下,
@@ -99,6 +112,11 @@ valid_hash() {
exit 2
}
claim_reject() { # 併入清單上出現不該清的路徑,一律當用法錯誤擋下
echo "[jsc][工作日誌暫存][ERR]:併入清單裡的「$1」不在這個 hash 的暫存目錄底下,不清。" >&2
exit 2
}
valid_claim() { # 併入清單只認 merge 產出的那一份,擋掉拿別處的清單來刪檔
case "$1" in
"$MERGE_DIR"/*.claim) ;;
@@ -109,10 +127,55 @@ valid_claim() { # 併入清單只認 merge 產出的那一份,擋掉拿別處
cmd="${1:-}"; [ -n "$cmd" ] || usage
shift || true
# orphans 不吃 hash:它要掃的就是「算不出對應 hash」的那些目錄。
if [ "$cmd" = orphans ]; then
[ -d "$ROOT" ] || exit 0
found=0
for d in "$ROOT"/*/; do
[ -d "$d" ] || continue
name=$(basename "$d")
[ "$name" = .merge ] && continue
case "$name" in
????????????????????????????????????????) continue ;;
esac
n=0
for f in "$d"*.md; do [ -f "$f" ] && n=$((n + 1)); done
printf '%s\t%s\t%s\n' "$name" "$n" "$d"
found=1
done
[ "$found" -eq 0 ] && exit 0
echo "[jsc][工作日誌暫存][ERR]:上列暫存目錄不是現行的 40 碼頁名,現行程式查不到它們,裡面的日誌永遠寫不進 wiki。舊名是新名的「H 加前 7 碼」,把目錄改成完整 40 碼即可。" >&2
exit 4
fi
hash="${1:-}"; [ -n "$hash" ] || usage
valid_hash "$hash"
dir="$ROOT/$hash"
# 舊規則的同一個專案會落在「H 加前 7 碼」那個目錄。拿 40 碼來查時一併收,否則規則改版
# 那一刻還沒寫進 wiki 的暫存就成為孤兒——查不到、也沒有人會說查不到,日誌就這樣消失。
# 只認這一種對映:舊名完全等於 H 加新名前 7 碼,推導唯一,不會收錯別的專案。
legacy_dir=""
case "$hash" in
????????????????????????????????????????)
_cand="$ROOT/H$(printf '%s' "$hash" | cut -c1-7)"
[ -d "$_cand" ] && legacy_dir="$_cand" ;;
esac
# 兩個目錄的暫存檔一起排。檔名開頭是 UTC 時間戳,字典序等同時間序,所以跨目錄排完
# 仍然是「舊的在前」。分開接的話,舊目錄那幾筆會整批插到新目錄最舊那一筆前面。
pending_files() {
for _d in "$dir" "$legacy_dir"; do
[ -n "$_d" ] && [ -d "$_d" ] || continue
for _f in "$_d"/*.md; do
[ -f "$_f" ] || continue
# 先印檔名再印全路徑,排完再把檔名那一欄切掉:排序的鍵是檔名,不是目錄。
printf '%s\t%s\n' "$(basename "$_f")" "$_f"
done
done | sort | cut -f2-
}
case "$cmd" in
add)
src="${2:-}"
@@ -125,30 +188,26 @@ case "$cmd" in
printf '%s\n' "$target"
;;
list)
[ -d "$dir" ] || exit 3
found=0
for f in "$dir"/*.md; do
[ -f "$f" ] || continue
printf '%s\n' "$f"
found=1
done
pending_files | while IFS= read -r f; do printf '%s\n' "$f"; done
pending_files | grep -q . && found=1
[ "$found" -eq 1 ] || exit 3
;;
cat)
[ -d "$dir" ] || exit 3
found=0
for f in "$dir"/*.md; do
[ -f "$f" ] || continue
cat "$f"
printf '\n'
found=1
done
pending_files | while IFS= read -r f; do cat "$f"; printf '\n'; done
pending_files | grep -q . && found=1
[ "$found" -eq 1 ] || exit 3
;;
clear)
[ -d "$dir" ] || exit 3
rm -rf "$dir" || { echo "[jsc][工作日誌暫存][ERR]:清不掉「$dir」。" >&2; exit 1; }
printf '已清除暫存:%s\n' "$dir"
cleared=0
for d in "$dir" "$legacy_dir"; do
[ -n "$d" ] && [ -d "$d" ] || continue
rm -rf "$d" || { echo "[jsc][工作日誌暫存][ERR]:清不掉「$d」。" >&2; exit 1; }
printf '已清除暫存:%s\n' "$d"
cleared=1
done
[ "$cleared" -eq 1 ] || exit 3
;;
merge)
# 合成這一次要寫進 wiki 的內容:暫存的在前(依時間),本次條目在後。
@@ -163,15 +222,13 @@ case "$cmd" in
: > "$merged" || { echo "[jsc][工作日誌暫存][ERR]:寫不進「$merged」。" >&2; exit 1; }
: > "$claim" || { echo "[jsc][工作日誌暫存][ERR]:寫不進「$claim」。" >&2; exit 1; }
taken=0
if [ -d "$dir" ]; then
for f in "$dir"/*.md; do
[ -f "$f" ] || continue
cat "$f" >> "$merged" || { echo "[jsc][工作日誌暫存][ERR]:讀不到「$f」。" >&2; exit 1; }
printf '\n' >> "$merged"
printf '%s\n' "$f" >> "$claim"
taken=$((taken + 1))
done
fi
pending_files > "$claim" || { echo "[jsc][工作日誌暫存][ERR]:列不出暫存檔。" >&2; exit 1; }
while IFS= read -r f; do
[ -n "$f" ] || continue
cat "$f" >> "$merged" || { echo "[jsc][工作日誌暫存][ERR]:讀不到「$f」。" >&2; exit 1; }
printf '\n' >> "$merged"
taken=$((taken + 1))
done < "$claim"
cat "$src" >> "$merged" || { echo "[jsc][工作日誌暫存][ERR]:併不進本次條目「$src」。" >&2; exit 1; }
printf '\n' >> "$merged"
printf 'MERGED=%s\n' "$merged"
@@ -187,15 +244,20 @@ case "$cmd" in
cleared=0
while IFS= read -r f; do
[ -n "$f" ] || continue
# 舊目錄也要放行:merge 會把「H 加前 7 碼」那個舊目錄的暫存一起併走,
# 這裡不認的話,寫進 wiki 的內容清不掉,下一輪會整批重複寫一次。
case "$f" in
"$dir"/*) ;;
*) echo "[jsc][工作日誌暫存][ERR]:併入清單裡的「$f」不在「$dir」底下,不清。" >&2; exit 2 ;;
*) if [ -n "$legacy_dir" ]; then
case "$f" in "$legacy_dir"/*) ;; *) claim_reject "$f" ;; esac
else claim_reject "$f"; fi ;;
esac
[ -e "$f" ] || continue
rm -f "$f" || { echo "[jsc][工作日誌暫存][ERR]:清不掉「$f」。" >&2; exit 1; }
cleared=$((cleared + 1))
done < "$claim"
rmdir "$dir" 2>/dev/null || true
[ -n "$legacy_dir" ] && rmdir "$legacy_dir" 2>/dev/null || true
rm -f "$claim" "${claim%.claim}.md"
printf '已清除暫存 %s 個檔案(wiki 寫入成功後才做)。\n' "$cleared"
;;