diff --git a/README.md b/README.md index 5c73ffa..f73510e 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/rules/mermaid.md b/docs/rules/mermaid.md new file mode 100644 index 0000000..a4ecf7e --- /dev/null +++ b/docs/rules/mermaid.md @@ -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. diff --git a/scripts/check-all.sh b/scripts/check-all.sh index 4fb4fdd..741a8a3 100755 --- a/scripts/check-all.sh +++ b/scripts/check-all.sh @@ -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 diff --git a/scripts/check-roadmap.sh b/scripts/check-roadmap.sh new file mode 100755 index 0000000..71df4d4 --- /dev/null +++ b/scripts/check-roadmap.sh @@ -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"