blob: b52cd3d23dfec1a1dc96e2af1adbb0b3a95fcdcf (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
|
#!/usr/bin/env bash
#
# task-review-staleness.sh — count top-level todo.org tasks whose review
# has gone stale.
#
# Usage: task-review-staleness.sh <todo-file> <threshold-days>
#
# Prints a single integer to stdout: the number of qualifying tasks that
# are stale. Shared by the wrap-up health check (threshold 30) and the
# startup reminder (threshold 7) so both count the same way.
#
# A qualifying task is a depth-2 (**) heading carrying a TODO/DOING/VERIFY
# keyword and an [#A]/[#B]/[#C] priority cookie. DONE/CANCELLED tasks,
# deeper headings, and cookie-less headings are not review units.
#
# A qualifying task is stale when its :LAST_REVIEWED: property is missing
# or unparseable (NIL sorts oldest), or when its age strictly exceeds the
# threshold (age > N days; age == N is still fresh).
set -euo pipefail
die() { echo "task-review-staleness: $*" >&2; exit 2; }
[ "$#" -eq 2 ] || die "usage: $(basename "$0") <todo-file> <threshold-days>"
todo_file="$1"
threshold="$2"
[ -f "$todo_file" ] || die "no such file: $todo_file"
[[ "$threshold" =~ ^[0-9]+$ ]] || die "threshold must be a non-negative integer: $threshold"
# Emit one line per qualifying top-level task: its LAST_REVIEWED value, or
# "NONE" when the task has no such property. Body prose is ignored; only
# the property drawer between a qualifying heading and the next heading is
# scanned.
extract_review_dates() {
awk '
function flush() {
if (in_task) print (have_lr ? lr : "NONE")
}
/^\*+ / {
flush()
have_lr = 0; lr = ""
in_task = ($0 ~ /^\*\* (TODO|DOING|VERIFY) \[#[ABC]\]/)
next
}
in_task && /^[ \t]*:LAST_REVIEWED:[ \t]*/ {
line = $0
sub(/^[ \t]*:LAST_REVIEWED:[ \t]*/, "", line)
sub(/[ \t]*$/, "", line)
lr = line; have_lr = 1
next
}
END { flush() }
' "$todo_file"
}
# Normalize "today" to local midnight so a task reviewed exactly N days
# ago measures as N, not N-and-a-fraction. LAST_REVIEWED dates parse to
# midnight already, so both ends of the diff sit on day boundaries.
today_epoch=$(date -d "$(date +%F)" +%s)
count=0
while IFS= read -r value; do
if [ "$value" = "NONE" ]; then
count=$((count + 1))
continue
fi
# Unparseable date → treat as NIL (stale).
if ! rev_epoch=$(date -d "$value" +%s 2>/dev/null); then
count=$((count + 1))
continue
fi
# Round to nearest day so a DST hour can't shift a boundary task.
age_days=$(( (today_epoch - rev_epoch + 43200) / 86400 ))
if [ "$age_days" -gt "$threshold" ]; then
count=$((count + 1))
fi
done < <(extract_review_dates)
echo "$count"
|