逾期判定接上來,逾期與連續失敗每一輪都提 #42

Merged
admin merged 1 commits from feat/overdue-and-failing-every-round into develop 2026-09-07 03:10:40 +00:00
8 changed files with 216 additions and 36 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "jsc-assist", "name": "jsc-assist",
"version": "0.3.3", "version": "0.3.4",
"description": "助理:事件收攏、健康巡檢與待辦簿(MONITOR_{HASH} wiki 頁)", "description": "助理:事件收攏、健康巡檢與待辦簿(MONITOR_{HASH} wiki 頁)",
"skills": "./skills", "skills": "./skills",
"author": { "author": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "jsc-assist", "name": "jsc-assist",
"version": "0.3.3", "version": "0.3.4",
"description": "助理:事件收攏、健康巡檢與待辦簿(MONITOR_{HASH} wiki 頁)", "description": "助理:事件收攏、健康巡檢與待辦簿(MONITOR_{HASH} wiki 頁)",
"skills": "./skills", "skills": "./skills",
"jsc": { "jsc": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "jsc-assist", "name": "jsc-assist",
"version": "0.3.3", "version": "0.3.4",
"description": "助理:事件收攏、健康巡檢與待辦簿(MONITOR_{HASH} wiki 頁)", "description": "助理:事件收攏、健康巡檢與待辦簿(MONITOR_{HASH} wiki 頁)",
"skills": "./skills/", "skills": "./skills/",
"jsc": { "jsc": {
File diff suppressed because one or more lines are too long
+4 -2
View File
@@ -348,7 +348,9 @@ That property holds only while nothing fakes a heartbeat. **`write` is called by
One round: read five sources, record the result, then beat. Everything before the heartbeat is read-only except the round's own scratch files. Ask nobody anything. One round: read five sources, record the result, then beat. Everything before the heartbeat is read-only except the round's own scratch files. Ask nobody anything.
1. **Collect.** Run `{CURRENT}/jsc-assist/tools/patrol.sh collect --trigger 排程` (use `--trigger 手動` when a person asked for this round). That is the same split step 0 branched on: 排程 is the unattended round that read its root out of the invocation text, 手動 the round somebody asked for. Judge the exit code by the patrol.sh table. Exit 4 stands the round down — report the holder and its age from the printed `lock=busy` line, and stop; write no page and no heartbeat. Exit 5 and 6 stop the round the same way, with the code and the stderr text. Exit 0, 1 and 3 all carry on to step 2. Record `round=`, `lock_broken=`, `hash=`, `page=`, `verdict=`, `failed_sources=`, `warn_sources=`, `pending=`, every `item=` line, and the file paths `latest_file=`, `summary_file=`, `summary_row_file=`, `newpage_file=`, `contents_file=` and `due_rows_file=` — that last one is what step 5 has to be given, and an empty value there means the judging step produced no list, so step 5 has nothing to act on and says so rather than falling back to anything. Completion condition: the round id, the page name and the five file paths are recorded, or the stand-down or the failure was reported and the round stopped. 1. **Collect.** Run `{CURRENT}/jsc-assist/tools/patrol.sh collect --trigger 排程` (use `--trigger 手動` when a person asked for this round). That is the same split step 0 branched on: 排程 is the unattended round that read its root out of the invocation text, 手動 the round somebody asked for. Judge the exit code by the patrol.sh table. Exit 4 stands the round down — report the holder and its age from the printed `lock=busy` line, and stop; write no page and no heartbeat. Exit 5 and 6 stop the round the same way, with the code and the stderr text. Exit 0, 1 and 3 all carry on to step 2. Record `round=`, `lock_broken=`, `hash=`, `page=`, `verdict=`, `failed_sources=`, `warn_sources=`, `pending=`, `tasks_total=`, `tasks_failing=`, `tasks_due=`, `tasks_overdue=`, every `item=` line, and the file paths `latest_file=`, `summary_file=`, `summary_row_file=`, `newpage_file=`, `contents_file=` and `due_rows_file=` — that last one is what step 5 has to be given, and an empty value there means the judging step produced no list, so step 5 has nothing to act on and says so rather than falling back to anything. Completion condition: the round id, the page name and the five file paths are recorded, or the stand-down or the failure was reported and the round stopped.
**`tasks_overdue=` and `tasks_failing=` earn a 待人處理 row every round, and `collect` writes both of them itself.** Neither condition heals on its own: a deadline that has passed does not become un-passed, and an entry that fails retries next round and fails again. So the row is repeated every round rather than suppressed after the first — **"already reported" is not "already handled"**, and the assistant does not pause an entry on anybody's behalf; `paused` is a state a person sets and only a person clears. The overdue count comes from the judging step and is a dash when that step could not judge, which is not the same as zero. The monitor page carries the two named tables under 待辦簿到期與逾期: which entries are overdue and by how long, and which ones have been failing and how many times. **The directory page gains no field for either.** That page holds one block per machine, each written by that machine's own round, so a new field would only appear for machines already on the new version, and a reader could not tell "nothing overdue here" from "this machine has not written that field yet" — the same reason the 本輪非 ok 事件數 field was kept off it.
**The status event lines come out of the same call.** `collect` drained the stream and rotated it (see 「The status event stream」 above), so record `events_total=`, `events_bad=`, `events_unpaired=`, `events_running=`, `events_rotated=` and `events_file=` alongside the rest, and read `item=D-11` for whether that source was readable at all. The 執行狀態事件 subsection of `latest_file` already carries the two detail tables — the non-`ok` events and the starts with no matching end — so never rebuild either by hand and never call `report-status.sh` yourself: a second `drain` this round would either return exit 3 or eat events that then reach no page at all. **The status event lines come out of the same call.** `collect` drained the stream and rotated it (see 「The status event stream」 above), so record `events_total=`, `events_bad=`, `events_unpaired=`, `events_running=`, `events_rotated=` and `events_file=` alongside the rest, and read `item=D-11` for whether that source was readable at all. The 執行狀態事件 subsection of `latest_file` already carries the two detail tables — the non-`ok` events and the starts with no matching end — so never rebuild either by hand and never call `report-status.sh` yourself: a second `drain` this round would either return exit 3 or eat events that then reach no page at all.
@@ -442,7 +444,7 @@ Read-only throughout. This operation creates, modifies and deletes nothing under
Completion condition: every matching sentence is printed, or none of the four combinations applied. Completion condition: every matching sentence is printed, or none of the four combinations applied.
6. **Flag the repeatedly failing tasks.** Append 已連續失敗 N 次 to every row whose `fail_count` is above 0, with `N` taken verbatim from the file. A broken entry that retries every round with nobody noticing is the reason this field exists, so let no such row leave the table unmarked. Completion condition: every row with `fail_count` above 0 carries the marker and its number matches the file. 6. **Flag the repeatedly failing and the overdue tasks.** Append 已連續失敗 N 次 to every row whose `fail_count` is above 0, with `N` taken verbatim from the file. A broken entry that retries every round with nobody noticing is the reason this field exists, so let no such row leave the table unmarked. Then run `{CURRENT}/jsc-assist/tools/due.sh overdue` and append 逾期 {human} to every row it names, with the wording taken from that tool's `human=` field. **Do not compare the `due` field against the clock yourself**: that would be a second judgement of the same thing, and the day the two disagree both will look right. That subcommand is read-only by construction — it advances no snapshot and compares no events, which is why `status` may call it while `scan` may not be called here at all. Exit 2 means at least one `due` field could not be parsed: report those as entries whose deadline was mistyped at registration, because **an unparseable deadline and no deadline look identical on screen**. A paused entry past its deadline is still overdue and is still named; being paused suspends the due judgement, not the calendar. Completion condition: every row with `fail_count` above 0 carries its marker, every row the overdue judgement named carries its own, and the numbers match what the two tools printed.
7. **Check the built-in items against the delegation list, read-only.** Run `{CURRENT}/jsc-assist/tools/seed-tasks.sh plan --root {CURRENT}`. `plan` writes nothing at all — it prints what a reconcile would do and stops — which is what makes it safe here, and `apply` must never be run from `status`. Judge the code by the seed-tasks.sh table and report the difference: the count of items the list expects but the task book lacks, the count of orphans the task book still holds, every `held=` row by name, every `drift=` row with the change the list asks for, every `pending=` row as a slice with an entry point still unwired — this is where a human finds out which reminders are waiting on plumbing rather than on them — and every `probe_bad=` row as an item that fell back to reminding. Say plainly that `start` is what applies any of it. On exit 1, 2 or 3 report that the comparison could not be made and why, and never present that as an aligned task book. Completion condition: the difference is reported with its counts and the held rows named, or the reason it could not be computed is reported, and nothing under `$JSC_HOME` was written. 7. **Check the built-in items against the delegation list, read-only.** Run `{CURRENT}/jsc-assist/tools/seed-tasks.sh plan --root {CURRENT}`. `plan` writes nothing at all — it prints what a reconcile would do and stops — which is what makes it safe here, and `apply` must never be run from `status`. Judge the code by the seed-tasks.sh table and report the difference: the count of items the list expects but the task book lacks, the count of orphans the task book still holds, every `held=` row by name, every `drift=` row with the change the list asks for, every `pending=` row as a slice with an entry point still unwired — this is where a human finds out which reminders are waiting on plumbing rather than on them — and every `probe_bad=` row as an item that fell back to reminding. Say plainly that `start` is what applies any of it. On exit 1, 2 or 3 report that the comparison could not be made and why, and never present that as an aligned task book. Completion condition: the difference is reported with its counts and the held rows named, or the reason it could not be computed is reported, and nothing under `$JSC_HOME` was written.
+159 -20
View File
@@ -8,8 +8,9 @@
# [--created {ISO 時間}] [--last-run {ISO 時間}] [--fail-count {數字}] # [--created {ISO 時間}] [--last-run {ISO 時間}] [--fail-count {數字}]
# [--now {epoch 秒}] # [--now {epoch 秒}]
# #
# scan 一輪一次:比對狀態快照算出本輪新事件、推進快照與事件計數,再逐筆判到期。 # scan 一輪一次:比對狀態快照算出本輪新事件、推進快照與事件計數,再逐筆判到期與逾期。
# events 唯讀預覽:只比對、只印,一律不推進快照,也不判到期。 # events 唯讀預覽:只比對、只印,一律不推進快照,也不判到期。
# overdue 只判逾期,唯讀:一份快照都不推進,一個事件都不比。狀態查詢那一路走這一個。
# next 純算:給一組欄位算出 next_run,不讀待辦簿、不碰快照、不發事件。 # next 純算:給一組欄位算出 next_run,不讀待辦簿、不碰快照、不發事件。
# #
# 結束碼: # 結束碼:
@@ -17,7 +18,8 @@
# 1 至少一個狀態來源存在卻讀不到。**結果照樣印得出來**,只是那一個來源本輪不判事件、 # 1 至少一個狀態來源存在卻讀不到。**結果照樣印得出來**,只是那一個來源本輪不判事件、
# 快照那一段原樣留著,呼叫端要把它標成警示 # 快照那一段原樣留著,呼叫端要把它標成警示
# 2 欄位值不合法或算不出來:trigger 或 recur 解不開、時間字串解不出 epoch、間隔寫錯、 # 2 欄位值不合法或算不出來:trigger 或 recur 解不開、時間字串解不出 epoch、間隔寫錯、
# --now 不是非負整數。scan 遇到這種待辦只把那一筆標成判不了,其餘各筆照判 # due 欄的截止時間解不開、--now 不是非負整數。scan 遇到這種待辦只把那一筆標成判不了,
# 其餘各筆照判
# 3 快照或事件計數換不上去。算是算出來了,但下一輪會拿同一份舊快照再比一次,於是同一批 # 3 快照或事件計數換不上去。算是算出來了,但下一輪會拿同一份舊快照再比一次,於是同一批
# 事件會被判第二次。這一碼要吵出來,不能當成成功 # 事件會被判第二次。這一碼要吵出來,不能當成成功
# 4 待辦簿目錄不存在或零筆,這一輪沒有任何一筆可判。不是失敗,但呼叫端要知道「沒判過」 # 4 待辦簿目錄不存在或零筆,這一輪沒有任何一筆可判。不是失敗,但呼叫端要知道「沒判過」
@@ -139,8 +141,13 @@
# #
# --- 這一支不判什麼 --- # --- 這一支不判什麼 ---
# #
# 不判逾期(due 欄有沒有過),不執行任何一筆待辦,不改任何一筆的 state,不送提醒到前景,不碰 wiki。 # 不執行任何一筆待辦,不改任何一筆的 state,不送提醒到前景,不碰 wiki。
# 它只回答兩個問題:本輪有哪些新事件、哪幾筆現在到期。做不做、怎麼做、失敗怎麼記,都在呼叫端。 # 它只回答三個問題:本輪有哪些新事件、哪幾筆現在到期、哪幾筆逾期了。做不做、怎麼做、失敗怎麼記,
# 都在呼叫端。
#
# 到期與逾期分兩欄,不合成一個判定:到期是「該跑了」,逾期是「due 欄那個截止時間過了」。
# 一筆七天一次的檢查項天天都可能到期,卻永遠不會逾期(它沒有截止時間);一筆有截止時間的交辦
# 可能早就逾期,卻因為排定點還沒到而判成「還沒到」。合成一個值就會把這兩種情況混成同一格。
# #
# 環境變數: # 環境變數:
# JSC_HOME 助理狀態檔的根目錄,預設 ~/.jsc。要是連 HOME 也沒有就回 6,不猜 # JSC_HOME 助理狀態檔的根目錄,預設 ~/.jsc。要是連 HOME 也沒有就回 6,不猜
@@ -197,6 +204,7 @@ usage() {
cat >&2 <<'EOF' cat >&2 <<'EOF'
usage: due.sh scan [--out 目錄] [--now epoch 秒] [--dry-run] usage: due.sh scan [--out 目錄] [--now epoch 秒] [--dry-run]
due.sh events [--now epoch 秒] due.sh events [--now epoch 秒]
due.sh overdue [--now epoch 秒]
due.sh next --trigger at:...|after:... --recur once|every:間隔 due.sh next --trigger at:...|after:... --recur once|every:間隔
[--created ISO 時間] [--last-run ISO 時間] [--fail-count 數字] [--created ISO 時間] [--last-run ISO 時間] [--fail-count 數字]
[--now epoch 秒] [--now epoch 秒]
@@ -869,15 +877,51 @@ kv_get() { # $1=檔案 $2=鍵
# 不寫成 key=value:cron 式子與標題裡有空白,key=value 的那一行沒辦法只靠空白切回來,切錯就是 # 不寫成 key=value:cron 式子與標題裡有空白,key=value 的那一行沒辦法只靠空白切回來,切錯就是
# recur 只剩「cron:0」那半截,而那半截看起來還很像一個完整的值。值一律折過,裡面不會有定位 # recur 只剩「cron:0」那半截,而那半截看起來還很像一個完整的值。值一律折過,裡面不會有定位
# 字元也不會有換行,所以定位字元切得準。 # 字元也不會有換行,所以定位字元切得準。
row_out() { # 十一個欄位,順序如上 row_out() { # 十二個欄位,順序如上
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
"$(flat "$1")" "$(flat "$2")" "$(flat "$3")" "$(flat "$4")" "$(flat "$5")" \ "$(flat "$1")" "$(flat "$2")" "$(flat "$3")" "$(flat "$4")" "$(flat "$5")" \
"$(flat "$6")" "$(flat "$7")" "$(flat "$8")" "$(flat "$9")" \ "$(flat "$6")" "$(flat "$7")" "$(flat "$8")" "$(flat "$9")" \
"$(flat "${10}")" "$(flat "${11}")" >>"$ROWS" "$(flat "${10}")" "$(flat "${11}")" "$(flat "${12}")" >>"$ROWS"
}
# 逾期判定。到期與逾期是兩件事:到期是「該跑了」,逾期是「截止時間過了」。
# 一筆七天一次的檢查項天天都可能到期,卻永遠不會逾期(它沒有截止時間);一筆有截止時間的
# 交辦事項可能早就逾期,卻因為排定點還沒到而判成「還沒到」。兩個判定各自獨立,一欄一個值。
#
# 回傳三種值:due 欄是空的印減號、還沒過印 no、過了印逾期的秒數。
# 印秒數不印 yes 的理由是「逾期一分鐘」與「逾期三個月」在報告上要看得出差別,而呼叫端
# 只要判「是不是數字」就分得出有沒有逾期。
# due 欄解不開的一律印 bad 並算成欄位不合法:一個解不開的截止時間跟沒有截止時間,
# 在畫面上長得一模一樣,而前者是登錄時就填錯了。
# 逾期多久,給人看的寫法。機器可讀的那幾處一律留秒數:秒數比得出大小、也不必解析。
# 頁面上只印秒數讀不出輕重——「528462 秒」要算過才知道是六天,而報告的用途正是讓人一眼
# 分出「逾期一分鐘」與「逾期三個月」。
dur_human() { # $1=秒數
_ds=$(( $1 / 86400 )); _hs=$(( ($1 % 86400) / 3600 )); _ms=$(( ($1 % 3600) / 60 ))
if [ "$_ds" -gt 0 ]; then printf '%s 天 %s 小時' "$_ds" "$_hs"
elif [ "$_hs" -gt 0 ]; then printf '%s 小時 %s 分' "$_hs" "$_ms"
else printf '%s 分' "$_ms"; fi
}
overdue_of() { # $1=due 欄的值;回傳值印在 stdout
case "$1" in
''|'-') printf '%s' '-'; return 0 ;;
esac
_de=$(iso_to_epoch "$1") || _de=''
case "${_de:-}" in
''|*[!0-9]*) printf '%s' 'bad'; return 1 ;;
esac
if [ "$NOW" -gt "$_de" ]; then
printf '%s' "$((NOW - _de))"
else
printf '%s' 'no'
fi
return 0
} }
scan_tasks() { scan_tasks() {
T_TOTAL=0; T_DUE=0; T_WAIT=0; T_INVALID=0; T_UNWIRED=0; T_SKIP=0 T_TOTAL=0; T_DUE=0; T_WAIT=0; T_INVALID=0; T_UNWIRED=0; T_SKIP=0
T_OVERDUE=0; T_DUEBAD=0; OVERDUE_LINES=''
true >"$ROWS" 2>/dev/null || { RC_FS=5; die 5 "逐筆判定的輸出檔寫不進去:$ROWS。"; } true >"$ROWS" 2>/dev/null || { RC_FS=5; die 5 "逐筆判定的輸出檔寫不進去:$ROWS。"; }
if [ ! -d "$TASKS_DIR" ]; then if [ ! -d "$TASKS_DIR" ]; then
RC_EMPTY=4 RC_EMPTY=4
@@ -898,11 +942,28 @@ scan_tasks() {
_cr=$(kv_get "$_f" created) _cr=$(kv_get "$_f" created)
_lr=$(kv_get "$_f" last_run) _lr=$(kv_get "$_f" last_run)
_fc=$(kv_get "$_f" fail_count) _fc=$(kv_get "$_f" fail_count)
_du=$(kv_get "$_f" due)
# 逾期照每一筆都判,連 paused 那幾筆也判。到期判定跳過 paused 是對的(那是人設的,
# 助理判它到期只會讓人以為它還在跑),但截止時間過了這件事不會因為有人按了暫停就消失
# ——一筆被暫停又逾期的交辦,正好是最容易被忘掉的那一種。
# done 那幾筆不算逾期:做完了就沒有截止時間可以過。
_ov='-'
if [ "$_st" != done ]; then
_ov=$(overdue_of "$_du") || { T_DUEBAD=$(( T_DUEBAD + 1 )); RC_FIELD=2; }
case "$_ov" in
''|*[!0-9]*) ;;
*) T_OVERDUE=$(( T_OVERDUE + 1 ))
OVERDUE_LINES="${OVERDUE_LINES}$(printf '%s\t%s\t%s\t%s\t%s' \
"$(flat "$_id")" "$(flat "$_st")" "$(flat "$_du")" "$_ov" "$(flat "$_ti")")
" ;;
esac
fi
if [ "$_st" != pending ]; then if [ "$_st" != pending ]; then
# done 與 paused 一律不判。paused 是人設的,助理判它到期只會讓人以為它還在跑。 # done 與 paused 一律不判到期。
T_SKIP=$(( T_SKIP + 1 )) T_SKIP=$(( T_SKIP + 1 ))
row_out "$_id" skip "$_st" "$_kd" "$_ac" '' '' '' '' 'state 不是 pending,不判到期' "$_ti" row_out "$_id" skip "$_st" "$_kd" "$_ac" '' '' '' '' 'state 不是 pending,不判到期' "$_ti" "$_ov"
continue continue
fi fi
@@ -914,7 +975,7 @@ scan_tasks() {
unwired) T_UNWIRED=$(( T_UNWIRED + 1 )) ;; unwired) T_UNWIRED=$(( T_UNWIRED + 1 )) ;;
esac esac
row_out "$_id" "$R_VERDICT" "$_st" "$_kd" "$_ac" "$_tg" "$_rc" \ row_out "$_id" "$R_VERDICT" "$_st" "$_kd" "$_ac" "$_tg" "$_rc" \
"$R_NEXT" "$R_REARM" "$R_WHY" "$_ti" "$R_NEXT" "$R_REARM" "$R_WHY" "$_ti" "$_ov"
done done
[ "$T_TOTAL" -eq 0 ] && { [ "$T_TOTAL" -eq 0 ] && {
RC_EMPTY=4 RC_EMPTY=4
@@ -931,7 +992,7 @@ compose_md() {
printf '事件靠比對狀態快照偵測:每一輪讀那幾支產生者留下的狀態,跟上一輪的快照比,有差別就算事件發生。快照在 `%s`,事件計數在 `%s`。\n\n' \ printf '事件靠比對狀態快照偵測:每一輪讀那幾支產生者留下的狀態,跟上一輪的快照比,有差別就算事件發生。快照在 `%s`,事件計數在 `%s`。\n\n' \
"$PREV" "$SEEN" "$PREV" "$SEEN"
printf '**兩輪之間發生又消失的事件會漏掉。** 一支工作包在同一個巡檢週期之內鎖上又合併、一個工作階段在同一個週期之內開又關,這一節都看不到——看到的只有兩張快照的差別。往後誰假設「事件不會漏」就會出錯,而那種錯是無聲的:那筆待辦看起來就是還沒到期。\n\n' printf '**兩輪之間發生又消失的事件會漏掉。** 一支工作包在同一個巡檢週期之內鎖上又合併、一個工作階段在同一個週期之內開又關,這一節都看不到——看到的只有兩張快照的差別。往後誰假設「事件不會漏」就會出錯,而那種錯是無聲的:那筆待辦看起來就是還沒到期。\n\n'
printf '這一節不判逾期(`due` 欄有沒有過),也不執行任何一筆。到期的那幾筆要不要做、怎麼做,在呼叫端。\n\n' printf '到期與逾期是兩件事,這一節兩件都判:到期是「該跑了」,逾期是「`due` 欄那個截止時間過了」。一筆七天一次的檢查項天天都可能到期,卻永遠不會逾期;一筆有截止時間的交辦可能早就逾期,卻因為排定點還沒到而判成還沒到。這一節不執行任何一筆——到期的那幾筆要不要做、怎麼做,在呼叫端。\n\n'
printf '| 項目 | 內容 |\n' printf '| 項目 | 內容 |\n'
printf '| --- | --- |\n' printf '| --- | --- |\n'
printf '| 判定時間 | %s |\n' "$(epoch_to_iso "$NOW")" printf '| 判定時間 | %s |\n' "$(epoch_to_iso "$NOW")"
@@ -943,10 +1004,12 @@ compose_md() {
printf '| 本輪新事件 | %s 種 |\n' "$EV_NEW" printf '| 本輪新事件 | %s 種 |\n' "$EV_NEW"
printf '| 待辦簿筆數 | %s |\n' "$T_TOTAL" printf '| 待辦簿筆數 | %s |\n' "$T_TOTAL"
printf '| 到期 | %s |\n' "$T_DUE" printf '| 到期 | %s |\n' "$T_DUE"
printf '| 逾期 | %s |\n' "$T_OVERDUE"
printf '| 還沒到 | %s |\n' "$T_WAIT" printf '| 還沒到 | %s |\n' "$T_WAIT"
printf '| 判不了(欄位不合法) | %s |\n' "$T_INVALID" printf '| 判不了(欄位不合法) | %s |\n' "$T_INVALID"
printf '| 判不了(來源還沒接線) | %s |\n' "$T_UNWIRED" printf '| 判不了(來源還沒接線) | %s |\n' "$T_UNWIRED"
printf '| 不判(state 不是 pending) | %s |\n' "$T_SKIP" printf '| 不判(state 不是 pending) | %s |\n' "$T_SKIP"
[ "$T_DUEBAD" -gt 0 ] && printf '| `due` 欄解不開 | %s |\n' "$T_DUEBAD"
printf '\n#### 狀態來源\n\n' printf '\n#### 狀態來源\n\n'
printf '| 來源 | 狀態 | 筆數 | 說明 |\n' printf '| 來源 | 狀態 | 筆數 | 說明 |\n'
@@ -975,19 +1038,36 @@ compose_md() {
printf '本輪沒有新事件。\n' printf '本輪沒有新事件。\n'
fi fi
printf '\n#### 逾期\n\n'
if [ "$T_OVERDUE" -gt 0 ]; then
printf '**這幾筆的截止時間過了。逾期每一輪都提,不因為提過就不再提**——助理只提醒,不代為執行,也不自己把它移出這一節。\n\n'
printf '| id | state | 截止時間 | 逾期多久 | 標題 |\n'
printf '| --- | --- | --- | ---: | --- |\n'
printf '%s' "$OVERDUE_LINES" | while IFS="$(printf '\t')" read -r _oi _os _od _oa _ot; do
[ -n "$_oi" ] || continue
printf '| `%s` | %s | %s | %s | %s |\n' "$_oi" "$_os" "$(cell "$_od")" "$(dur_human "$_oa")" "$(cell "${_ot:--}")"
done
printf '\n被暫停的那幾筆也算在裡面:截止時間過了這件事不會因為有人按了暫停就消失,而一筆被暫停又逾期的交辦正好是最容易被忘掉的那一種。\n'
else
printf '沒有逾期的待辦。`due` 欄是空的那幾筆沒有截止時間,永遠不會逾期。\n'
fi
if [ "$T_DUEBAD" -gt 0 ]; then
printf '\n有 %s 筆的 `due` 欄解不開,上表 `overdue` 那一欄是 `bad`。**一個解不開的截止時間跟沒有截止時間,在畫面上長得一模一樣**,而前者是登錄的時候就填錯了:重新登錄一筆,`id` 由建立時間與標題算出來,改欄位值算不回同一個 id。\n' "$T_DUEBAD"
fi
printf '\n#### 逐筆判定\n\n' printf '\n#### 逐筆判定\n\n'
if [ -s "$ROWS" ]; then if [ -s "$ROWS" ]; then
printf '| id | 判定 | 標題 | 動作 | trigger | recur | next_run | 重新武裝 | 為什麼 |\n' printf '| id | 判定 | 逾期 | 標題 | 動作 | trigger | recur | next_run | 重新武裝 | 為什麼 |\n'
printf '| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n' printf '| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |\n'
# 這一段交給 awk 一次做完,不用殼的 while read 逐列切。殼的 read 拿定位字元當分隔時會 # 這一段交給 awk 一次做完,不用殼的 while read 逐列切。殼的 read 拿定位字元當分隔時會
# 把連續的定位字元併成一個——定位字元算 IFS 的空白字元——於是空欄位整個消失,後面每一欄 # 把連續的定位字元併成一個——定位字元算 IFS 的空白字元——於是空欄位整個消失,後面每一欄
# 都往前挪一格,印出來的表格看起來完整,值卻全錯位。awk 的 -F'\t' 不併,空欄位就是空欄位。 # 都往前挪一格,印出來的表格看起來完整,值卻全錯位。awk 的 -F'\t' 不併,空欄位就是空欄位。
awk -F'\t' ' awk -F'\t' '
function d(s) { return s == "" ? "-" : s } function d(s) { return s == "" ? "-" : s }
function esc(s) { gsub(/\|/, "\\|", s); return s } function esc(s) { gsub(/\|/, "\\|", s); return s }
NF >= 11 && $1 != "" { NF >= 12 && $1 != "" {
printf "| `%s` | %s | %s | %s | `%s` | `%s` | %s | %s | %s |\n", \ printf "| `%s` | %s | %s | %s | %s | `%s` | `%s` | %s | %s | %s |\n", \
$1, $2, esc(d($11)), esc(d($5)), d($6), d($7), d($8), d($9), esc(d($10)) $1, $2, d($12), esc(d($11)), esc(d($5)), d($6), d($7), d($8), d($9), esc(d($10))
}' "$ROWS" }' "$ROWS"
else else
printf '待辦簿零筆,或目錄不存在。這一輪沒有任何一筆可判——「沒判過」不等於「都沒到期」。\n' printf '待辦簿零筆,或目錄不存在。這一輪沒有任何一筆可判——「沒判過」不等於「都沒到期」。\n'
@@ -1042,9 +1122,9 @@ cmd_scan() {
# stdout 上的 task= 那幾行是給人看的。trigger、recur、why、title 四欄都可能帶空白,所以這 # stdout 上的 task= 那幾行是給人看的。trigger、recur、why、title 四欄都可能帶空白,所以這
# 幾行切不回欄位——要逐筆取值的呼叫端讀 rows_file 那個定位字元分隔的檔案,不要切這幾行。 # 幾行切不回欄位——要逐筆取值的呼叫端讀 rows_file 那個定位字元分隔的檔案,不要切這幾行。
if [ -s "$ROWS" ]; then if [ -s "$ROWS" ]; then
awk -F'\t' 'NF >= 11 && $1 != "" { awk -F'\t' 'NF >= 12 && $1 != "" {
printf "task=%s verdict=%s state=%s kind=%s action=%s next_run=%s rearm_at=%s trigger=%s recur=%s why=%s title=%s\n", \ printf "task=%s verdict=%s state=%s kind=%s action=%s next_run=%s rearm_at=%s trigger=%s recur=%s overdue=%s why=%s title=%s\n", \
$1, $2, $3, $4, $5, $8, $9, $6, $7, $10, $11 $1, $2, $3, $4, $5, $8, $9, $6, $7, $12, $10, $11
}' "$ROWS" }' "$ROWS"
fi fi
printf 'tasks_total=%s\n' "$T_TOTAL" printf 'tasks_total=%s\n' "$T_TOTAL"
@@ -1053,8 +1133,18 @@ cmd_scan() {
printf 'tasks_invalid=%s\n' "$T_INVALID" printf 'tasks_invalid=%s\n' "$T_INVALID"
printf 'tasks_unwired=%s\n' "$T_UNWIRED" printf 'tasks_unwired=%s\n' "$T_UNWIRED"
printf 'tasks_skipped=%s\n' "$T_SKIP" printf 'tasks_skipped=%s\n' "$T_SKIP"
printf 'tasks_overdue=%s\n' "$T_OVERDUE"
printf 'tasks_due_bad=%s\n' "$T_DUEBAD"
# 逾期的那幾筆逐筆點名,不只印一個數字。一份只有數字的回報讀不出「是哪一筆過期了」,
# 而那正是唯一有用的資訊;逾期的處置一律是人接手,人要知道接哪一筆。
if [ -n "$OVERDUE_LINES" ]; then
printf '%s' "$OVERDUE_LINES" | while IFS="$(printf '\t')" read -r _oi _os _od _oa _ot; do
[ -n "$_oi" ] || continue
printf 'overdue_row=%s state=%s due=%s overdue_secs=%s title=%s\n' "$_oi" "$_os" "$_od" "$_oa" "$_ot"
done
fi
printf 'rows_file=%s\n' "$ROWS" printf 'rows_file=%s\n' "$ROWS"
printf 'rows_columns=id verdict state kind action trigger recur next_run rearm_at why title\n' printf 'rows_columns=id verdict state kind action trigger recur next_run rearm_at why title overdue\n'
printf 'due_file=%s\n' "$MD" printf 'due_file=%s\n' "$MD"
printf 'prev_file=%s\n' "$PREV" printf 'prev_file=%s\n' "$PREV"
printf 'seen_file=%s\n' "$SEEN" printf 'seen_file=%s\n' "$SEEN"
@@ -1062,6 +1152,53 @@ cmd_scan() {
finish_rc finish_rc
} }
# 只判逾期,一律唯讀。
#
# 為什麼另開一個子命令:逾期判定跟事件與到期判定不同,它一個狀態都不必比、一份快照都不必推進
# ——只要把每一筆的 due 欄跟現在比一下。狀態查詢那一路每次都想知道「有沒有逾期的」,走 scan
# 就會推進快照,同一輪之後的事件判定會全部落空;走 events 又判不到逾期。
# 讓呼叫端自己拿 due 欄去比更糟:那就是第二套判定,跟這一支的規則會漂移,而漂移的那一天
# 兩邊都說自己是對的。
cmd_overdue() {
while [ "$#" -gt 0 ]; do
case "$1" in
--now) [ "$#" -ge 2 ] || usage; NOW_ARG="$2"; shift 2 ;;
*) usage ;;
esac
done
resolve_now
T_TOTAL=0; T_OVERDUE=0; T_DUEBAD=0; OVERDUE_LINES=''
if [ ! -d "$TASKS_DIR" ]; then
RC_EMPTY=4
note "待辦簿目錄不存在($TASKS_DIR),這一輪沒有任何一筆可判。「沒判過」不等於「都沒逾期」。"
else
for _f in "$TASKS_DIR"/*; do
[ -f "$_f" ] || continue
T_TOTAL=$(( T_TOTAL + 1 ))
_id=$(kv_get "$_f" id); [ -n "$_id" ] || _id=$(basename "$_f")
_st=$(kv_get "$_f" state); [ -n "$_st" ] || _st=pending
_ti=$(kv_get "$_f" title)
_du=$(kv_get "$_f" due)
[ "$_st" = done ] && continue
_ov=$(overdue_of "$_du") || { T_DUEBAD=$(( T_DUEBAD + 1 )); RC_FIELD=2; }
case "$_ov" in
''|*[!0-9]*) continue ;;
esac
T_OVERDUE=$(( T_OVERDUE + 1 ))
printf 'overdue_row=%s state=%s due=%s overdue_secs=%s human=%s title=%s\n' \
"$_id" "$_st" "$_du" "$_ov" "$(dur_human "$_ov")" "$_ti"
done
[ "$T_TOTAL" -eq 0 ] && { RC_EMPTY=4; note "待辦簿目錄在($TASKS_DIR),但零筆。"; }
fi
printf 'now=%s\n' "$(epoch_to_iso "$NOW")"
printf 'tasks_total=%s\n' "$T_TOTAL"
printf 'tasks_overdue=%s\n' "$T_OVERDUE"
printf 'tasks_due_bad=%s\n' "$T_DUEBAD"
note '這是唯讀判定:一份快照都沒推進,一筆待辦都沒改。逾期的處置一律是人接手。'
cleanup_tmp
finish_rc
}
cmd_events() { cmd_events() {
while [ "$#" -gt 0 ]; do while [ "$#" -gt 0 ]; do
case "$1" in case "$1" in
@@ -1149,6 +1286,7 @@ FIRST_RUN=0
EV_NAMES='' EV_NAMES=''
EV_NEW=0 EV_NEW=0
T_TOTAL=0; T_DUE=0; T_WAIT=0; T_INVALID=0; T_UNWIRED=0; T_SKIP=0 T_TOTAL=0; T_DUE=0; T_WAIT=0; T_INVALID=0; T_UNWIRED=0; T_SKIP=0
T_OVERDUE=0; T_DUEBAD=0; OVERDUE_LINES=''
CMD="${1:-}" CMD="${1:-}"
[ -n "$CMD" ] || usage [ -n "$CMD" ] || usage
@@ -1156,6 +1294,7 @@ shift
case "$CMD" in case "$CMD" in
scan) cmd_scan "$@" ;; scan) cmd_scan "$@" ;;
events) cmd_events "$@" ;; events) cmd_events "$@" ;;
overdue) cmd_overdue "$@" ;;
next) cmd_next "$@" ;; next) cmd_next "$@" ;;
*) usage ;; *) usage ;;
esac esac
+42 -3
View File
@@ -148,6 +148,7 @@
# warn_sources= 本輪的警示來源,以「、」分隔;沒有警示就是「無」。各項全過卻判成警示 # warn_sources= 本輪的警示來源,以「、」分隔;沒有警示就是「無」。各項全過卻判成警示
# 時,原因只寫在這裡 # 時,原因只寫在這裡
# tasks_total= tasks_failing= 待辦簿筆數與連續失敗筆數,供目錄頁那一個區塊與摘要用 # tasks_total= tasks_failing= 待辦簿筆數與連續失敗筆數,供目錄頁那一個區塊與摘要用
# tasks_overdue= 逾期筆數(截止時間已經過了),取自到期判定那一支;判不出來時是減號
# tasks_due= 本輪到期的筆數;到期判定那一支失敗時為空 # tasks_due= 本輪到期的筆數;到期判定那一支失敗時為空
# events_new= 本輪偵測到的新事件種類數;到期判定那一支失敗時為空 # events_new= 本輪偵測到的新事件種類數;到期判定那一支失敗時為空
# due_status= due_rc= 到期判定那一支的結果與結束碼 # due_status= due_rc= 到期判定那一支的結果與結束碼
@@ -209,6 +210,8 @@ DUE_ROWS=''
DUE_NOTE='' DUE_NOTE=''
DUE_TASKS='' DUE_TASKS=''
DUE_EVENTS='' DUE_EVENTS=''
DUE_OVERDUE=''
FAILING_LINES=''
# 這支腳本是不是從 $JSC_HOME/current 那一組路徑被叫起來的。不是就大聲警告,但照跑。 # 這支腳本是不是從 $JSC_HOME/current 那一組路徑被叫起來的。不是就大聲警告,但照跑。
# 只警告、不中止是刻意的取捨:從工作樹直接跑腳本是開發時的正當用法,中止會把那條路擋掉; # 只警告、不中止是刻意的取捨:從工作樹直接跑腳本是開發時的正當用法,中止會把那條路擋掉;
@@ -875,7 +878,7 @@ d11() {
# --- 待辦簿筆數(只供目錄頁那一個區塊用)--- # --- 待辦簿筆數(只供目錄頁那一個區塊用)---
count_tasks() { count_tasks() {
TASKS_TOTAL=0; TASKS_FAILING=0 TASKS_TOTAL=0; TASKS_FAILING=0; FAILING_LINES=''
_d="$STATE_DIR/tasks" _d="$STATE_DIR/tasks"
[ -d "$_d" ] && [ -r "$_d" ] || return 0 [ -d "$_d" ] && [ -r "$_d" ] || return 0
for _f in "$_d"/*; do for _f in "$_d"/*; do
@@ -883,7 +886,19 @@ count_tasks() {
TASKS_TOTAL=$(( TASKS_TOTAL + 1 )) TASKS_TOTAL=$(( TASKS_TOTAL + 1 ))
_fc=$(sed -n 's/^fail_count=//p' "$_f" 2>/dev/null | head -n1) _fc=$(sed -n 's/^fail_count=//p' "$_f" 2>/dev/null | head -n1)
case "$_fc" in ''|*[!0-9]*) _fc=0 ;; esac case "$_fc" in ''|*[!0-9]*) _fc=0 ;; esac
[ "$_fc" -gt 0 ] && TASKS_FAILING=$(( TASKS_FAILING + 1 )) if [ "$_fc" -gt 0 ]; then
TASKS_FAILING=$(( TASKS_FAILING + 1 ))
# 逐筆點名,不只累加。一個數字說得出「有幾筆在連續失敗」,說不出是哪幾筆,而失敗次數
# 這個欄位存在的理由是指出「有一筆壞掉的項目每輪重試而沒人知道」——不說出是哪一筆,
# 那個理由就只完成了一半。
_ti=$(sed -n 's/^title=//p' "$_f" 2>/dev/null | head -n1)
_id=$(sed -n 's/^id=//p' "$_f" 2>/dev/null | head -n1)
[ -n "$_id" ] || _id=$(basename -- "$_f")
_lr=$(sed -n 's/^last_run=//p' "$_f" 2>/dev/null | head -n1)
FAILING_LINES="${FAILING_LINES}$(printf '%s\t%s\t%s\t%s' \
"$_id" "$_fc" "${_lr:--}" "${_ti:--}")
"
fi
done done
return 0 return 0
} }
@@ -920,6 +935,7 @@ due_scan() {
DUE_ROWS=$(sed -n 's/^rows_file=//p' "$RD/due.out" 2>/dev/null | head -n1) DUE_ROWS=$(sed -n 's/^rows_file=//p' "$RD/due.out" 2>/dev/null | head -n1)
DUE_TASKS=$(sed -n 's/^tasks_due=//p' "$RD/due.out" 2>/dev/null | head -n1) DUE_TASKS=$(sed -n 's/^tasks_due=//p' "$RD/due.out" 2>/dev/null | head -n1)
DUE_EVENTS=$(sed -n 's/^events_new=//p' "$RD/due.out" 2>/dev/null | head -n1) DUE_EVENTS=$(sed -n 's/^events_new=//p' "$RD/due.out" 2>/dev/null | head -n1)
DUE_OVERDUE=$(sed -n 's/^tasks_overdue=//p' "$RD/due.out" 2>/dev/null | head -n1)
case "$_rc" in case "$_rc" in
0) DUE_STATUS=ok ;; 0) DUE_STATUS=ok ;;
4) 4)
@@ -997,8 +1013,19 @@ compose() {
cat "$RD/d07.md"; printf '\n' cat "$RD/d07.md"; printf '\n'
if [ -n "$DUE_MD" ] && [ -s "$DUE_MD" ]; then if [ -n "$DUE_MD" ] && [ -s "$DUE_MD" ]; then
cat "$DUE_MD" cat "$DUE_MD"
printf '\n本輪待辦簿共 %s 筆,其中 %s 筆 `fail_count` 大於 0。逾期(`due` 欄已經過了)還沒實作,上面的判定只講到期,不講逾期。\n\n' \ printf '\n本輪待辦簿共 %s 筆,其中 %s 筆 `fail_count` 大於 0。\n\n' \
"$TASKS_TOTAL" "$TASKS_FAILING" "$TASKS_TOTAL" "$TASKS_FAILING"
if [ -n "$FAILING_LINES" ]; then
printf '#### 連續失敗的那幾筆\n\n'
printf '**這幾筆每一輪都會再試一次,而且不會自動暫停。** 助理不替人按暫停:那個狀態留給人設,也只有人解得開。\n\n'
printf '| id | 已連續失敗 | 最後一次執行 | 標題 |\n'
printf '| --- | ---: | --- | --- |\n'
printf '%s' "$FAILING_LINES" | while IFS="$(printf '\t')" read -r _fi _fn _fl _ft; do
[ -n "$_fi" ] || continue
printf '| `%s` | %s 次 | %s | %s |\n' "$_fi" "$_fn" "$(cell "$_fl")" "$(cell "$_ft")"
done
printf '\n'
fi
else else
printf '### 待辦簿到期與逾期\n\n' printf '### 待辦簿到期與逾期\n\n'
printf '**這一輪判不出到期。** 到期判定那一支(`tools/due.sh`)%s。本輪待辦簿共 %s 筆,其中 %s 筆 `fail_count` 大於 0。這一節沒有判定結果**不代表沒有任何一筆到期**,要逐筆看就跑 `/jsc-assist:assistant status`。\n\n' \ printf '**這一輪判不出到期。** 到期判定那一支(`tools/due.sh`)%s。本輪待辦簿共 %s 筆,其中 %s 筆 `fail_count` 大於 0。這一節沒有判定結果**不代表沒有任何一筆到期**,要逐筆看就跑 `/jsc-assist:assistant status`。\n\n' \
@@ -1129,6 +1156,17 @@ case "$CMD" in
d11 d11
count_tasks count_tasks
due_scan due_scan
# 逾期與連續失敗,每一輪都列一筆待人處理。
#
# 這兩件事的處置都是人接手,而且**不會自己好**:一筆逾期的交辦不會因為過了更久就不逾期,
# 一筆連續失敗的檢查項每一輪都會再試一次然後再失敗一次。所以每一輪都提,不因為上一輪
# 提過就這一輪不提——「提過了」不是「處理過了」,而助理不替人按暫停:那個狀態留給人設。
if [ -n "$DUE_OVERDUE" ] && [ "$DUE_OVERDUE" -gt 0 ] 2>/dev/null; then
add_pending "有 $DUE_OVERDUE 筆待辦逾期,截止時間已經過了" '待辦簿到期與逾期' '/jsc-assist:assistant status'
fi
if [ "$TASKS_FAILING" -gt 0 ]; then
add_pending "有 $TASKS_FAILING 筆待辦連續失敗,每一輪都在重試" '待辦簿到期與逾期' '/jsc-assist:assistant status'
fi
tally "$D01_STATUS"; tally "$D04_STATUS"; tally "$D07_STATUS"; tally "$D09_STATUS" tally "$D01_STATUS"; tally "$D04_STATUS"; tally "$D07_STATUS"; tally "$D09_STATUS"
tally "$D11_STATUS" tally "$D11_STATUS"
@@ -1159,6 +1197,7 @@ case "$CMD" in
printf 'tasks_total=%s\n' "$TASKS_TOTAL" printf 'tasks_total=%s\n' "$TASKS_TOTAL"
printf 'tasks_failing=%s\n' "$TASKS_FAILING" printf 'tasks_failing=%s\n' "$TASKS_FAILING"
printf 'tasks_due=%s\n' "$DUE_TASKS" printf 'tasks_due=%s\n' "$DUE_TASKS"
printf 'tasks_overdue=%s\n' "${DUE_OVERDUE:--}"
printf 'events_new=%s\n' "$DUE_EVENTS" printf 'events_new=%s\n' "$DUE_EVENTS"
printf 'due_status=%s\n' "$DUE_STATUS" printf 'due_status=%s\n' "$DUE_STATUS"
printf 'due_rc=%s\n' "$DUE_RC" printf 'due_rc=%s\n' "$DUE_RC"
+3 -3
View File
@@ -306,9 +306,9 @@ RC_CMD=0; RC_WRITE=0
# 欄位數,對不上一筆都不跑,而不是照舊讀進錯的欄位。 # 欄位數,對不上一筆都不跑,而不是照舊讀進錯的欄位。
_cols=$(awk -F"$TAB" 'NF>1{print NF; exit}' "$ROWS" 2>/dev/null) _cols=$(awk -F"$TAB" 'NF>1{print NF; exit}' "$ROWS" 2>/dev/null)
case "${_cols:-0}" in case "${_cols:-0}" in
11) ;; 12) ;;
0) note '到期清單是空的,這一輪沒有任何一筆要跑。'; _cols=11 ;; 0) note '到期清單是空的,這一輪沒有任何一筆要跑。'; _cols=12 ;;
*) die 2 "到期清單的欄位數是 ${_cols},這一支認得的是 11 欄。判到期那一支的輸出格式換過了,先對齊再跑——照舊讀下去會把指令讀成別的欄位。" ;; *) die 2 "到期清單的欄位數是 ${_cols},這一支認得的是 12 欄。判到期那一支的輸出格式換過了,先對齊再跑——照舊讀下去會把指令讀成別的欄位。第 12 欄是逾期判定,跟這一支要跑什麼無關,但欄位數不對就代表兩支不是同一版,那時候前 11 欄的順序也不能假設還是原來那樣。" ;;
esac esac
while IFS="$TAB" read -r c_id c_verdict c_state c_kind c_action c_trigger c_recur c_next c_rearm c_why c_rest; do while IFS="$TAB" read -r c_id c_verdict c_state c_kind c_action c_trigger c_recur c_next c_rearm c_why c_rest; do