docs(roadmap): add Apple-palette mermaid roadmap and the roadmap gate

Follow the mihaela-agents mermaid rule: add a README roadmap using the TileDown
Apple-color status classes (done/review/epic/todo), legend block first, then a
libxml2-parity roadmap showing shipped vs open work. Add docs/rules/mermaid.md
and scripts/check-roadmap.sh (wired into check-all.sh) so the diagrams stay
mechanically checkable.
This commit is contained in:
Mihaela Mihaljevic 2026-06-08 00:58:50 +02:00
parent e4b2e0127d
commit 0f3086c8e6
4 changed files with 313 additions and 0 deletions

View File

@ -19,6 +19,56 @@ intentionally strict about portability:
It is a sibling project to [PureYAML](https://github.com/mihaelamj/PureYAML) and
follows the same structure, rules, and verification gates.
## Roadmap
The roadmap uses the TileDown Mermaid palette: green for shipped work, yellow for
review, purple for epic grouping, and gray for open work.
```mermaid
flowchart TB
classDef done fill:#ddf9e4,stroke:#34c759,color:#111827
classDef review fill:#fff7d6,stroke:#ffcc00,color:#111827
classDef epic fill:#f2e5ff,stroke:#af52de,color:#111827
classDef todo fill:#f2f4f7,stroke:#8e8e93,color:#111827
LDone["In main now"]:::done
LReview["PR in review"]:::review
LEpic["Epic grouping"]:::epic
LTodo["Open issue, no PR"]:::todo
LDone --> LReview
LReview --> LEpic
LEpic --> LTodo
```
Capability is being grown toward libxml2 parity in pure Swift, guided by the
private PureXML-research analysis. Shipped vs open work:
```mermaid
flowchart TB
classDef done fill:#ddf9e4,stroke:#34c759,color:#111827
classDef review fill:#fff7d6,stroke:#ffcc00,color:#111827
classDef epic fill:#f2e5ff,stroke:#af52de,color:#111827
classDef todo fill:#f2f4f7,stroke:#8e8e93,color:#111827
Model["Node model + emitter"]:::done
Stream["Streaming pull parser"]:::done
Escape["Iterative emitter + escaping"]:::done
Limits["Bounded-by-default limits"]:::done
Bytes["Byte input + encoding detection"]:::done
Namespaces["Namespace resolution"]:::todo
ByteStream["Streaming byte decode"]:::todo
DTD["Optional DTD + entities"]:::todo
Validation["Schema validation"]:::todo
XPath["XPath 1.0"]:::todo
Model --> Stream
Stream --> Escape
Escape --> Limits
Limits --> Bytes
Bytes --> Namespaces
Namespaces --> ByteStream
ByteStream --> DTD
DTD --> Validation
Validation --> XPath
```
## Status
The node model, the emitter, and a streaming parser are implemented and usable

51
docs/rules/mermaid.md Normal file
View File

@ -0,0 +1,51 @@
# Mermaid Rules
## Rule 1: Use the TileDown Mermaid palette
Every Mermaid roadmap diagram in this repository must use TileDown's Mermaid
status classes and exact colors.
```mermaid
flowchart TB
classDef done fill:#ddf9e4,stroke:#34c759,color:#111827
classDef review fill:#fff7d6,stroke:#ffcc00,color:#111827
classDef epic fill:#f2e5ff,stroke:#af52de,color:#111827
classDef todo fill:#f2f4f7,stroke:#8e8e93,color:#111827
Done["In main now"]:::done
Review["PR in review"]:::review
Epic["Epic grouping"]:::epic
Todo["Open issue, no PR"]:::todo
Done ~~~ Review
Review ~~~ Epic
Epic ~~~ Todo
```
Allowed classes:
- `done`: merged or shipped work
- `review`: pull request in review
- `epic`: grouping issue or roadmap parent
- `todo`: open work with no PR
Do not add ad-hoc Mermaid status colors. Add a new class only after TileDown's
Mermaid palette adds the same class.
## Rule 2: Keep roadmap Mermaid mechanically checkable
Roadmap Mermaid blocks must keep `classDef` lines before nodes and edges. Every
node must use one of the allowed classes, and every edge must reference nodes
already defined in that block.
The README must start its Mermaid section with the shared legend. Run
`bash scripts/check-roadmap.sh` after changing roadmap diagrams.
## Rule 3: Collapse completed epics
Completed epics must not be expanded into their child issues in Mermaid
roadmaps. Once every child in an epic is `done`, remove that detailed Mermaid
block and show the completed epic as one `done` node in its parent or
super-epic roadmap.
Do not keep all-green detailed roadmap diagrams. The issue checklist can retain
the historical child issue list, but Mermaid must stay focused on active or
upcoming work.

View File

@ -18,6 +18,7 @@ run_gate() {
run_gate "style" bash scripts/check-style.sh
run_gate "namespacing" bash scripts/check-namespacing.sh
run_gate "forbidden patterns" bash scripts/check-forbidden-patterns.sh
run_gate "roadmap" bash scripts/check-roadmap.sh
run_gate "swiftformat" swiftformat . --config .swiftformat --lint
run_gate "swiftlint" swiftlint --config .swiftlint.yml --strict
run_gate "swift build" swift build

211
scripts/check-roadmap.sh Executable file
View File

@ -0,0 +1,211 @@
#!/usr/bin/env bash
# Mermaid roadmap gate. Keeps README roadmap diagrams vertical, legend-first,
# TileDown-palette colored, and covered by live epic issues.
# See docs/rules/mermaid.md.
set -u
README="README.md"
FAIL=0
fail() {
echo "roadmap: $1" >&2
FAIL=1
}
expected_classes() {
cat <<'EOF'
classDef done fill:#ddf9e4,stroke:#34c759,color:#111827
classDef review fill:#fff7d6,stroke:#ffcc00,color:#111827
classDef epic fill:#f2e5ff,stroke:#af52de,color:#111827
classDef todo fill:#f2f4f7,stroke:#8e8e93,color:#111827
EOF
}
known_class() {
case "$1" in
done|review|epic|todo) return 0 ;;
*) return 1 ;;
esac
}
extract_blocks() {
awk -v dir="$1" '
/^```mermaid$/ {
inside = 1
count += 1
file = sprintf("%s/block-%03d.mmd", dir, count)
next
}
/^```$/ {
if (inside == 1) {
inside = 0
file = ""
}
next
}
inside == 1 {
print > file
}
END {
count_file = dir "/count"
print count + 0 > count_file
}
' "$README"
}
node_defined() {
grep -Fxq "$1" "$2"
}
validate_block() {
block="$1"
index="$2"
defined_nodes="$3"
expected="$4"
defs="$5"
grep '^ classDef ' "$block" > "$defs" || true
if ! diff -u "$expected" "$defs" >/dev/null; then
fail "mermaid block $index must use the TileDown Mermaid color classDefs"
fi
: > "$defined_nodes"
saw_non_class_def=0
while IFS= read -r line || [ -n "$line" ]; do
trimmed=$(printf '%s' "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
case "$trimmed" in
""|"flowchart "*|"%%"*) continue ;;
esac
case "$trimmed" in
classDef\ *)
if [ "$saw_non_class_def" -ne 0 ]; then
fail "mermaid block $index has classDef after nodes or edges"
fi
continue
;;
esac
saw_non_class_def=1
if [[ "$line" =~ ^[[:space:]]*([A-Za-z][A-Za-z0-9_]*)[[:space:]]*(\[.*\]|\(.*\)|\{.*\})[[:space:]]*:::[[:space:]]*([A-Za-z][A-Za-z0-9_]*)[[:space:]]*$ ]]; then
node="${BASH_REMATCH[1]}"
node_class="${BASH_REMATCH[3]}"
if ! known_class "$node_class"; then
fail "mermaid block $index uses unknown class $node_class"
fi
echo "$node" >> "$defined_nodes"
continue
fi
if [[ "$line" =~ (--\>|~~~) ]]; then
if [[ "$line" =~ ^[[:space:]]*([A-Za-z][A-Za-z0-9_]*)[[:space:]]*(--\>|~~~)[[:space:]]*([A-Za-z][A-Za-z0-9_]*)[[:space:]]*$ ]]; then
left="${BASH_REMATCH[1]}"
right="${BASH_REMATCH[3]}"
if ! node_defined "$left" "$defined_nodes" || ! node_defined "$right" "$defined_nodes"; then
fail "mermaid block $index has edge with undefined node: $left to $right"
fi
else
fail "mermaid block $index has unsupported edge syntax: $trimmed"
fi
continue
fi
if [[ "$line" =~ ^[[:space:]]*[A-Za-z][A-Za-z0-9_]*[[:space:]]*(\[.*\]|\(.*\)|\{.*\})[[:space:]]*$ ]]; then
fail "mermaid block $index has unclassed node: $trimmed"
fi
done < "$block"
if [ "$index" -gt 1 ] &&
grep -Fq ':::done' "$block" &&
! grep -Eq ':::(todo|review|epic)' "$block"; then
fail "mermaid block $index expands completed work; collapse completed epics to one done node"
fi
}
repo_name() {
if [ -n "${GITHUB_REPOSITORY:-}" ]; then
printf '%s\n' "$GITHUB_REPOSITORY"
return 0
fi
command -v gh >/dev/null 2>&1 || return 1
gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null
}
check_epic_coverage() {
repo=$(repo_name || true)
if [ -z "$repo" ] || ! command -v gh >/dev/null 2>&1; then
if [ -n "${GITHUB_ACTIONS:-}" ]; then
fail "gh is required in CI to verify epic coverage"
else
echo "roadmap: skipping GitHub epic coverage because gh is unavailable" >&2
fi
return
fi
numbers=$(gh issue list \
--repo "$repo" \
--label epic \
--state all \
--limit 1000 \
--json number \
--jq '.[].number' 2>/dev/null)
if [ $? -ne 0 ]; then
if [ -n "${GITHUB_ACTIONS:-}" ]; then
fail "failed to query epic issues"
else
echo "roadmap: skipping GitHub epic coverage because gh issue list failed" >&2
fi
return
fi
combined="$1"
for number in $numbers; do
if ! grep -Fq "#$number" "$combined"; then
fail "epic issue #$number is missing from README Mermaid diagrams"
fi
done
}
tmp=$(mktemp -d "${TMPDIR:-/tmp}/purexml-roadmap.XXXXXX") || exit 1
trap 'rm -rf "$tmp"' EXIT
extract_blocks "$tmp"
count=$(cat "$tmp/count")
if [ "$count" -eq 0 ]; then
fail "README has no Mermaid blocks"
exit "$FAIL"
fi
first="$tmp/block-001.mmd"
if ! grep -Fq 'LDone["In main now"]' "$first" || ! grep -Fq 'LTodo["Open issue, no PR"]' "$first"; then
fail "first Mermaid block must be the shared legend"
fi
expected="$tmp/expected-classes.txt"
expected_classes > "$expected"
combined="$tmp/all-blocks.mmd"
: > "$combined"
index=1
while [ "$index" -le "$count" ]; do
block=$(printf '%s/block-%03d.mmd' "$tmp" "$index")
cat "$block" >> "$combined"
validate_block \
"$block" \
"$index" \
"$tmp/defined-$index.txt" \
"$expected" \
"$tmp/defs-$index.txt"
index=$((index + 1))
done
check_epic_coverage "$combined"
if [ "$FAIL" -eq 0 ]; then
echo "roadmap: OK"
fi
exit "$FAIL"