Skip to content

Commit 135d761

Browse files
malmsteinclaude
andcommitted
Add NativeInput architecture docs and automated diagram workflows
- docs/architecture/native-input/: five HTML diagram sources (module deps, data flow, plugin flow, current module graph, state management) - native-input-docs-watcher: gh-aw workflow that detects architectural changes to duckchat files on develop and posts an intelligent summary to Asana task 1214251791869701 - native-input-diagram-renderer: regular GHA workflow that re-renders HTML sources to PNG on merge, commits them back, and uploads fresh diagrams to the Asana task as attachments - .gitignore: unignore docs/architecture/ so diagram sources are tracked Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent eabedaa commit 135d761

9 files changed

Lines changed: 1961 additions & 1 deletion
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
name: Render NativeInput architecture diagrams
2+
3+
on:
4+
push:
5+
branches: [develop]
6+
paths:
7+
- 'docs/architecture/native-input/*.html'
8+
workflow_dispatch:
9+
10+
jobs:
11+
render-and-publish:
12+
runs-on: ubuntu-latest
13+
permissions:
14+
contents: write
15+
16+
steps:
17+
- name: Checkout
18+
uses: actions/checkout@v4
19+
with:
20+
token: ${{ secrets.GT_DAXMOBILE }}
21+
fetch-depth: 1
22+
23+
- name: Install ffmpeg
24+
run: sudo apt-get install -y --no-install-recommends ffmpeg
25+
26+
- name: Render diagrams
27+
run: |
28+
set -euo pipefail
29+
DIAGRAM_DIR="docs/architecture/native-input"
30+
31+
for html_file in "$DIAGRAM_DIR"/*.html; do
32+
name=$(basename "$html_file" .html)
33+
34+
# Read SVG viewport dimensions from the first <svg> tag
35+
svg_line=$(grep -m1 '<svg ' "$html_file")
36+
width=$(echo "$svg_line" | grep -oP 'width="\K[0-9]+')
37+
height=$(echo "$svg_line" | grep -oP 'height="\K[0-9]+')
38+
39+
if [[ -z "$width" || -z "$height" ]]; then
40+
echo "⚠ Could not parse dimensions for $name — skipping"
41+
continue
42+
fi
43+
44+
# Viewport = SVG size + 40px body padding on each side
45+
vw=$((width + 80))
46+
vh=$((height + 80))
47+
48+
echo "→ Rendering $name (SVG ${width}×${height}, viewport ${vw}×${vh})"
49+
50+
google-chrome-stable --headless --disable-gpu --no-sandbox \
51+
--screenshot="/tmp/raw_${name}.png" \
52+
--window-size="${vw},${vh}" \
53+
"file://${GITHUB_WORKSPACE}/${html_file}" 2>/dev/null
54+
55+
# Crop: full width, SVG height only, skip top padding (y=40)
56+
ffmpeg -y -i "/tmp/raw_${name}.png" \
57+
-vf "crop=${vw}:${height}:0:40" \
58+
-update 1 "${DIAGRAM_DIR}/${name}.png" \
59+
-loglevel error
60+
61+
echo " ✓ ${DIAGRAM_DIR}/${name}.png"
62+
done
63+
64+
- name: Commit rendered PNGs
65+
id: commit
66+
run: |
67+
git config user.email "github-actions[bot]@users.noreply.github.com"
68+
git config user.name "github-actions[bot]"
69+
git add docs/architecture/native-input/*.png
70+
71+
if git diff --staged --quiet; then
72+
echo "changed=false" >> "$GITHUB_OUTPUT"
73+
echo "No PNG changes to commit"
74+
else
75+
git commit -m "Update NativeInput architecture diagrams [skip ci]"
76+
git push
77+
echo "changed=true" >> "$GITHUB_OUTPUT"
78+
echo "sha=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
79+
fi
80+
81+
- name: Sync Asana task
82+
if: steps.commit.outputs.changed == 'true'
83+
env:
84+
ASANA_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }}
85+
TASK_GID: "1214251791869701"
86+
REPO: ${{ github.repository }}
87+
SHA: ${{ steps.commit.outputs.sha }}
88+
run: |
89+
set -euo pipefail
90+
DIAGRAM_DIR="docs/architecture/native-input"
91+
RAW_BASE="https://raw.githubusercontent.com/${REPO}/develop/${DIAGRAM_DIR}"
92+
DATE=$(date -u '+%Y-%m-%d')
93+
94+
# ── 1. Delete existing PNG attachments ─────────────────────────────
95+
echo "Fetching existing attachments…"
96+
existing=$(curl -sf \
97+
"https://app.asana.com/api/1.0/tasks/${TASK_GID}/attachments?opt_fields=gid,name" \
98+
-H "Authorization: Bearer $ASANA_TOKEN")
99+
100+
echo "$existing" | python3 - <<'PYEOF'
101+
import sys, json, os, subprocess
102+
data = json.loads(sys.stdin.read())["data"]
103+
token = os.environ["ASANA_TOKEN"]
104+
task = os.environ["TASK_GID"]
105+
deleted = 0
106+
for a in data:
107+
if a.get("name", "").endswith(".png"):
108+
r = subprocess.run(
109+
["curl", "-sf", "-X", "DELETE",
110+
f"https://app.asana.com/api/1.0/attachments/{a['gid']}",
111+
"-H", f"Authorization: Bearer {token}"],
112+
capture_output=True)
113+
if r.returncode == 0:
114+
print(f" Deleted {a['name']} ({a['gid']})")
115+
deleted += 1
116+
print(f"Removed {deleted} old diagram attachment(s)")
117+
PYEOF
118+
119+
# ── 2. Upload new PNGs ─────────────────────────────────────────────
120+
echo "Uploading new diagrams…"
121+
for png in "$DIAGRAM_DIR"/*.png; do
122+
label=$(basename "$png")
123+
curl -sf -X POST "https://app.asana.com/api/1.0/attachments" \
124+
-H "Authorization: Bearer $ASANA_TOKEN" \
125+
-F "parent=${TASK_GID}" \
126+
-F "file=@${png};type=image/png" \
127+
-F "name=${label}" > /dev/null
128+
echo " ✓ $label"
129+
done
130+
131+
# ── 3. Update task html_notes ──────────────────────────────────────
132+
# Diagram labels and file stems in display order
133+
declare -A LABELS=(
134+
["00-module-dependencies"]="Module Dependencies (before/after refactor)"
135+
["01-native-input-dataflow"]="Widget Data Flow"
136+
["02-plugin-dataflow"]="Plugin System (setup · tap · send)"
137+
["03-current-module-deps"]="Current Module Graph (develop)"
138+
["04-state-management"]="State Management (ViewModel → Widget)"
139+
)
140+
141+
diagram_links=""
142+
for stem in 00-module-dependencies 01-native-input-dataflow 02-plugin-dataflow 03-current-module-deps 04-state-management; do
143+
label="${LABELS[$stem]}"
144+
url="${RAW_BASE}/${stem}.png"
145+
diagram_links+="<li><a href=\"${url}\">${label}</a></li>"
146+
done
147+
148+
# Read the current task notes to preserve any existing content
149+
current_notes=$(curl -sf \
150+
"https://app.asana.com/api/1.0/tasks/${TASK_GID}?opt_fields=html_notes" \
151+
-H "Authorization: Bearer $ASANA_TOKEN" | \
152+
python3 -c "import sys,json; print(json.load(sys.stdin)['data'].get('html_notes',''))")
153+
154+
# Build the updated notes: replace the diagram section if it exists,
155+
# otherwise append it. We delimit our section with a known marker comment.
156+
OPEN_MARKER="<!-- diagrams-section -->"
157+
CLOSE_MARKER="<!-- /diagrams-section -->"
158+
159+
diagram_section="${OPEN_MARKER}<h2>Architecture Diagrams</h2><p>Last rendered: ${DATE} · commit <code>${SHA}</code> · see attachments below for full-resolution images.</p><ul>${diagram_links}</ul>${CLOSE_MARKER}"
160+
161+
# Use Python to splice/append the section
162+
updated_notes=$(python3 <<PYEOF2
163+
import re, sys
164+
165+
current = """${current_notes}"""
166+
section = """${diagram_section}"""
167+
open_m = "<!-- diagrams-section -->"
168+
close_m = "<!-- /diagrams-section -->"
169+
170+
if open_m in current and close_m in current:
171+
# Replace existing section
172+
pattern = re.escape(open_m) + ".*?" + re.escape(close_m)
173+
result = re.sub(pattern, section, current, flags=re.DOTALL)
174+
elif current.strip():
175+
# Append to existing non-empty notes
176+
result = current.rstrip("</body>").rstrip() + section + "</body>"
177+
else:
178+
result = "<body>" + section + "</body>"
179+
180+
print(result)
181+
PYEOF2
182+
)
183+
184+
payload=$(python3 -c "
185+
import json, sys
186+
notes = sys.stdin.read()
187+
print(json.dumps({'data': {'html_notes': notes}}))
188+
" <<< "$updated_notes")
189+
190+
curl -sf -X PUT \
191+
"https://app.asana.com/api/1.0/tasks/${TASK_GID}" \
192+
-H "Authorization: Bearer $ASANA_TOKEN" \
193+
-H "Content-Type: application/json" \
194+
-d "$payload" > /dev/null
195+
196+
echo "✓ Asana task updated"
197+
198+
# ── 4. Post summary comment ────────────────────────────────────────
199+
curl -sf -X POST \
200+
"https://app.asana.com/api/1.0/tasks/${TASK_GID}/stories" \
201+
-H "Authorization: Bearer $ASANA_TOKEN" \
202+
-H "Content-Type: application/json" \
203+
-d "{\"data\": {\"text\": \"📐 Architecture diagrams re-rendered (${DATE}, commit ${SHA}). Attachments and description updated.\"}}" \
204+
> /dev/null
205+
206+
echo "✓ Posted Asana comment"

0 commit comments

Comments
 (0)