diff options
Diffstat (limited to 'claude-templates/.ai/scripts')
| -rwxr-xr-x | claude-templates/.ai/scripts/task-review-staleness.sh | 40 | ||||
| -rw-r--r-- | claude-templates/.ai/scripts/tests/task-review-staleness.bats | 53 |
2 files changed, 84 insertions, 9 deletions
diff --git a/claude-templates/.ai/scripts/task-review-staleness.sh b/claude-templates/.ai/scripts/task-review-staleness.sh index ed43712..50e0257 100755 --- a/claude-templates/.ai/scripts/task-review-staleness.sh +++ b/claude-templates/.ai/scripts/task-review-staleness.sh @@ -19,9 +19,14 @@ # deeper headings, and cookie-less headings are not review units. # # A task is stale (count mode), and sorts oldest (list mode), when its -# :LAST_REVIEWED: property is missing or unparseable (NIL sorts first), or -# when its age strictly exceeds the threshold (age > N days; age == N is -# still fresh). +# :LAST_REVIEWED: property is missing (NIL sorts first) or when its age +# strictly exceeds the threshold (age > N days; age == N is still fresh). +# +# :LAST_REVIEWED: accepts a bare date (2026-07-09) or an org-native +# timestamp ([2026-07-09 Thu] or <2026-07-09 Thu ...>); both normalize to +# the ISO date. A value that is present but parses to neither is a data +# error: the script warns loudly to stderr (file:line:value) and leaves it +# out of the stale count rather than silently treating it as never-reviewed. set -euo pipefail @@ -46,7 +51,7 @@ num="$2" # only the property drawer between a qualifying heading and the next heading # is scanned. extract_tasks() { - awk ' + awk -v fname="$todo_file" ' function flush() { if (in_task) printf "%s\t%s\t%s\n", hline, (have_lr ? lr : "NONE"), heading } @@ -65,7 +70,21 @@ extract_tasks() { v = $0 sub(/^[ \t]*:LAST_REVIEWED:[ \t]*/, "", v) sub(/[ \t]*$/, "", v) - lr = v; have_lr = 1 + raw = v + # Accept org-native stamps: strip a leading [ or < (inactive/active + # timestamp bracket) and take the leading ISO date, so [2026-07-09 Thu] + # and 2026-07-09 both normalize to 2026-07-09. + sub(/^[[<]/, "", v) + if (match(v, /^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/)) { + lr = substr(v, RSTART, RLENGTH); have_lr = 1 + } else { + # Present but unparseable — a data error, not "never reviewed". Warn + # loudly (file:line:value) instead of silently folding it into the + # stale count, where a re-review in the same bad format would never + # drop the number and nothing would explain why. + printf "task-review-staleness: %s:%d: unparseable :LAST_REVIEWED: %s (expected YYYY-MM-DD or [YYYY-MM-DD Day])\n", fname, NR, raw > "/dev/stderr" + lr = "INVALID"; have_lr = 1 + } next } END { flush() } @@ -98,9 +117,16 @@ while IFS=$'\t' read -r hline value heading; do continue fi - # Unparseable date → treat as NIL (stale). + # Malformed stamp: extract_tasks already warned. A data error is not + # "never reviewed", so it stays out of the stale count. + if [ "$value" = "INVALID" ]; then + continue + fi + + # A shape-valid but impossible date (e.g. 2026-13-45) slips past the awk + # regex; warn and skip rather than silently counting it. if ! rev_epoch=$(date -d "$value" +%s 2>/dev/null); then - count=$((count + 1)) + echo "task-review-staleness: $todo_file: unparseable :LAST_REVIEWED: $value" >&2 continue fi diff --git a/claude-templates/.ai/scripts/tests/task-review-staleness.bats b/claude-templates/.ai/scripts/tests/task-review-staleness.bats index 488b023..79aad79 100644 --- a/claude-templates/.ai/scripts/tests/task-review-staleness.bats +++ b/claude-templates/.ai/scripts/tests/task-review-staleness.bats @@ -49,6 +49,16 @@ task_unreviewed() { printf '** %s [#%s] %s\nBody.\n\n' "$keyword" "$prio" "$title" >> "$TODO" } +# Emit a qualifying task whose LAST_REVIEWED is an org-native inactive +# timestamp — [YYYY-MM-DD Day] — matching the CREATED:/CLOSED: cookies that +# sit in the same drawer. The date is derived from an ISO date via `date`. +task_reviewed_org() { + local keyword="$1" prio="$2" title="$3" isodate="$4" + local org="[$(date -d "$isodate" '+%F %a')]" + printf '** %s [#%s] %s\n:PROPERTIES:\n:LAST_REVIEWED: %s\n:END:\nBody.\n\n' \ + "$keyword" "$prio" "$title" "$org" >> "$TODO" +} + # ---- Normal cases ---------------------------------------------------- @test "staleness: empty file reports zero" { @@ -85,6 +95,20 @@ task_unreviewed() { [ "$output" = "2" ] } +@test "staleness: org-native bracketed LAST_REVIEWED parses — recent is fresh" { + task_reviewed_org TODO A "Reviewed five days ago, org stamp" "$D5" + run bash "$SCRIPT" "$TODO" 30 + [ "$status" -eq 0 ] + [ "$output" = "0" ] +} + +@test "staleness: org-native bracketed LAST_REVIEWED parses — old is stale" { + task_reviewed_org TODO A "Reviewed forty days ago, org stamp" "$D40" + run bash "$SCRIPT" "$TODO" 30 + [ "$status" -eq 0 ] + [ "$output" = "1" ] +} + # ---- Boundary cases -------------------------------------------------- @test "staleness: age exactly equal to threshold is fresh" { @@ -136,9 +160,23 @@ task_unreviewed() { [ "$output" = "0" ] } -@test "staleness: malformed LAST_REVIEWED is treated as stale" { +@test "staleness: malformed LAST_REVIEWED warns to stderr and is not counted" { task_reviewed TODO A "Bad date" "not-a-date" - run bash "$SCRIPT" "$TODO" 30 + # stdout carries only the count — the malformed stamp is not folded in. + run bash -c "bash '$SCRIPT' '$TODO' 30 2>/dev/null" + [ "$status" -eq 0 ] + [ "$output" = "0" ] + # stderr carries the loud warning naming the offending value. + run bash -c "bash '$SCRIPT' '$TODO' 30 2>&1 1>/dev/null" + [ "$status" -eq 0 ] + [[ "$output" == *"not-a-date"* ]] + [[ "$output" == *"LAST_REVIEWED"* ]] +} + +@test "staleness: malformed stamp is excluded while real stale tasks still count" { + task_reviewed TODO A "Real stale" "$D40" + task_reviewed TODO B "Broken stamp" "garbage" + run bash -c "bash '$SCRIPT' '$TODO' 30 2>/dev/null" [ "$status" -eq 0 ] [ "$output" = "1" ] } @@ -161,6 +199,17 @@ task_unreviewed() { [[ "${lines[2]}" == *"Reviewed recently"* ]] } +@test "staleness --list: org-native bracketed stamp sorts by its real date" { + task_reviewed TODO A "Bare recent" "$D5" + task_reviewed_org TODO B "Org-stamped old" "$D40" + run bash "$SCRIPT" --list "$TODO" 10 + [ "$status" -eq 0 ] + # The org-bracketed old stamp must sort ahead of the bare recent one — + # proof it parsed to a real date rather than falling to 0000-00-00. + [[ "${lines[0]}" == *"Org-stamped old"* ]] + [[ "${lines[1]}" == *"Bare recent"* ]] +} + @test "staleness --list: takes only the requested count" { task_unreviewed TODO A "First" task_reviewed TODO B "Second" "$D40" |
