Files
puzzle-design-kb/scripts/check-dangling-nodes.sh

211 lines
5.3 KiB
Bash
Executable File

#!/bin/bash
# Checks a Mermaid flowchart for dangling nodes (orphans, dead-ends, undefined references)
usage() {
cat <<EOF
Usage: $0 <file.mmd>
Parses a Mermaid flowchart and reports:
- Orphan nodes (no incoming edges, except START)
- Dead-end nodes (no outgoing edges, except END)
- Undefined references (referenced in edges but never defined)
EOF
exit 1
}
[[ $# -eq 1 ]] || usage
FILE="$1"
if [[ ! -f "$FILE" ]]; then
echo "Error: File '$FILE' not found" >&2
exit 1
fi
awk '
function trim(s) {
gsub(/^[[:space:]]+|[[:space:]]+$/, "", s)
return s
}
# Extract bare node ID (strip any [label] or (label) suffix)
function node_id(s) {
# Find first [ or ( and take everything before it
if (match(s, /[[(]/)) {
return trim(substr(s, 1, RSTART-1))
}
return trim(s)
}
/^[[:space:]]*$/ { next }
/^[[:space:]]*%%/ { next }
/^[[:space:]]*(classDef|class)[[:space:]]/ { next }
/^[[:space:]]*::/ { next }
/^[[:space:]]*subgraph[[:space:]]/ { next }
/^[[:space:]]*end[[:space:]]*$/ { next }
/^[[:space:]]*flowchart[[:space:]]/ { next }
{
line = $0
gsub(/%.*$/, "", line)
# Find node definitions: ID with optional ( before [
while (match(line, /(^|[^A-Za-z_])[A-Za-z_][A-Za-z0-9_]*\(?\[/) > 0) {
match_start = RSTART
# Calculate where the ID actually starts
leading_char = ""
if (match_start > 1) {
leading_char = substr(line, match_start - 1, 1)
}
id_start = match_start + (leading_char != "" ? 1 : 0)
rest_of_line = substr(line, id_start)
bracket_pos_in_rest = index(rest_of_line, "[")
if (bracket_pos_in_rest == 0) break
bracket_pos = id_start + bracket_pos_in_rest - 1
node_id_str = substr(line, id_start, bracket_pos_in_rest - 1)
node_id_str = trim(node_id_str)
sub(/[(\[]+$/, "", node_id_str)
if (node_id_str != "") {
defined[node_id_str] = 1
# Extract label between [" and "]
label_rest = substr(line, bracket_pos + 1)
if (match(label_rest, /"([^"]+)"/) > 0) {
label[node_id_str] = substr(label_rest, RSTART + 1, RLENGTH - 2)
}
}
# Remove this node from line - find the closing ] and remove everything up to and including it
after_close = bracket_pos + index(substr(line, bracket_pos + 1), "]")
line = substr(line, after_close + 1)
}
}
(/-->|-\.->/) {
line = $0
gsub(/%.*$/, "", line)
arrow = ""
if (match(line, /-->/)) {
arrow = "-->"
} else if (match(line, /-\.->/)) {
arrow = "-.->"
} else {
next
}
n = split(line, parts, arrow)
if (n < 2) next
sources_str = parts[1]
targets_str = parts[2]
# Process sources
ns = split(sources_str, srcs, /&/)
for (i = 1; i <= ns; i++) {
src = node_id(srcs[i])
if (src == "") continue
outgoing[src]++
if (!(src in defined)) {
ref_line[src] = FNR
}
}
# Process targets
nt = split(targets_str, tgts, /&/)
for (i = 1; i <= nt; i++) {
tgt = node_id(tgts[i])
if (tgt == "") continue
incoming[tgt]++
if (!(tgt in defined)) {
ref_line[tgt] = FNR
}
}
}
END {
orphan_count = 0
for (node in defined) {
if (node == "START" || node == "END") continue
if (!(node in incoming)) {
orphans[++orphan_count] = node
}
}
deadend_count = 0
for (node in defined) {
if (node == "START" || node == "END") continue
if (!(node in outgoing)) {
deadends[++deadend_count] = node
}
}
undef_count = 0
for (node in ref_line) {
if (!(node in defined)) {
undefined[++undef_count] = node
}
}
print "=== DANGLING NODE REPORT ==="
print "File: " FILENAME
print ""
total = orphan_count + deadend_count + undef_count
if (total == 0) {
print "No issues found! Flowchart appears well-formed."
exit 0
}
print "ORPHAN NODES (no incoming edges):"
if (orphan_count == 0) {
print " (none)"
} else {
for (i = 1; i <= orphan_count; i++) {
node = orphans[i]
if (label[node] != "") {
printf " - %s: %s\n", node, label[node]
} else {
printf " - %s\n", node
}
}
}
print ""
print "DEAD-END NODES (no outgoing edges):"
if (deadend_count == 0) {
print " (none)"
} else {
for (i = 1; i <= deadend_count; i++) {
node = deadends[i]
if (label[node] != "") {
printf " - %s: %s\n", node, label[node]
} else {
printf " - %s\n", node
}
}
}
print ""
print "UNDEFINED REFERENCES:"
if (undef_count == 0) {
print " (none)"
} else {
for (i = 1; i <= undef_count; i++) {
node = undefined[i]
printf " - %s: referenced on line %d but never defined\n", node, ref_line[node]
}
}
print ""
print "TOTAL ISSUES: " total
exit (total > 0 ? 1 : 0)
}
' "$FILE"