You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

143 lines
5.7 KiB

#!/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: <item text> => open
# Follow-on: <item text> => goal G-<id>
# Follow-on: <item text> => landed YYYY-MM-DD (<where>)
# Follow-on: <item text> => declined YYYY-MM-DD (<why>)
# - 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