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
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
|
|
|