/ devlog / devscripts / check_devlog.sh
#!/bin/sh

# The arc's own sections are Chris's to set. A new one added by this
# session was refused work he never asked for, and a fix found while
# implementing an item is part of that item rather than a section or an
# entry of its own. So a patch may not add a top-level entry to the
# plan: the number of them has to be what gold already holds.
top_level_count()
{
    awk '/^<li>/ {count++} END {print count + 0}' "$1"
}
check_top_level()
{
    plan="$1"
    gold_plan="$GOLD/$plan"
    if [ ! -f "$gold_plan" ]; then
        return 0
    fi
    here=`top_level_count "$plan"`
    there=`top_level_count "$gold_plan"`
    allowed=${SANCTIONED_ENTRIES:-0}
    if [ "$here" -gt "`expr $there + $allowed`" ]; then
        echo "check_devlog: FAIL the plan grew by `expr $here - $there`"
        echo "  entry(s) written at its outer edge. Those are Chris's to"
        echo "  add: work belongs under the item it is part of, and a"
        echo "  fix found while doing an item is part of that item."
        return 1
    fi
    return 0
}
# Refuses a plan that carries a checkmark this session put there, or a
# typed outline label, or unbalanced list tags. Only Chris sets a
# checkmark; a plan file is compared against gold to find any that were
# added since.
#
# Where Chris has asked in so many words for entries to be added at the
# outer edge, run this with SANCTIONED_ENTRIES set to how many he asked
# for, and that many are allowed. Any beyond that are still refused, so
# the number has to be written down and shown in the round's output.
#
# Where Chris has said in so many words which items to check, run this
# with CONFIRMED_CHECKS set to how many he asked for, and that many are
# allowed. Any beyond that number are still refused, so the count has to
# be written down and shown in the round's output rather than the guard
# being stepped around.
GOLD=${GOLD:-/home/claude/gold}
CONFIRMED_CHECKS=${CONFIRMED_CHECKS:-0}
plan="$1"
# With no plan named, take the newest dated arc folder that holds one.
# Naming one arc here meant the check went on reading that arc long
# after the work had moved to the next, so it passed a plan nobody was
# writing in.
if [ -z "$plan" ]; then
    plan=`ls -d devlog/v10/*/plan.html 2>/dev/null | sort | tail -1`
fi
if [ -z "$plan" ] || [ ! -f "$plan" ]; then
    echo "check_devlog: FAIL no plan to read"
    exit 1
fi
fail=0
# A checkmark is written with either class the plans use, so both are
# counted. Counting one of them let eighteen checkmarks through unseen.
marks='class="\(check\|done\)">&#10003;'
here=`grep -c "$marks" "$plan"`
there=`git -C "$GOLD" show "confirmed-head:$plan" 2>/dev/null | \
    grep -c "$marks"`
if [ "$there" = "" ]; then
    there=0
fi
added=$((here - there))
if [ "$added" -gt "$CONFIRMED_CHECKS" ]; then
    echo "check_devlog: FAIL added $added checkmark(s), $CONFIRMED_CHECKS"
    echo "  confirmed; only Chris sets those. Use the waiting marker."
    fail=1
elif [ "$added" -gt 0 ]; then
    echo "check_devlog: $added checkmark(s) added, which Chris asked for"
fi
opens=`grep -o '<li' "$plan" | wc -l`
closes=`grep -o '</li>' "$plan" | wc -l`
if [ "$opens" != "$closes" ]; then
    echo "check_devlog: FAIL $opens <li> against $closes </li>"
    fail=1
fi
opens=`grep -o '<ol' "$plan" | wc -l`
closes=`grep -o '</ol>' "$plan" | wc -l`
if [ "$opens" != "$closes" ]; then
    echo "check_devlog: FAIL $opens <ol> against $closes </ol>"
    fail=1
fi
typed=`grep -cE '<li[^>]*>[[:space:]]*(<[^>]+>)*[[:space:]]*([IVX]+|[A-Z]|[0-9]+)\.' "$plan"`
if [ "$typed" -gt 0 ]; then
    echo "check_devlog: FAIL $typed typed outline label(s)"
    fail=1
fi
if ! check_top_level "$plan"; then
    fail=1
fi
# The plan rule says no sentence in an entry runs past about twenty
# words, and that rule was the one with no check behind it: a round
# wrote an entry of nine long sentences and nothing caught it. The plan
# is append-only and holds older entries written before this check, so
# only the lines this patch adds are read, found by diffing against the
# copy gold holds. Each added line's prose is pulled out tag-free, split
# into sentences on a period, and any sentence past the limit is named.
# A title in <strong> is dropped first, since a full-sentence title does
# not count toward the sentence rule, and a few words of slack are
# allowed over twenty, since the rule says "about".
WORD_LIMIT=${DEVLOG_WORD_LIMIT:-24}
added_lines=`git -C "$GOLD" show "confirmed-head:$plan" 2>/dev/null | \
    diff - "$plan" | grep '^>' | sed 's/^> //'`
long_sentences=`echo "$added_lines" | awk -v limit="$WORD_LIMIT" '
    {
        line = $0
        gsub(/<strong>[^<]*<\/strong>/, "", line)
        gsub(/<[^>]*>/, " ", line)
        gsub(/&#[0-9]+;/, "", line)
        gsub(/&[a-z]+;/, "", line)
        prose = prose " " line
    }
    END {
        n = split(prose, parts, /[.!?]/)
        for (i = 1; i <= n; i++) {
            sentence = parts[i]
            words = split(sentence, w, /[ \t]+/)
            count = 0
            for (j = 1; j <= words; j++) {
                if (w[j] != "") {
                    count++
                }
            }
            if (count > limit) {
                printf "%d\n", count
            }
        }
    }
'`
if [ -n "$long_sentences" ]; then
    worst=`echo "$long_sentences" | sort -rn | head -1`
    many=`echo "$long_sentences" | wc -l | tr -d ' '`
    echo "check_devlog: FAIL $many added sentence(s) past $WORD_LIMIT words,"
    echo "  the longest $worst. Each entry sentence stays near twenty"
    echo "  words; split the long ones or move detail to a nested part."
    fail=1
fi
if [ "$fail" = "0" ]; then
    echo "check_devlog: PASS $plan"
fi
exit $fail
X