Fix undefined reference: O_TRADE_COAL_FOR_EGG → A_TRADE_COAL_FOR_EGG
This commit is contained in:
210
scripts/check-dangling-nodes.sh
Executable file
210
scripts/check-dangling-nodes.sh
Executable file
@@ -0,0 +1,210 @@
|
|||||||
|
#!/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"
|
||||||
@@ -461,7 +461,7 @@ flowchart TD
|
|||||||
A_GET_COAL --> O_RECEIVE_COAL_IOM
|
A_GET_COAL --> O_RECEIVE_COAL_IOM
|
||||||
|
|
||||||
O_RECEIVE_COAL_IOM --> A_TRADE_COAL_FOR_EGG
|
O_RECEIVE_COAL_IOM --> A_TRADE_COAL_FOR_EGG
|
||||||
O_TRADE_COAL_FOR_EGG -.-> O_RECEIVE_SULFUR_EGG["O: Receive Sulfur Egg"]
|
A_TRADE_COAL_FOR_EGG -.-> O_RECEIVE_SULFUR_EGG["O: Receive Sulfur Egg"]
|
||||||
|
|
||||||
%% =============================================================================
|
%% =============================================================================
|
||||||
%% PHASE 9: Realm of the Dead
|
%% PHASE 9: Realm of the Dead
|
||||||
|
|||||||
Reference in New Issue
Block a user