da9bd53cae
兩張票放同一條分支:都是「該發生卻不會自己發生的事」,觸發點都在 session 邊界。 ## inkstone/ISEP#93 —— 逾期和掛著沒人接的事會主動叫 - scripts/isep-nag 撈三種沒人會叫的事(逾期 milestone/等 leo 的票/掉在地上的棒子) - scripts/isep-notify 發 Telegram,而且**發不出去的時候不會安靜** - hooks/overdue-nag-guard.sh SessionStart 跑一次(不輪詢、不 fan-out、不掛 Actions) - hooks/tests/overdue-nag.test.sh 35 條,全程離線 實跑撈得出票上點名的那五個逾期 milestone(08-24 三個、08-26 兩個)。 沒東西可報時會說「查過了,沒有」——安靜跟壞掉長得一模一樣。 🔴 工單補的那個限制(今天實測出來的)已經處理: 「能不能發得出去」取決於這個 session 載到的 ISEP 是哪一版 (prod-write-guard v0.10.0 才認得出 notify_leo 不是部署,而 hook 註冊路徑 在 session 啟動當下就寫死了)。所以 isep-notify 會**先拿真的要送的那一則 去問這個 session 註冊的那支閘**,把判定寫成檔(誰都查得到),然後: · 放行 ⇒ 送,並驗內層 data.data.ok(外層 200 不算送到) · 會擋 ⇒ **不繞路**,改貼回票上並把原文印在眼前 用 git 歷史裡的真跡(c263866 那一版閘)測過「會擋」那條路。 ⛔ 實測發現通道本身現在是斷的:實例上找不到 notify_leo 工作流(404)。 不是閘、不是網路、不是金鑰。詳情與修法寫在 docs/TESTING.md 最後一段。 退路兩次都走通了(inkstone/ISEP#93 comment 5182、5184)。 ## inkstone/ISEP#89 —— wiki 太長時有人整理 - scripts/wiki-compress audit/plan/apply/verify/bench 五個動詞 - hooks/wiki-size-guard.sh SessionStart 點名太長的檔;寫檔時擋「沒走流程的壓縮」 - hooks/tests/wiki-compress.test.sh 25 條,全程離線、不碰真的 wiki 設計上最重要的一條:**只搬不改**,正文一個字都不動。 「合併同類、濃縮成一行」要重寫正文,而重寫的當下沒有人會發現弄丟了什麼。 所以機器只做「搬 + 目錄 + 標 ×N/↻」,合併留給人。 拿現在的 mistakes.md 複本實壓過(不動真的 wiki): 7,681 行 → 1,199 行 260 條一條都沒少(verify 用內文雜湊逐條對帳) 80 個查詢命中率 80/80,定位成本 2,956 → 131 行,**快 22.5 倍**(bench) verify 反向測過:真的弄丟一條時它抓得到(抓不到的對帳表比沒有更糟)。 ## 盤點數字 在這棵樹上當場數的,不是拿上一版加減推的: ls hooks/*.sh | wc -l → 55(was 53) grep -c '"command":' hooks.json → 71(was 68) plugin.json/README/hooks-inventory 三處同步改。 **版本號沒動**(0.11.0),待總管定版。
184 lines
8.4 KiB
Bash
Executable File
184 lines
8.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# wiki-size-guard.sh — 讀不完的必讀檔要被講出來;壓它的時候不准沒人知道
|
||
# (inkstone/ISEP#89)
|
||
#
|
||
# ── 為什麼要有這一支 ──────────────────────────────────────────────────
|
||
# leo 2026-08-27 說那天「**失去記憶**」。而記憶失效的其中一個原因是:
|
||
# **東西還在,只是沒人讀得完。** `mistakes.md` 實測 7,681 行、260 條,
|
||
# 而它是「做新功能前讀一遍」等級的必讀檔。**讀不完的必讀檔,等於沒有。**
|
||
#
|
||
# 既有三支管的是別的事,一支都接不到這裡:
|
||
# · `wiki-secret-scan.sh` 寫進去的內容安不安全
|
||
# · `wiki-first-search.sh` 有沒有先查 wiki
|
||
# · `wiki-first-police.sh` 收工前有沒有查過
|
||
# **沒有任何一支管「太大了」。**
|
||
#
|
||
# ── 兩個事件,是同一件事的兩半 ────────────────────────────────────────
|
||
# ① SessionStart ── 講:哪幾個檔已經沒有人讀得完了(📝 記錄,不擋)
|
||
# ② Write|Edit|MultiEdit ── 擋:**大幅刪減 wiki 檔**卻沒走正式流程 ⇒ 擋一次
|
||
#
|
||
# 只有 ① = 又一個會被忽略的提醒。
|
||
# 只有 ② = 罰它做一件沒人告訴它該做的事。
|
||
#
|
||
# ── 🔴 ② 擋的判準是「動作」不是「措辭」(leo 2026-08-17)────────────────
|
||
# 「自然語言的變體是無限的,blacklist 永遠追不完……封路哲學之所以有效,
|
||
# 是因為它封的是**動作**——動作有限且可枚舉。」
|
||
# 本支比的是**字元數**:wiki 檔的內容淨縮水超過門檻 ⇒ 那就是一次壓縮,
|
||
# 不管它在 commit 訊息裡怎麼講。加字、改字、小修一律不碰。
|
||
#
|
||
# 為什麼壓縮要擋:**壓縮會弄丟東西,而弄丟的當下沒有人會發現**(票上原話)。
|
||
# 走 `scripts/wiki-compress` 有兩件手寫做不到的事——
|
||
# · `--ticket` 逼票號,並把「這一版壓掉了什麼」寫進 `.compress-log.md`(留痕)
|
||
# · `verify` 用內文雜湊**逐條對帳**,「沒弄丟」是算出來的,不是宣稱的
|
||
#
|
||
# 出口(留痕式,同 `wiki-secret-ok` 的慣例):真的要手改,
|
||
# 在寫入的內容裡放一個 `wiki-compress-ok`,並在 commit 說明理由。
|
||
set -uo pipefail
|
||
|
||
ROOT="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
||
TOOL_PY="$ROOT/scripts/wiki-compress"
|
||
INPUT="$(cat)"
|
||
|
||
EVENT=$(printf '%s' "$INPUT" | python3 -c '
|
||
import sys, json
|
||
try: print(json.load(sys.stdin).get("hook_event_name", "") or "")
|
||
except Exception: print("")
|
||
' 2>/dev/null || printf '')
|
||
|
||
# ══ ① SessionStart:講出來哪幾個檔已經沒人讀得完 ═══════════════════════
|
||
if [ "$EVENT" = "SessionStart" ]; then
|
||
[ -f "$TOOL_PY" ] || exit 0
|
||
PROJ="${CLAUDE_PROJECT_DIR:-$PWD}"
|
||
DIRS=""
|
||
for d in "$PROJ/system-dev/wiki" "$PROJ/.claude/wiki" \
|
||
"$PROJ/InkStoneCo/system-dev/wiki"; do
|
||
[ -d "$d" ] && DIRS="$DIRS $d"
|
||
done
|
||
[ -n "$DIRS" ] || exit 0
|
||
# shellcheck disable=SC2086
|
||
# 🔴 不要寫成 `OUT=$(…) || OUT=""`:`audit` 有東西可報時**刻意回離開碼 1**,
|
||
# 加上 pipefail,那個 `||` 會把剛拿到的結果整個清成空的
|
||
# ⇒ **有 7 個檔爆表,而這一段一聲不吭**。(2026-08-28 寫的時候當場踩到。)
|
||
OUT=$(python3 "$TOOL_PY" audit $DIRS 2>/dev/null | grep '🔴 太長' | head -6 || true)
|
||
[ -n "$OUT" ] || exit 0
|
||
python3 - "$OUT" <<'PY'
|
||
import json, sys
|
||
rows = [r for r in sys.argv[1].split("\n") if r.strip()]
|
||
msg = "📚 wiki 有 %d 個檔沒人讀得完了" % len(rows)
|
||
ctx = "\n".join([
|
||
"📚 **這幾個 wiki 檔已經超過『讀得完』的門檻**(inkstone/ISEP#89):",
|
||
"",
|
||
] + rows + [
|
||
"",
|
||
"🔴 讀不完的必讀檔,跟沒有那個檔的差別只在於**它讓人以為有**。",
|
||
"leo 2026-08-27 說那天「失去記憶」——東西都還在,只是沒人讀得完。",
|
||
"",
|
||
"要整理:`python3 scripts/wiki-compress plan <檔>` 看怎麼壓,",
|
||
"然後 `apply <檔> --ticket owner/repo#N`(**只搬不改**,正文一個字都不動),",
|
||
"壓完一定要 `verify` 逐條對帳。**壓縮這件事本身要有票**,不准順手做完沒人知道。",
|
||
])
|
||
print(json.dumps({"systemMessage": msg,
|
||
"hookSpecificOutput": {"hookEventName": "SessionStart",
|
||
"additionalContext": ctx}},
|
||
ensure_ascii=False))
|
||
PY
|
||
exit 0
|
||
fi
|
||
|
||
# ══ ② Write|Edit|MultiEdit:大幅刪減 wiki 檔 ⇒ 擋一次 ═══════════════════
|
||
VERDICT=$(printf '%s' "$INPUT" | python3 -c '
|
||
import json, os, re, sys
|
||
|
||
try:
|
||
d = json.load(sys.stdin)
|
||
except Exception:
|
||
print("SKIP:bad-json"); raise SystemExit
|
||
|
||
ti = d.get("tool_input") or {}
|
||
path = ti.get("file_path") or ""
|
||
# 只管 wiki 底下的 .md。別的檔案縮水是它們自己的事。
|
||
if not path.endswith(".md") or not re.search(r"(^|/)(wiki)/", path):
|
||
print("SKIP:not-wiki"); raise SystemExit
|
||
|
||
def size(s):
|
||
return len(re.sub(r"\s+", "", s or ""))
|
||
|
||
# 留痕式出口:內容裡放了 wiki-compress-ok ⇒ 放行(同 wiki-secret-ok 的慣例)
|
||
blob = json.dumps(ti, ensure_ascii=False)
|
||
if "wiki-compress-ok" in blob:
|
||
print("SKIP:opt-out"); raise SystemExit
|
||
|
||
before = after = 0
|
||
if "content" in ti: # Write:跟磁碟上現有的比
|
||
try:
|
||
with open(path, encoding="utf-8", errors="replace") as f:
|
||
before = size(f.read())
|
||
except Exception:
|
||
print("SKIP:new-file"); raise SystemExit # 新檔不是壓縮
|
||
after = size(ti.get("content"))
|
||
else: # Edit / MultiEdit
|
||
edits = ti.get("edits") or [ti]
|
||
for e in edits:
|
||
if not isinstance(e, dict):
|
||
continue
|
||
before += size(e.get("old_string"))
|
||
after += size(e.get("new_string"))
|
||
|
||
# 判準:**淨縮水的量**要夠大,比例也要夠大。兩個都要,才不會抓到小修。
|
||
# · 至少砍掉 800 個非空白字元 —— 改錯字、刪一段話都到不了
|
||
# · 而且砍掉的超過原本的 45% —— 「重寫一段」通常增減相抵
|
||
if before < 800:
|
||
print("SKIP:too-small"); raise SystemExit
|
||
cut = before - after
|
||
if cut < 800 or cut < before * 0.45:
|
||
print("SKIP:not-a-compression"); raise SystemExit
|
||
print("BLOCK:%s:%d:%d" % (os.path.basename(path), before, cut))
|
||
' 2>/dev/null || echo "SKIP:crash")
|
||
|
||
case "$VERDICT" in
|
||
BLOCK:*) ;;
|
||
*) exit 0 ;;
|
||
esac
|
||
|
||
# 至多擋一次:被擋兩次的閘會被想辦法繞過,而繞過的閘等於不存在。
|
||
SID=$(printf '%s' "$INPUT" | python3 -c '
|
||
import sys, json
|
||
try: print(json.load(sys.stdin).get("session_id", "") or "x")
|
||
except Exception: print("x")
|
||
' 2>/dev/null || printf 'x')
|
||
STAMP="/tmp/.wiki-size-guard-$SID"
|
||
[ -f "$STAMP" ] && exit 0
|
||
touch "$STAMP" 2>/dev/null || true
|
||
|
||
FILE=$(printf '%s' "$VERDICT" | cut -d: -f2)
|
||
BEFORE=$(printf '%s' "$VERDICT" | cut -d: -f3)
|
||
CUT=$(printf '%s' "$VERDICT" | cut -d: -f4)
|
||
|
||
cat >&2 <<MSG
|
||
📚 這是一次 wiki 壓縮,不是一次編輯(inkstone/ISEP#89)
|
||
|
||
檔案:$FILE
|
||
這一筆會砍掉 $CUT 個字(原本 $BEFORE 個)——超過一半。
|
||
|
||
🔴 **壓縮會弄丟東西,而弄丟的當下沒有人會發現。**(票上原話)
|
||
所以壓縮不能順手做,它要留得下痕跡、而且要能逐條對帳。
|
||
|
||
改走這條(三步,機器會幫你證明沒弄丟):
|
||
|
||
python3 scripts/wiki-compress plan <檔>
|
||
python3 scripts/wiki-compress apply <檔> --ticket owner/repo#N
|
||
python3 scripts/wiki-compress verify <檔>.before-compress <檔> <搬去的檔...>
|
||
|
||
它跟手改的差別只有兩件,但這兩件正是手改做不到的:
|
||
· **--ticket 逼票號**,並把「這一版壓掉了什麼」寫進 \`.compress-log.md\`
|
||
· **verify 用內文雜湊逐條對帳**——「沒弄丟」是算出來的,不是你說的
|
||
|
||
它的做法是**只搬不改**:舊的整條原封不動搬去 archive,原檔留一份目錄。
|
||
實測 \`mistakes.md\` 7,681 行 → 1,199 行,**260 條一條都沒少,查詢快 22.5 倍**。
|
||
|
||
真的要手改(例如只是刪掉一段過期的說明):
|
||
在寫入的內容裡放一個 \`wiki-compress-ok\`,並在 commit 說明理由。
|
||
本閘**至多擋一次**,不會卡死。
|
||
MSG
|
||
exit 2
|