plan-boundary 4.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
  1. #!/usr/bin/env bash
  2. # Check the next plan against the code as built before starting it.
  3. #
  4. # Collects every backticked identifier in the plan's task-level `Consumes`
  5. # lines and in its own Plan Set entry, drops the ones this plan itself
  6. # produces (its `Produces` lines and the functions/types its code blocks
  7. # define), and reports each remaining name that does not occur in the
  8. # repository's source outside plans/ and docs/. Then, for every plan that
  9. # comes after it in the same directory, checks the names its Plan Set entry
  10. # attributes to plans already complete ("from plan K: ...", K before the next
  11. # plan), so a ruling in the plan just finished cannot leave a later plan
  12. # consuming a name the code does not have. A hit is a plan that says what the
  13. # code does not: fix the plan, re-run until it prints "boundary: clean".
  14. #
  15. # A plan is a file or a directory (00-header.md plus NN-<task>.md files).
  16. #
  17. # Usage: plan-boundary NEXT_PLAN
  18. # Exit: 0 when clean, 1 when any consumed name is missing, 2 on usage error.
  19. set -euo pipefail
  20. [ $# -eq 1 ] || { echo "usage: plan-boundary NEXT_PLAN" >&2; exit 2; }
  21. next=${1%/}; [ -e "$next" ] || { echo "no such plan: $next" >&2; exit 2; }
  22. root=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
  23. stop='^(and|or|the|from|of|in|a|an|to|with|for|int|string|bool|byte|rune|error|nil|true|false|func|type|struct|var|const|Plan|Task|Tasks|plan|plans|md|s|m|g|t|x|y|dt|None|self|str|list|dict|float|Decimal|date|datetime|time|Duration)$'
  24. idents() { # backticked spans on stdin -> identifiers, one per line; paths and files skipped
  25. grep -o '`[^`]*`' | tr -d '`' | grep -v -e '/' -e '\.md$' | grep -o '[A-Za-z_][A-Za-z0-9_]*' | sort -u
  26. }
  27. plan_text() { if [ -d "$1" ]; then cat "$1"/00-header.md "$1"/[0-9][0-9]-*.md 2>/dev/null; else cat "$1"; fi; }
  28. in_code() { git -C "$root" grep -q -w -- "$1" -- ':!plans/*' ':!docs/*' ':!*.md' 2>/dev/null; }
  29. missing=0; found=0
  30. check() { # names on stdin, label
  31. local n; while IFS= read -r n; do [ -n "$n" ] || continue
  32. if in_code "$n"; then found=$((found+1)); else echo "missing in code: $n (consumed by $1)"; missing=1; fi
  33. done
  34. }
  35. # 1. the next plan: task-level Consumes plus its own Plan Set entry, minus what it produces
  36. name=$(basename "$next"); text=$(plan_text "$next")
  37. body=$(printf '%s\n' "$text" | awk '/^## Plan Set/{skip=1;next} /^## /{skip=0} !skip')
  38. own=$(printf '%s\n' "$text" | awk '/^## Plan Set/{f=1;next} /^## /{f=0} f' | grep -F -- "$name" || true)
  39. consumes=$( { printf '%s\n' "$body" | grep -i -A8 'Consumes'; printf '%s\n' "$own"; } | idents | grep -v -E "$stop" || true)
  40. produces=$( { printf '%s\n' "$body" | grep -i -A8 'Produces' | idents; printf '%s\n' "$body" | awk '/^```/{f=!f;next} f' | grep -o -E '^(func(tion)? (\([^)]*\) )?[A-Za-z_][A-Za-z0-9_]*|type [A-Za-z_][A-Za-z0-9_]*|class [A-Za-z_][A-Za-z0-9_]*|def [A-Za-z_][A-Za-z0-9_]*)' | awk '{print $NF}'; } | sort -u || true)
  41. check "$name" < <(comm -23 <(echo "$consumes") <(echo "$produces")) # no pipe: check must set missing/found in this shell
  42. # 2. every later plan in the same directory: names its Plan Set entry attributes to plans already complete
  43. parent=$(dirname "$next"); idx=0; k=0
  44. for p in $(ls -d "$parent"/* 2>/dev/null | sort); do k=$((k+1)); [ "${p%/}" = "$next" ] && idx=$k; done
  45. if [ "$idx" -gt 0 ]; then k=0
  46. for p in $(ls -d "$parent"/* 2>/dev/null | sort); do
  47. k=$((k+1)); [ "$k" -gt "$idx" ] || continue
  48. pn=$(basename "$p"); entry=$(plan_text "$p" | awk '/^## Plan Set/{f=1;next} /^## /{f=0} f' | grep -F -- "$pn" || true)
  49. [ -n "$entry" ] || continue
  50. # segments "from plan K: `a`, `b`" (or "Plan K's `a`") for K before the next plan
  51. names=$(printf '%s\n' "$entry" | grep -o -i -E "(from plans? [0-9]+[^\`]*|plan [0-9]+'s)[^;.]*" | while IFS= read -r seg; do
  52. kk=$(printf '%s' "$seg" | grep -o -E '[0-9]+' | head -1); [ -n "$kk" ] && [ "$kk" -lt "$idx" ] || continue
  53. printf '%s\n' "$seg" | idents | grep -v -E "$stop" || true
  54. done | sort -u)
  55. check "$pn (from plans before $name)" <<< "$names"
  56. done
  57. fi
  58. [ $missing -eq 0 ] && { echo "boundary: clean ($found consumed names found in code)"; exit 0; }
  59. exit 1