#!/usr/bin/env tclsh # goals_followons.tcl - advisory follow-on disposition report over the goals archive # # The goals-system v4 Follow-ons contract (goals/AGENTS.md): archived detail files # record deferred items as structured column-0 lines in a '## Follow-ons' section - # Follow-on: => open # Follow-on: => goal G- # Follow-on: => landed YYYY-MM-DD () # Follow-on: => declined YYYY-MM-DD () # - and the disposition is flipped (the one sanctioned edit to an archived detail # file) in the same work unit that acts on the item. # # This tool answers "which follow-ons have not been acted on": # report (default) enumerate every structured Follow-on line in # goals/archive/G-*.md grouped by disposition, open items first; then # advisory flags: archived files carrying follow-on VOCABULARY # (follow-on/follow-up/loose end/parked; word-bounded, # case-insensitive) with no '## Follow-ons' section - the drift # signature of a file whose deferred items were never structured. # Malformed Follow-on lines are listed for visibility, but judging # them (as fatal findings) is goals_lint.tcl's job. # # Heuristic flags are advisory by design (vocabulary in historical prose is # legitimate); a flagged file is silenced by giving it a '## Follow-ons' section, # with real items or with a '(none - ...)' prose note. # # Plain tclsh (8.6+ or 9), no package dependencies - runnable from any checkout: # tclsh scriptlib/developer/goals_followons.tcl [report] [repo-root] # Always exit 0 when the scan ran (advisory); exit 2 on bad invocation/root. set args $argv if {[llength $args] && [lindex $args 0] eq "report"} { set args [lrange $args 1 end] } elseif {[llength $args] && ![file isdir [lindex $args 0]]} { puts stderr "goals_followons: unknown subcommand '[lindex $args 0]' (only: report)" exit 2 } set root [expr {[llength $args] ? [lindex $args 0] : [file dirname [file dirname [file dirname [file normalize [info script]]]]]}] if {![file isfile [file join $root GOALS.md]] || ![file isdir [file join $root goals archive]]} { puts stderr "goals_followons: '$root' does not look like the punkshell repo root (need GOALS.md and goals/archive/)" exit 2 } proc read_lines {path} { set f [open $path r] fconfigure $f -encoding utf-8 set lines [split [read $f] \n] close $f return $lines } set followon_re {^Follow-on: (.+) => (open|goal G-(\d+)|landed (\d{4}-\d{2}-\d{2})( \(.*\))?|declined (\d{4}-\d{2}-\d{2})( \(.*\))?)$} set vocab_re {(?i)\y(follow[- ]?ons?|follow[- ]?ups?|loose[- ]ends?|parked)\y} set open_items {} ;# {fid item} set goal_items {} ;# {fid gid item} set landed_items {} ;# {fid date item} set declined_items {} ;# {fid date item} set malformed {} ;# {relpath lineno} set unstructured {} ;# {relpath hits} set nfiles_with 0 foreach path [lsort [glob -nocomplain -directory [file join $root goals archive] G-*.md]] { set fname [file tail $path] set relpath goals/archive/$fname set fid "" regexp {^G-(\d+)-} $fname -> fid set lines [read_lines $path] set has_section 0 set file_has_lines 0 set i 0 foreach line $lines { incr i if {$line eq "## Follow-ons"} { set has_section 1 } if {![string match "Follow-on:*" $line]} continue if {![regexp $followon_re $line -> item dispo gid ldate lwhere ddate dwhy]} { lappend malformed [list $relpath $i] continue } set file_has_lines 1 if {$dispo eq "open"} { lappend open_items [list $fid $item] } elseif {$gid ne ""} { lappend goal_items [list $fid $gid $item] } elseif {$ldate ne ""} { lappend landed_items [list $fid $ldate $item] } else { lappend declined_items [list $fid $ddate $item] } } if {$file_has_lines} { incr nfiles_with } set hits [regexp -all $vocab_re [join $lines \n]] if {$hits > 0 && !$has_section} { lappend unstructured [list $relpath $hits] } } set ntotal [expr {[llength $open_items] + [llength $goal_items] + [llength $landed_items] + [llength $declined_items]}] puts "goals_followons report: $ntotal structured follow-on(s) in $nfiles_with archived file(s): [llength $open_items] open, [llength $goal_items] goal, [llength $landed_items] landed, [llength $declined_items] declined; [llength $unstructured] unstructured-vocabulary file(s)" if {[llength $open_items]} { puts "\nOPEN (not yet acted on):" foreach row [lsort -dictionary -index 0 $open_items] { lassign $row fid item puts " G-$fid $item" } } if {[llength $goal_items]} { puts "\nBECAME GOALS:" foreach row [lsort -dictionary -index 0 $goal_items] { lassign $row fid gid item puts " G-$fid -> G-$gid $item" } } if {[llength $landed_items]} { puts "\nLANDED (done without a goal):" foreach row [lsort -dictionary -index 0 $landed_items] { lassign $row fid date item puts " G-$fid $date $item" } } if {[llength $declined_items]} { puts "\nDECLINED:" foreach row [lsort -dictionary -index 0 $declined_items] { lassign $row fid date item puts " G-$fid $date $item" } } if {[llength $malformed]} { puts "\nMALFORMED Follow-on line(s) (goals_lint judges these):" foreach row $malformed { lassign $row relpath lineno puts " $relpath line $lineno" } } if {[llength $unstructured]} { puts "\nUNSTRUCTURED (follow-on vocabulary, no '## Follow-ons' section - goals/AGENTS.md Follow-ons):" foreach row [lsort -index 0 $unstructured] { lassign $row relpath hits puts " $relpath ($hits vocabulary hit(s))" } } exit 0