返回 Skills
yizhiyanhua-ai/fireworks-tech-graph· MIT 内容可用

fireworks-tech-graph

>-

安装

与 skills.sh 相同的 Command / Prompt 安装方式


name: fireworks-tech-graph description: >- Create technical diagrams such as software architecture, data flow, flowcharts, sequence diagrams, C4 reviews, cloud deployments, event streams, observability investigations, agent/memory systems, UML, ER, network topology, timelines, and technical concept maps, then export SVG, PNG, focused semantic SVG-to-GIF motion, or offline interactive HTML. Treat direct requests such as "Generate a GIF", "生成 GIF", or "制作 GIF" as motion requests, and use this skill when the user asks to visualize a system or engineering concept. Do not use for photos, raster artwork, or quantitative data charts.

Fireworks Tech Graph

Generate geometry-checked SVG technical diagrams, high-resolution PNG, validated SVG-to-GIF semantic motion, and sanitized offline interactive HTML.

Runtime Compatibility

Use this repository unchanged in both Codex and Claude Code. It follows the Agent Skills layout: SKILL.md is the shared entry point, bundled resources use relative paths, and agents/openai.yaml adds optional Codex UI metadata without affecting Claude Code.

Before reading a reference or running a script, resolve the directory containing this SKILL.md as SKILL_ROOT. Do not assume the current working directory is the skill directory, and do not assume a variable set in one shell call persists into the next.

  • In Claude Code, use ${CLAUDE_SKILL_DIR}.
  • In Codex, use the absolute skill directory shown in the loaded skill metadata.

Every command block below sets SKILL_ROOT itself. In Codex, replace /absolute/path/from-codex-skill-metadata with the absolute skill directory before running the command.

Helper Scripts (Recommended)

The unified scripts/fireworks.py CLI and compatibility helpers provide stable rendering, geometry validation, inspection, animation, and export:

1. generate-diagram.sh - Validate SVG + export PNG

SKILL_ROOT="${CLAUDE_SKILL_DIR:-/absolute/path/from-codex-skill-metadata}"
"$SKILL_ROOT/scripts/generate-diagram.sh" -t architecture -s 1 -o ./output/arch.svg
  • Validates an existing SVG file
  • Exports PNG after validation
  • Example: "$SKILL_ROOT/scripts/generate-diagram.sh" -t architecture -s 1 -o ./output/arch.svg

2. generate-from-template.py - Create starter SVG from template

SKILL_ROOT="${CLAUDE_SKILL_DIR:-/absolute/path/from-codex-skill-metadata}"
mkdir -p ./output
python3 "$SKILL_ROOT/scripts/generate-from-template.py" architecture ./output/arch.svg '{"title":"My Diagram","nodes":[],"arrows":[]}'
  • Loads a built-in SVG template
  • Renders nodes, arrows, and legend entries from JSON input
  • Escapes text content to keep output XML-valid

3. validate-svg.sh - Validate SVG syntax

SKILL_ROOT="${CLAUDE_SKILL_DIR:-/absolute/path/from-codex-skill-metadata}"
"$SKILL_ROOT/scripts/validate-svg.sh" <svg-file>
  • Checks XML syntax
  • Verifies tag balance
  • Validates marker references
  • Checks attribute completeness
  • Validates path data

4. test-all-styles.sh - Batch test all styles

SKILL_ROOT="${CLAUDE_SKILL_DIR:-/absolute/path/from-codex-skill-metadata}"
"$SKILL_ROOT/scripts/test-all-styles.sh"
  • Tests multiple diagram sizes
  • Validates all generated SVGs
  • Generates test report

When to use scripts:

  • Use scripts when generating complex SVGs to avoid syntax errors
  • Scripts provide automatic validation and error reporting
  • Recommended for production diagrams

Workflow (Always Follow This Order)

  1. Classify the diagram type (see Diagram Types below)
  2. Extract structure — identify layers, nodes, edges, flows, and semantic groups from user description
  3. Plan layout — load $SKILL_ROOT/references/composition-quality-contract.md, then apply the diagram-type layout rules
  4. Load style reference — always load $SKILL_ROOT/references/style-1-flat-icon.md unless user specifies another; load the matching $SKILL_ROOT/references/style-N-*.md for exact color tokens and SVG patterns
  5. Select semantic contract — Styles 9–12 default to c4-review, cloud-fabric, event-transit, and ops-pulse; use scripts/fireworks.py validate before layout so missing or contradictory engineering facts fail closed
  6. Map nodes to shapes — use Shape Vocabulary below
  7. Check icon needs — load $SKILL_ROOT/references/icons.md for known products
  8. Write SVG with adaptive strategy (see SVG Generation Strategy below)
  9. Validate: Run "$SKILL_ROOT/scripts/validate-svg.sh" file.svg to check XML, markers, geometry, composition budgets, and renderability
  10. Export PNG: Use cairosvg (recommended). Load $SKILL_ROOT/references/png-export.md when choosing another renderer
  11. Animate on request让这张图动起来 / 生成 GIF / 制作 GIF / Animate this diagram / Generate a GIF means the latest generated semantic SVG to one GIF with auto, 5.75s, 20fps, and 960px width when that SVG satisfies one of the 12 approved motion contracts. Exact source bytes are not pinned, but role/stage/order coverage, route directions, required colors, and geometry fail closed; do not claim arbitrary same-style topologies are supported. Load $SKILL_ROOT/references/motion-effects.md, run fireworks.py animate, and report SVG/GIF/report paths. GIF is the only motion media format, while the default command also emits <output>.motion.json. Styles 1–12 are enabled, and their contracts plus the shared +2s-settled-flow timing revision are user-approved; the default keeps frames 38–109 at full opacity and resets on frames 110–114. Every scene begins connector-free and advances its moving primitives toward each target. The 75-vs-115 compatibility gate counts binary-exact frames first, decoded-RGBA-exact frames second, and permits a guarded antialias equivalent only when AE ≤ 128, normalized RMSE ≤ 0.001, every difference component is at most 2px wide or high, and all differences remain on edge or node borders; DOM and signature geometry stay strict-exact. Reject raster animation inputs and non-GIF motion outputs. Explicit 3.75s/75-frame and 2.75s/55-frame timelines remain supported
  12. Visual review gate — if your runtime can read images, load the exported PNG back and inspect it. Syntactic validity does not guarantee visual correctness: arrows may cross through component interiors, labels may collide with lifelines or other labels, boxes may overlap, alt-frame text may sit on top of a message, or a legend may cover content. If you see any of these, revise the SVG and re-export, with at most two focused correction passes. Common fixes:
    • Route arrows through gaps between boxes, not through box interiors
    • Move arrow labels 6-8px away from the arrow line (offset-first); add background rects only when offset is insufficient
    • Widen inter-row/inter-column gutters so same-layer arrows have clear corridors
    • Collapse repeated cross-layer arrows into a single "delegates down" rail outside the content area
    • Move legend/notes out of any region where arrows or labels land
    • Increase viewBox height/width rather than packing elements tighter
    • If a filtered element (drop-shadow, blur) is missing one side of its border, move it ≥30px away from that viewBox edge, or remove the filter and rely on color/contrast for visual separation Report visual_review: passed after inspection. If image reading is unavailable, report visual_review: skipped (image reader unavailable) — do not guess or claim visual correctness.

Rule Precedence

Use this order when instructions disagree:

  1. The user's explicit content and style request
  2. The selected $SKILL_ROOT/references/style-N-*.md visual tokens (palette, typography, corner radius, shadow treatment)
  3. Diagram-type layout rules and semantic flow requirements in this file
  4. Universal defaults and examples

Geometry and validation gates always remain active: style guidance cannot justify unreadable text, missing marker definitions, or arrows crossing component interiors. Tables in this file define semantic defaults; a selected style may override their colors and stroke treatment while preserving the meaning and direction of each flow.

Diagram Types & Layout Rules

Architecture Diagram

Nodes = services/components. Group into horizontal layers (top→bottom or left→right).

  • Typical layers: Client → Gateway/LB → Services → Data/Storage
  • Use <rect> dashed containers to group related services in the same layer
  • Arrow direction follows data/request flow
  • ViewBox: 0 0 960 600 standard, 0 0 960 800 for tall stacks

Data Flow Diagram

Emphasizes what data moves where. Focus on data transformation.

  • Label every arrow with the data type (e.g., "embeddings", "query", "context")
  • Use wider arrows (stroke-width: 2.5) for primary data paths
  • Dashed arrows for control/trigger flows
  • Color arrows by data category (not just Agent/RAG — use semantics)

Flowchart / Process Flow

Sequential decision/process steps.

  • Top-to-bottom preferred; left-to-right for wide flows
  • Diamond shapes for decisions, rounded rects for processes, parallelograms for I/O
  • Keep node labels short (≤3 words); put detail in sub-labels
  • Align nodes on a grid: x positions snap to 120px intervals, y to 80px

Agent Architecture Diagram

Shows how an AI agent reasons, uses tools, and manages memory. Key conceptual layers to always consider:

  • Input layer: User, query, trigger
  • Agent core: LLM, reasoning loop, planner
  • Memory layer: Short-term (context window), Long-term (vector/graph DB), Episodic
  • Tool layer: Tool calls, APIs, search, code execution
  • Output layer: Response, action, side-effects Use cyclic arrows (loop arcs) to show iterative reasoning. Separate memory types visually.

Memory Architecture Diagram (Mem0, MemGPT-style)

Specialized agent diagram focused on memory operations.

  • Show memory write path and read path separately (different arrow colors)
  • Memory tiers: Working Memory → Short-term → Long-term → External Store
  • Label memory operations: store(), retrieve(), forget(), consolidate()
  • Use stacked rects or layered cylinders for storage tiers

Sequence Diagram

Time-ordered message exchanges between participants.

  • Participants as vertical lifelines (top labels + vertical dashed lines)
  • Messages as horizontal arrows between lifelines, top-to-bottom time order
  • Activation boxes (thin filled rects on lifeline) show active processing
  • Group with <rect> loop/alt frames with label in top-left corner
  • ViewBox height = 80 + (num_messages × 50)

Comparison / Feature Matrix

Side-by-side comparison of approaches, systems, or components.

  • Column headers = systems, row headers = attributes
  • Row height: 40px; column width: min 120px; header row height: 50px
  • Checked cell: tinted background (e.g. #dcfce7) + checkmark; unsupported: #f9fafb fill
  • Alternating row fills (#f9fafb / #ffffff) for readability
  • Max readable columns: 5; beyond that, split into two diagrams

Timeline / Gantt

Horizontal time axis showing durations, phases, and milestones.

  • X-axis = time (weeks/months/quarters); Y-axis = items/tasks/phases
  • Bars: rounded rects, colored by category, labeled inside or beside
  • Milestone markers: diamond or filled circle at specific x position with label above
  • ViewBox: 0 0 960 400 typical; wider for many time periods: 0 0 1200 400

Mind Map / Concept Map

Radial layout from central concept.

  • Central node at cx=480, cy=280
  • First-level branches: evenly distributed around center (360/N degrees)
  • Second-level branches: branch off first-level at 30-45° offset
  • Use curved <path> with cubic bezier for branches, not straight lines

Class Diagram (UML)

Static structure showing classes, attributes, methods, and relationships.

  • Class box: 3-compartment rect (name / attributes / methods), min width 160px
    • Top compartment: class name, bold, centered (abstract = italic)
    • Middle: attributes with visibility (+ public, - private, # protected)
    • Bottom: method signatures, same visibility notation
  • Relationships:
    • Inheritance (extends): solid line + hollow triangle arrowhead, child → parent
    • Implementation (interface): dashed line + hollow triangle, class → interface
    • Association: solid line + open arrowhead, label with multiplicity (1, 0.., 1..)
    • Aggregation: solid line + hollow diamond on container side
    • Composition: solid line + filled diamond on container side
    • Dependency: dashed line + open arrowhead
  • Interface: <<interface>> stereotype above name, or circle/lollipop notation
  • Enum: compartment rect with <<enumeration>> stereotype, values in bottom
  • Layout: parent classes top, children below; interfaces to the left/right of implementors
  • ViewBox: 0 0 960 600 standard; 0 0 960 800 for deep hierarchies

Use Case Diagram (UML)

System functionality from user perspective.

  • Actor: stick figure (circle head + body line) placed outside system boundary
    • Label below figure, 13-14px
    • Primary actors on left, secondary/supporting on right
  • Use case: ellipse with label centered inside, min 140×60px
    • Keep names verb phrases: "Create Order", "Process Payment"
  • System boundary: large rect with dashed border + system name in top-left
  • Relationships:
    • Include: dashed arrow <<include>> from base to included use case
    • Extend: dashed arrow <<extend>> from extension to base use case
    • Generalization: solid line + hollow triangle (specialized → general)
  • Layout: system boundary centered, actors outside, use cases inside
  • ViewBox: 0 0 960 600 standard

State Machine Diagram (UML)

Lifecycle states and transitions of an entity.

  • State: rounded rect with state name, min 120×50px
    • Internal activities: small text entry/ action, exit/ action, do/ activity
    • Initial state: filled black circle (r=8), one outgoing arrow
    • Final state: filled circle (r=8) inside hollow circle (r=12)
    • Choice: small hollow diamond, guard labels on outgoing arrows [condition]
  • Transition: arrow with optional label event [guard] / action
    • Guard conditions in square brackets
    • Actions after /
  • Composite/nested state: larger rect containing sub-states, with name tab
  • Fork/join: thick horizontal or vertical black bar (synchronization)
  • Layout: initial state top-left, final state bottom-right, flow top-to-bottom
  • ViewBox: 0 0 960 600 standard

ER Diagram (Entity-Relationship)

Database schema and data relationships.

  • Entity: rect with entity name in header (bold), attributes below
    • Primary key attribute: underlined
    • Foreign key: italic or marked with (FK)
    • Min width: 160px; attribute font-size: 12px
  • Relationship: diamond shape on connecting line
    • Label inside diamond: "has", "belongs to", "enrolls in"
    • Cardinality labels near entity: 1, N, 0..1, 0..*, 1..*
  • Weak entity: double-bordered rect with double diamond relationship
  • Associative entity: diamond + rect hybrid (rect with diamond inside)
  • Line style: solid for identifying relationships, dashed for non-identifying
  • Layout: entities in 2-3 rows, relationships between related entities
  • ViewBox: 0 0 960 600 standard; wider 0 0 1200 600 for many entities

Network Topology

Physical or logical network infrastructure.

  • Devices: icon-like rects or rounded rects
    • Router: circle with cross arrows
    • Switch: rect with arrow grid
    • Server: stacked rect (rack icon)
    • Firewall: brick-pattern rect or shield shape
    • Load Balancer: horizontal split rect with arrows
    • Cloud: cloud path (overlapping arcs)
  • Connections: lines between device centers
    • Ethernet/wired: solid line, label bandwidth
    • Wireless: dashed line with WiFi symbol
    • VPN: dashed line with lock icon
  • Subnets/Zones: dashed rect containers with zone label (DMZ, Internal, External)
  • Labels: device hostname + IP below, 12-13px
  • Layout: tiered top-to-bottom (Internet → Edge → Core → Access → Endpoints)
  • ViewBox: 0 0 960 600 standard

UML Coverage Map

Full mapping of UML 14 diagram types to supported diagram types:

UML DiagramSupported AsNotes
ClassClass DiagramFull UML notation
ComponentArchitecture DiagramUse colored fills per component type
DeploymentArchitecture DiagramAdd node/instance labels
PackageArchitecture DiagramUse dashed grouping containers
Composite StructureArchitecture DiagramNested rects within components
ObjectClass DiagramInstance boxes with underlined name
Use CaseUse Case DiagramFull actor/ellipse/relationship
ActivityFlowchart / Process FlowAdd fork/join bars
State MachineState Machine DiagramFull UML notation
SequenceSequence DiagramAdd alt/opt/loop frames
CommunicationApproximate with Sequence (swap axes)
TimingTimelineAdapt time axis
Interaction OverviewFlowchartCombine activity + sequence fragments
ER DiagramER DiagramChen/Crow's foot notation

Shape Vocabulary

Map semantic concepts to consistent shapes across all diagram types:

ConceptShapeNotes
User / HumanCircle + body pathStick figure or avatar
LLM / ModelRounded rect with brain/spark icon or gradient fillUse accent color
Agent / OrchestratorHexagon or rounded rect with double borderSignals "active controller"
Memory (short-term)Rounded rect, dashed borderEphemeral = dashed
Memory (long-term)Cylinder (database shape)Persistent = solid cylinder
Vector StoreCylinder with grid lines insideAdd 3 horizontal lines
Graph DBCircle cluster (3 overlapping circles)
Tool / FunctionGear-like rect or rect with wrench icon
API / GatewayHexagon (single border)
Queue / StreamHorizontal tube (pipe shape)
File / DocumentFolded-corner rect
Browser / UIRect with 3-dot titlebar
DecisionDiamondFlowcharts only
Process / StepRounded rectStandard box
External ServiceRect with cloud icon or dashed border
Data / ArtifactParallelogramI/O in flowcharts

Arrow Semantics

Always assign arrow meaning, not just color. The values below are defaults; the selected style reference overrides colors and stroke weights while preserving flow semantics:

Flow TypeColorStrokeDashMeaning
Primary data flowblue #2563eb2px solidnoneMain request/response path
Control / triggerorange #ea580c1.5px solidnoneOne system triggering another
Memory readgreen #0596691.5px solidnoneRetrieval from store
Memory writegreen #0596691.5px5,3Write/store operation
Async / eventgray #6b72801.5px4,2Non-blocking, event-driven
Embedding / transformpurple #7c3aed1px solidnoneData transformation
Feedback / looppurple #7c3aed1.5px curvednoneIterative reasoning loop

Always include a legend when 2+ arrow types are used.

Layout Rules & Validation

Spacing:

  • Same-layer nodes: 80px horizontal, 120px vertical between layers
  • Canvas margins: 40px minimum, 60px between node edges
  • Snap to 8px grid: horizontal 120px intervals, vertical 120px intervals

Arrow Labels (CRITICAL):

  • Offset-first (default): place label 6-8px above horizontal arrows, or 8px left/right of vertical arrows — do not overlap the arrow line
  • Background fallback: add <rect fill="canvas_bg" opacity="0.95"/> only when the offset label still crosses another visual element (another arrow, a node edge, etc.)
  • Place mid-arrow, ≤3 words, stagger by 15-20px when multiple arrows converge
  • Maintain 10px safety distance from nodes

Arrow Routing:

  • Prefer orthogonal (L-shaped) paths to minimize crossings
  • Anchor arrows on component edges, not geometric centers
  • Route around dense node clusters, use different y-offsets for parallel arrows
  • Jump-over arcs (5px radius) for unavoidable crossings
  • Compress equivalent bidirectional traffic only when both directions share the same semantics and styling: use one corridor with marker-start + marker-end, or two visibly offset paths in that corridor
  • Keep read/write, request/response, sync/async, or differently labeled directions as separate arrows; remove redundant bends and duplicate rails without erasing direction or meaning

Post-Generation Arrow Optimization:

When a user asks to "优化箭头" / "fix arrow routing" / "optimize the diagram" on an already-generated diagram, preserve all nodes, containers, styles, and layout — only modify the arrows entries in the JSON data, then re-render with generate-from-template.py.

Available arrow override fields (in recommended order of use):

FieldTypeWhen to Use
source_port / target_port"left" / "right" / "top" / "bottom"Arrow exits/enters from the wrong edge
corridor_x[x, ...]Hint vertical segments toward this x lane (soft preference)
corridor_y[y, ...]Hint horizontal segments toward this y lane (soft preference)
route_points[[x1,y1], [x2,y2], ...]Exact ordered waypoints; each leg is routed orthogonally and unsafe points are rejected
routing_paddingnumber (default: 24)(Advanced) Adjust obstacle clearance for this arrow
port_clearancenumber(Advanced) Adjust first-segment offset from node edge
label_style"badge" / "offset"Choose "offset" when badge backgrounds create visual clutter; keep "badge" (default) for legacy/high-contrast labels

For JSON/template rendering, the default remains "badge" for backward compatibility. Set "label_style": "offset" on individual arrows when you want offset-first labels without background rects.

Optimization steps:

  1. Read the existing SVG — identify which arrows overlap, cross nodes, or look misaligned
  2. Find those arrows in the JSON data by source / target pair
  3. Add source_port / target_port if the exit/entry direction is wrong; add corridor_x / corridor_y to space parallel arrows apart; use route_points only when hints alone cannot resolve the path
  4. Re-run generate-from-template.py with the updated JSON and validate with validate-svg.sh

Example — spacing two overlapping arrows into separate corridors:

{ "source": "nodeA", "target": "nodeB", "corridor_y": [280] }
{ "source": "nodeC", "target": "nodeD", "corridor_y": [320] }

Line Overlap Prevention (CRITICAL - common in AI-generated diagrams): When two arrows must cross each other, ALWAYS use jump-over arcs to prevent visual overlap:

  • Crossing horizontal arrows: add a small semicircle arc (radius 5px, stroke same color as arrow, fill none) that "jumps over" the other line
  • SVG pattern for jump-over: use a white/matching-background arc on the lower layer, then draw the upper arc on top
  • Multiple crossings: stagger arc radii (5px, 7px, 9px) so arcs don't overlap each other
  • Never let two arrows' straight-line segments cross without a jump-over arc

Validation Checklist (run before finalizing):

  1. Arrow-Component Collision: Arrows MUST NOT pass through component interiors (route around with orthogonal paths)
  2. Text Overflow: All text MUST fit with 8px padding (estimate: text.length × 7px ≤ shape_width - 16px)
  3. Arrow-Text Alignment: Arrow endpoints MUST connect to shape edges (not floating); arrow labels should not overlap arrow lines (use offset positioning or background rects)
  4. Container Discipline: Prefer arrows entering and leaving section containers through open gaps between components, not through inner component bodies
  5. Filter Boundary Safety: For every element with filter="url(...)", verify (element_x + element_width + filter_extension) ≤ viewBox_width AND element_x ≥ filter_extension. The default filter region extends 10-20% beyond bbox; staying near viewBox edges causes Chrome/cairosvg to clip the element's edge-side stroke (one side of the border vanishes while other sides render correctly)
  6. Arrow-Title Collision: Arrows MUST NOT cross through section/container title text or region labels (font-size ≥ 13px). For smaller annotations (< 13px), prefer routing around but tolerate if layout constraints require it. (Visual self-review check — not covered by validate-svg.sh automated checks)
  7. Frame Label–Arrow Alignment (sequence diagrams): Section/frame label badges MUST be vertically centered with their first message arrow. Compute badge_y = first_arrow_y - (badge_height / 2). When appending new sections to an existing diagram, verify alignment matches the existing sections — this is the most common regression when adding content incrementally. Use variables in Python list generation to enforce the constraint: sec_y = 840; badge_y = sec_y - 9 # for height=18 badge
  8. Marker Integrity: Every marker-start, marker-mid, and marker-end URL MUST resolve to a <marker id="..."> definition
  9. Visual Review Status: Report whether the exported PNG was visually inspected; automated validation does not cover every text, legend, or arrow-arrow collision

SVG Technical Rules

  • ViewBox: 0 0 960 600 default; 0 0 960 800 tall; 0 0 1200 600 wide
  • Fonts: embed via <style>font-family: ...</style> — no external @import (cairosvg / rsvg-convert cannot fetch external URLs)
  • <defs>: arrow markers, gradients, filters, clip paths
  • Text: minimum 12px, prefer 13-14px labels, 11px sub-labels, 16-18px titles
  • All arrows: <marker> with markerEnd, sized markerWidth="10" markerHeight="7"
  • Drop shadows: <feDropShadow> in <filter>, apply sparingly (key nodes only)
  • Curved paths: use M x1,y1 C cx1,cy1 cx2,cy2 x2,y2 cubic bezier for loops/feedback arrows
  • Clip content: use <clipPath> if text might overflow a node box
  • Z-order (drawing order): SVG uses painter's model — later elements cover earlier ones. Recommended layer order (bottom → top): ① canvas background ② dashed containers / region backgrounds ③ arrows and connection lines ④ node shapes (rects, circles) ⑤ text labels and annotations ⑥ legends and overlays. When arrows pass near text, draw arrows BEFORE text so text stays readable. Adjust per diagram needs — this is guidance, not rigid.

SVG Generation & Error Prevention

MANDATORY: Python List Method (ALWAYS use this):

python3 << 'EOF'
lines = []
lines.append('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 700">')
lines.append('  <defs>')
# ... each line separately
lines.append('</svg>')

with open('/path/to/output.svg', 'w') as f:
    f.write('\n'.join(lines))
print("SVG generated successfully")
EOF

Why mandatory: Prevents character truncation, typos, and syntax errors. Each line is independent and easy to verify.

Pre-Tool-Call Checklist (CRITICAL - use EVERY time):

  1. ✅ Can I write out the COMPLETE command/content right now?
  2. ✅ Do I have ALL required parameters ready?
  3. ✅ Have I checked for syntax errors in my prepared content?

If ANY answer is NO: STOP. Do NOT call the tool. Prepare the content first.

Error Recovery Protocol:

  • First error: Analyze root cause, apply targeted fix
  • Second error: Switch method entirely (Python list → chunked generation)
  • Third error: STOP and report to user - do NOT loop endlessly
  • Never: Retry the same failing command or call tools with empty parameters

Validation (run after generation):

python3 -c "import xml.etree.ElementTree as ET; ET.parse('file.svg')" && echo "✓ Valid XML"
# Or use cairosvg as a render-time check:
python3 -c "import cairosvg; cairosvg.svg2png(url='file.svg', write_to='/tmp/test.png')" && echo "✓ Renders" && rm /tmp/test.png

If using generate-from-template.py:

  • Prefer source / target node ids in arrow JSON so the generator can snap to node edges
  • Keep x1,y1,x2,y2 as hints or fallback coordinates, not the main routing primitive
  • Let the generator choose orthogonal routes; avoid hardcoding center-to-center straight lines unless the path is guaranteed clear

Common Syntax Errors to Avoid:

  • yt-anchor → ✅ y="60" text-anchor="middle"
  • x="390 (missing y) → ✅ x="390" y="250"
  • fill=#fff → ✅ fill="#ffffff"
  • marker-end= → ✅ marker-end="url(#arrow)"
  • L 29450 → ✅ L 290,220
  • ❌ Missing </svg> at end
  • ❌ Element with filter near viewBox edge — filter region extends 20% (default) or more beyond bbox; if that region exceeds viewBox, Chrome/cairosvg clip the filter rendering AND can drop the element's own stroke on that side. Keep filtered elements at least max(20% of element size, shadow blur radius × 3) away from viewBox edges, or omit the filter.

Output

  • Default: ./[derived-name].svg and ./[derived-name].png in current directory
  • Custom: user specifies path with --output /path/ or 输出到 /path/
  • PNG / motion export: see SVG → PNG Conversion below and $SKILL_ROOT/references/motion-effects.md

SVG → PNG Conversion

Use $SKILL_ROOT/scripts/generate-diagram.sh by default. Load $SKILL_ROOT/references/png-export.md only when selecting a renderer manually, handling CJK/emoji fallback, converting browser-generated SVG, or using the bundled Puppeteer converter.

Styles

#NameBackgroundBest For
1Flat Icon (default)WhiteBlogs, docs, presentations
2Dark Terminal#0f0f1aGitHub, dev articles
3Blueprint#0a1628Architecture docs
4Notion CleanWhite, minimalNotion, Confluence, wikis
5GlassmorphismDark gradientProduct sites, keynotes
6Claude OfficialWarm cream #f8f6f3Anthropic-style diagrams
7OpenAI OfficialPure white #ffffffOpenAI-style diagrams
8Dark Luxury (AI-authored)#0a0a0a deep blackArchitecture docs, premium editorial — hand-craft SVG from $SKILL_ROOT/references/style-8-dark-luxury.md
9C4 Review CanvasWarm paper #f7f2e8C4 reviews and ADRs; enforces one abstraction level
10Cloud FabricCloud blue #edf5fbRegion/network/workload deployment ownership
11Event TransitTransit paper #fbf7eeTopics, processors, consumer groups, DLQ, state
12Ops PulseOps navy #07111fGolden signals, critical paths, correlated traces

Load the matching $SKILL_ROOT/references/style-N-*.md for exact color tokens and SVG patterns.

Style Selection

Default: Style 1 (Flat Icon) for most diagrams. Load $SKILL_ROOT/references/style-diagram-matrix.md for detailed style-to-diagram-type recommendations.

Prompt fingerprints: C4评审画布/C4 review board → 9; 多区域云部署/deployment topology → 10; 事件地铁图/event metro map → 11; 可靠性脉冲/golden signals trace → 12; 让这张图动起来/生成 GIF/制作 GIF/animate this diagram/Generate a GIF → auto motion. Auto-select these only with matching domain evidence; otherwise use Styles 1–7 or semantic_profile: "generic", and split mixed C4/deployment/event/ops views.

These patterns appear frequently — internalize them:

RAG Pipeline: Query → Embed → VectorSearch → Retrieve → Augment → LLM → Response Agentic RAG: adds Agent loop with Tool use between Query and LLM Agentic Search: Query → Planner → [Search Tool / Calculator / Code] → Synthesizer → Response Mem0 / Memory Layer: Input → Memory Manager → [Write: VectorDB + GraphDB] / [Read: Retrieve+Rank] → Context Agent Memory Types: Sensory (raw input) → Working (context window) → Episodic (past interactions) → Semantic (facts) → Procedural (skills) Multi-Agent: Orchestrator → [SubAgent A / SubAgent B / SubAgent C] → Aggregator → Output Tool Call Flow: LLM → Tool Selector → Tool Execution → Result Parser → LLM (loop)

附带文件

.github/FUNDING.yml
custom: ["https://paypal.me/yizhiyanhua"]
.github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
  workflow_dispatch:

permissions:
  contents: read

jobs:
  quality:
    strategy:
      fail-fast: false
      matrix:
        python: ["3.9", "3.12"]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
        with:
          persist-credentials: false
      - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
        with:
          python-version: ${{ matrix.python }}
      - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
        with:
          node-version: "20"
      - run: python3 -m unittest discover -s tests -v
      - run: bash -n scripts/*.sh tools/*.sh
      - run: node --check scripts/svg2png.js
      - run: node --check scripts/svg2gif.js
      - run: python3 -m py_compile scripts/*.py tools/*.py

  render-regression:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
        with:
          persist-credentials: false
      - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
        with:
          python-version: "3.12"
      - run: python3 -m pip install --disable-pip-version-check CairoSVG==2.7.1
      - run: TEST_OUTPUT_DIR="$RUNNER_TEMP/fireworks-tech-graph" ./scripts/test-all-styles.sh
      - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
        with:
          name: render-regression
          path: ${{ runner.temp }}/fireworks-tech-graph

  motion-smoke:
    runs-on: ubuntu-latest
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
        with:
          persist-credentials: false
      - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
        with:
          python-version: "3.12"
      - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
        with:
          node-version: "20"
      - run: sudo apt-get update && sudo apt-get install -y ffmpeg imagemagick
      - run: npm install --ignore-scripts --no-save --package-lock=false puppeteer-core@25.3.0
      - name: Validate installed SVG-to-GIF runtime for all 12 styles
        env:
          FIREWORKS_CHROME_NO_SANDBOX: "1"
          FIREWORKS_INSTALL_CANARY_MOTION: "1"
          FIREWORKS_INSTALL_CANARY_ALL_STYLES: "1"
          FIREWORKS_INSTALL_CANARY_NODE_MODULES: ${{ github.workspace }}/node_modules
        run: |
          ./tools/install-canary.sh "$PWD/skills/fireworks-tech-graph"

  distribution:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
        with:
          persist-credentials: false
      - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
        with:
          python-version: "3.12"
      - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
        with:
          node-version: "20"
      - run: python3 tools/distribution.py --check
      - run: npm pack --dry-run --json
      - run: ./tools/install-canary.sh "$PWD/skills/fireworks-tech-graph"

  docs-site:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
        with:
          persist-credentials: false
      - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
        with:
          python-version: "3.12"
      - run: python3 tools/check_project_consistency.py
.github/workflows/release.yml
name: Release

on:
  push:
    tags: ["v*"]

permissions:
  contents: write

jobs:
  release:
    runs-on: ubuntu-latest
    timeout-minutes: 25
    steps:
      - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
        with:
          persist-credentials: false
      - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
        with:
          python-version: "3.12"
      - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
        with:
          node-version: "20"
      - name: Verify tag matches package version
        run: |
          test "$GITHUB_REF_NAME" = "v$(node -p "require('./package.json').version")"
      - run: sudo apt-get update && sudo apt-get install -y ffmpeg imagemagick
      - run: python3 -m pip install --disable-pip-version-check CairoSVG==2.7.1
      - run: npm install --ignore-scripts --no-save --package-lock=false puppeteer-core@25.3.0
      - run: python3 -m unittest discover -s tests -v
      - run: TEST_OUTPUT_DIR="$RUNNER_TEMP/fireworks-tech-graph" ./scripts/test-all-styles.sh
      - run: python3 tools/check_project_consistency.py
      - run: python3 tools/distribution.py --check
      - name: Validate installed SVG-to-GIF runtime for all 12 styles
        env:
          FIREWORKS_CHROME_NO_SANDBOX: "1"
          FIREWORKS_INSTALL_CANARY_MOTION: "1"
          FIREWORKS_INSTALL_CANARY_ALL_STYLES: "1"
          FIREWORKS_INSTALL_CANARY_NODE_MODULES: ${{ github.workspace }}/node_modules
        run: ./tools/install-canary.sh "$PWD/skills/fireworks-tech-graph"
      - run: python3 tools/distribution.py --build-release --output-dir dist
      - name: Verify public tagged skill installation
        run: ./tools/install-canary.sh "yizhiyanhua-ai/fireworks-tech-graph/skills/fireworks-tech-graph#$GITHUB_REF_NAME"
      - name: Create GitHub release
        env:
          GH_TOKEN: ${{ github.token }}
        run: gh release create "$GITHUB_REF_NAME" dist/* --verify-tag --notes-file "docs/releases/$GITHUB_REF_NAME.md" --title "Fireworks Tech Graph $GITHUB_REF_NAME"
.npmignore
.DS_Store
node_modules/
test-output/
scripts/__pycache__/
**/__pycache__/**
**/*.pyc
*.py[cod]
*.backup
*.bak
*~
agentloop-core.png
agentloop-core.svg
SKILL.md.backup
README.md
[English](README.md) | [中文](README.zh.md)

[Release history](docs/releases/README.md) · [Changelog](CHANGELOG.md)

# fireworks-tech-graph

> **Stop drawing diagrams by hand.** Describe your system in English or Chinese — get geometry-safe SVG, PNG, focused SVG-to-GIF motion, and offline interactive technical diagrams.

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![GitHub Release](https://img.shields.io/github/v/release/yizhiyanhua-ai/fireworks-tech-graph)](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/releases)
[![Codex Skill](https://img.shields.io/badge/Codex-Skill-10a37f)](https://learn.chatgpt.com/docs/build-skills)
[![Claude Code Skill](https://img.shields.io/badge/Claude%20Code-Skill-d97757)](https://code.claude.com/docs/en/skills)
[![12 Visual Styles](https://img.shields.io/badge/Styles-12-purple)]()
[![14 Diagram Types](https://img.shields.io/badge/Diagram%20Types-14-green)]()
[![UML Support](https://img.shields.io/badge/UML-Full%20Support-orange)]()

---

## Overview

`fireworks-tech-graph` is one Agent Skill that works unchanged in **Codex and Claude Code**. It turns natural language descriptions into polished, geometry-checked SVG diagrams, high-resolution PNGs, validated SVG-to-GIF semantic motion, and offline interactive HTML. The focused animation path accepts a generated semantic SVG and emits one compact, probed GIF. It ships with **11 generator-backed styles** and **1 AI-authored style (Dark Luxury)**. Four engineering-first styles add executable contracts for C4 reviews, cloud deployments, event streams, and reliability investigations, alongside deep AI/Agent domain patterns and all 14 UML diagram types.

```
User: "Generate a Mem0 memory architecture diagram, dark style"
  → Skill classifies: Memory Architecture Diagram, Style 2
  → Generates SVG with swim lanes, cylinders, semantic arrows
  → Exports 1920px PNG
  → Reports: mem0-architecture.svg / mem0-architecture.png
```

---

## Work With the Builder

This project is also a proof surface for a broader capability: turning vague AI/devtool workflows into constrained, reusable systems with validation, documentation, export paths, and product-facing polish.

If you are building agent infrastructure, AI IDEs, internal copilots, developer tools, technical documentation systems, or applied AI workflow products, I am open to scoped paid sprints, design-partner work, and founding engineer conversations.

- Founder-facing profile: https://bradzhang.dev/en
- Commercial case study: https://bradzhang.dev/en/case-studies/fireworks-tech-graph
- Work with me: https://bradzhang.dev/en/work-with-me

---

## Showcase

> The animated previews use the user-approved 5.75-second settled-flow timeline: routes draw in first, then the final topology keeps live data moving for two additional seconds. Each full-size GIF is 960px wide at 20fps / 115 frames; the 3×4 overview is an optimized 1200px preview. Lossless 1920px PNGs remain in `assets/samples/` as static regression baselines.

![Animated 12-style showcase — one distinct engineering scenario per style](assets/samples/showcase-12-styles.gif)

The v1.2.0 overview above and every full-size animated sample below come from the approved regression set. Each style keeps a distinct scenario while sharing the same geometry, text-fit, wire-routing, and semantic-motion quality gates.

### Style 1 — Flat Icon (default)
*Mem0 Memory Architecture — personal-memory extraction, conflict resolution, storage, and retrieval*
![Style 1 — Flat Icon](assets/samples/sample-style1-flat.gif)

### Style 2 — Dark Terminal
*Tool Call Flow — dark terminal execution, source grounding, retrieval, and answer synthesis*
![Style 2 — Dark Terminal](assets/samples/sample-style2-dark.gif)

### Style 3 — Blueprint
*Microservices Architecture — engineering grid, domain services, data stores, events, and telemetry*
![Style 3 — Blueprint](assets/samples/sample-style3-blueprint.gif)

### Style 4 — Notion Clean
*Agent Memory Types — minimal hierarchy from sensory and working context to durable memory*
![Style 4 — Notion Clean](assets/samples/sample-style4-notion.gif)

### Style 5 — Glassmorphism
*Multi-Agent Collaboration — coordinator, specialists, shared state, review, and synthesis*
![Style 5 — Glassmorphism](assets/samples/sample-style5-glass.gif)

### Style 6 — Claude Official
*System Architecture — warm interface, runtime, safety, memory, tools, and operations layers*
![Style 6 — Claude Official](assets/samples/sample-style6-claude.gif)

### Style 7 — OpenAI Official
*API Integration Flow — clean SDK, prompt, model, tool, delivery, and release stages*
![Style 7 — OpenAI Official](assets/samples/sample-style7-openai.gif)

### Style 8 — Dark Luxury *(AI-authored)*
*Agent Runtime Architecture — control plane, execution and state layers, champagne-gold structure, semantic color buckets*
![Style 8 — Dark Luxury](assets/samples/sample-style8-dark-luxury.gif)

### Style 9 — C4 Review Canvas
*Checkout Container Review — one abstraction level, explicit responsibilities, technologies, and protocols*
![Style 9 — C4 Review Canvas](assets/samples/sample-style9-c4-review-canvas.gif)

### Style 10 — Cloud Fabric
*Active–Active Checkout Deployment — global ingress, regions, VPC ownership, and cross-region replication*
![Style 10 — Cloud Fabric](assets/samples/sample-style10-cloud-fabric.gif)

### Style 11 — Event Transit
*Checkout Event Line — topics as rails, processors as stations, a declared junction, DLQ, and state projection*
![Style 11 — Event Transit](assets/samples/sample-style11-event-transit.gif)

### Style 12 — Ops Pulse
*Checkout Reliability Pulse — golden signals, one critical path, OTel export, and a correlated trace*
![Style 12 — Ops Pulse](assets/samples/sample-style12-ops-pulse.gif)

---

## Stable Prompt Recipe

The public showcase keeps a distinct domain scene for every style. They remain comparable because every fixture passes the same executable composition contract. A same-topology regression set remains internal under `fixtures/quality-baseline/`.

```text
Draw the scenario assigned to style N:
1 Mem0 Memory Architecture; 2 Tool Call Flow; 3 Microservices Architecture;
4 Agent Memory Types; 5 Multi-Agent Collaboration; 6 System Architecture;
7 API Integration Flow; 8 Agent Runtime Architecture; 9 C4 Checkout Review;
10 Active–Active Cloud Deployment; 11 Checkout Event Line; 12 Checkout Reliability Pulse.
Preserve the scenario-specific nodes, sections, and reading direction.
Apply the showcase composition contract: zero crossings, zero bridge jumps, at most two bends per edge,
at most eight bends overall, at least 40px between nodes, at least 20px container gutter,
short orthogonal segments, and labels kept clear of nodes, routes, and section headers.
Preserve the selected style's typography, palette, card material, and brand details.
```

For the four engineering-first styles, use one of these prompt fingerprints so
the router selects the domain contract as well as the visual theme:

```text
Style 9 · C4 review board: show one C4 level, responsibilities, technologies, review state, and relationship protocols.
Style 10 · Multi-region deployment map: show global ingress, Region/VPC ownership, neutral cloud glyphs, deployment mode, and named boundary mechanisms.
Style 11 · Event metro map: show thin topic rails, numbered processor stations, declared junctions, consumer groups, DLQ, and state projections.
Style 12 · Reliability pulse: show one observation window, four golden signals per service, numbered critical hops, telemetry export, and one correlated trace.
```

Replace `N` with `1`–`12`. Style 8 remains AI-authored and loads `references/style-8-dark-luxury.md`; Styles 9–12 also enforce their engineering semantic contract. All styles load `references/composition-quality-contract.md`.

---

## Features

- **12 visual styles** — 11 generator-backed profiles + 1 AI-authored style (Dark Luxury)
- **Engineering semantic contracts** — C4 abstraction levels, deployment ownership, event-rail topology, and exact golden signals fail closed before rendering
- **Executable style system** — style guides are encoded into the generator, not only documented in markdown
- **Shared composition-quality contract** — every official style enforces zero crossings/bridges, ≤2 bends per edge, route-stretch, spacing, gutter, micro-segment, and label-clearance budgets
- **14 diagram types** — Full UML support (Class, Component, Deployment, Package, Composite Structure, Object, Use Case, Activity, State Machine, Sequence, Communication, Timing, Interaction Overview, ER Diagram) plus AI/Agent domain diagrams
- **AI/Agent domain patterns** — RAG, Agentic Search, Mem0, Multi-Agent, Tool Call, and more built-in
- **Semantic shape vocabulary** — LLM = double-border rect, Agent = hexagon, Vector Store = ringed cylinder
- **Semantic arrow system** — color + dash pattern encode meaning (write vs read vs async vs loop)
- **Geometry-safe routing** — deterministic orthogonal routes, exact waypoints, distinct ports, automatic legend relocation, labels kept inside the canvas, and verified bridge jumps for unavoidable crossings
- **Versioned diagram IR** — legacy JSON normalizes to schema v1; duplicate IDs, dangling references, malformed waypoints, and non-finite geometry fail before rendering
- **Structured SVG validation** — XML and marker integrity plus semantic node, reserved-region, label, canvas, edge-overlap, and edge-crossing checks
- **Unified CLI + interactive export** — render, validate, inspect, and export one offline HTML file with pan/zoom, themes, copy, and SVG/PNG/JPEG/WebP output up to 4×
- **Focused semantic GIF motion** — generated SVG in, validated GIF out; connectors begin absent and draw in semantic order. All twelve style contracts are user-approved. The shared `+2s-settled-flow` timing revision is also user-approved, so the default 5.75s/115-frame loop holds full settled flow on frames 38–109, then resets on 110–114
- **Visual review gate** — exported PNGs are inspected for clipping, overlap, label placement, and routing regressions before delivery
- **Product icons** — 40+ products with brand colors: OpenAI, Anthropic, Pinecone, Weaviate, Kafka, PostgreSQL…
- **Swim lane grouping** — automatic layer labeling for complex architectures
- **SVG + PNG output** — SVG for editing, 1920px PNG for embedding
- **Renderer-friendly** — pure inline SVG, no external font fetching; renders cleanly in cairosvg, rsvg-convert, and headless Chrome

---

## Loop Engineering

The first render is treated as a candidate, not an automatic final result. `fireworks-tech-graph` uses an agent-driven, bounded validation feedback loop to move each diagram toward a verified deliverable:

```text
Prompt
  → Diagram Contract
  → Semantic IR
  → Style Spec
  → Route Planner
  → SVG Build
  → Structural Validation
  → PNG Visual Readback
  → Targeted Revision
  → Verified SVG + PNG
```

The loop follows five design principles:

1. **Evaluate, don't assert** — completion is backed by validator and render evidence, not by the model saying the diagram looks correct.
2. **Deterministic checks first** — XML structure, marker integrity, path geometry, arrow-component collisions, and renderability are checked before visual judgment.
3. **Perceptual validation second** — the exported PNG is read back to inspect clipping, label collisions, hierarchy, whitespace, and routing quality that syntax checks cannot see.
4. **Targeted correction** — each pass changes only the diagnosed labels, coordinates, corridors, or spacing, then reruns validation and rendering.
5. **Bounded convergence** — visual review allows at most two focused correction passes by default, preventing an unbounded self-editing loop.

The loop is observable in the final status:

```text
validation: passed
visual_review: passed
```

If the runtime cannot read images, the skill reports `visual_review: skipped (image reader unavailable)` explicitly. The workflow remains bounded and auditable; it does not claim visual verification without image evidence.

---

## Installation

### Recommended: install the complete skill for both runtimes

Use the real nested skill path. The final `/skills/fireworks-tech-graph` segment is required because a bare repository install can select only the root `SKILL.md` in current versions of `skills` CLI.

```bash
npx -y skills@1.5.17 add \
  yizhiyanhua-ai/fireworks-tech-graph/skills/fireworks-tech-graph \
  --agent codex claude-code -g -y --copy
```

This creates complete copies at `~/.agents/skills/fireworks-tech-graph` for Codex and `~/.claude/skills/fireworks-tech-graph` for Claude Code, including scripts, schemas, fixtures, templates, tests, references, and metadata.

### Editable Git checkout for Codex

```bash
mkdir -p ~/.agents/skills
git clone https://github.com/yizhiyanhua-ai/fireworks-tech-graph.git ~/.agents/skills/fireworks-tech-graph
```

Codex discovers personal skills from `~/.agents/skills` and reads the optional `agents/openai.yaml` metadata included in this repository.

### Editable Git checkout for Claude Code

```bash
mkdir -p ~/.claude/skills
git clone https://github.com/yizhiyanhua-ai/fireworks-tech-graph.git ~/.claude/skills/fireworks-tech-graph
```

Claude Code discovers personal skills from `~/.claude/skills` and ignores the Codex-only UI metadata.

### One editable checkout shared by Codex and Claude Code

For a fresh install with Claude Code 2.1.203 or newer, keep one checkout and link both discovery paths to it. Move any existing destinations aside before creating the links.

```bash
mkdir -p ~/.local/share/agent-skills ~/.agents/skills ~/.claude/skills
git clone https://github.com/yizhiyanhua-ai/fireworks-tech-graph.git ~/.local/share/agent-skills/fireworks-tech-graph
ln -s ~/.local/share/agent-skills/fireworks-tech-graph ~/.agents/skills/fireworks-tech-graph
ln -s ~/.local/share/agent-skills/fireworks-tech-graph ~/.claude/skills/fireworks-tech-graph
```

This keeps `SKILL.md`, references, scripts, templates, and future updates identical in both agents. The npm registry is a separate distribution channel and may lag GitHub Releases. For the current Skill version, use the nested GitHub path above; the npm page remains available for package metadata:

```text
https://www.npmjs.com/package/@yizhiyanhua-ai/fireworks-tech-graph
```

## Update

For a `skills` CLI copy, rerun the recommended nested-path command. For Git installations, update whichever checkout you installed:

```bash
git -C ~/.agents/skills/fireworks-tech-graph pull
# or
git -C ~/.claude/skills/fireworks-tech-graph pull
# or, for the shared checkout
git -C ~/.local/share/agent-skills/fireworks-tech-graph pull
```

After the first install, restart Codex and Claude Code so both discover the skill. Later `SKILL.md` edits are detected automatically; restart the runtime after changing bundled scripts or references if the update is not visible.

The shell commands above target macOS, Linux, WSL, and Git Bash. On native Windows, use the equivalent `%USERPROFILE%\.agents\skills` and `%USERPROFILE%\.claude\skills` paths. Python 3.9+ is required; the optional Puppeteer path requires Node.js 18+.

---

## Unified CLI

```bash
SKILL_ROOT="${CLAUDE_SKILL_DIR:-$HOME/.agents/skills/fireworks-tech-graph}"

python3 "$SKILL_ROOT/scripts/fireworks.py" doctor
python3 "$SKILL_ROOT/scripts/fireworks.py" validate architecture "$SKILL_ROOT/fixtures/api-flow-style7.json"
python3 "$SKILL_ROOT/scripts/fireworks.py" render architecture "$SKILL_ROOT/fixtures/api-flow-style7.json" diagram.svg --report layout.json
python3 "$SKILL_ROOT/scripts/fireworks.py" check diagram.svg
python3 "$SKILL_ROOT/scripts/fireworks.py" export-html diagram.svg diagram.html --title "Agent Runtime Architecture"
python3 "$SKILL_ROOT/scripts/fireworks.py" animate diagram.svg diagram.gif
```

The HTML export is one offline file. It sanitizes the SVG, adds pan/zoom/reset, light and dark themes, SVG source copy, and SVG/PNG/JPEG/WebP downloads at 1×–4×.

For motion, say **“Generate a GIF”**, **“Animate this diagram”**, `生成 GIF`, `制作 GIF`, or `让这张图动起来`. The command accepts a generated semantic SVG that carries one of the twelve approved motion contracts. Exact source bytes are not pinned, so validated title and content variants of a supported topology work; missing or changed role/stage/order coverage, route direction, required colors, or geometry fails closed. GIF is the only motion media format, and the default command also writes `<output>.motion.json` as its verification report. The approved default is 960px, 5.75 seconds, 20fps, and 115 frame-center samples. All twelve scenes start connector-free, draw routes on frames 1–36, fade live flow on 36–38, hold full settled flow on 38–109, and reset on 110–114. Their approved identities include the packet heads, terminal evidence trace, Blueprint registration beads, 14×10 Notion memory cards, and the eight scene-specific signatures listed below. Default packages report both the style contract and shared `+2s-settled-flow` timing revision as `user-approved`. Timelines of 75 frames or fewer remain all-unique. Longer timelines allow non-adjacent repeated rasters inside the full-opacity interval; frame 110 is the sole boundary exception because its unchanged reset opacity is exactly 1.00, and such evidence is classified as `intentional_reset_boundary_repeat`. Frames 111–114 remain globally distinct. Long timelines require at least 75 unique rasters and forbid adjacent duplicates. The all-style 75-vs-115 gate counts binary-exact and decoded-RGBA-exact frames separately; compositor-only fallback is accepted only at AE ≤ 128, normalized RMSE ≤ 0.001, with components no thicker than 2px and confined to edge or node borders. DOM and signature geometry remain strict-exact. Explicit 3.75s/75-frame and 2.75s/55-frame calls remain supported. See [Focused SVG-to-GIF Motion](references/motion-effects.md).

| Style | Preset | Live signature |
|---:|---|---|
| 5 | `agent-orchestration` | glass task capsule + coordinator halo |
| 6 | `governed-runtime` | governance thread + policy seal |
| 7 | `token-stream` | API rail + three-cell token train |
| 8 | `golden-circuit` | luxury circuit rail + gem tracer |
| 9 | `review-trace` | review rail + moving review cursor |
| 10 | `cloud-flow` | region chevrons + replication capsule |
| 11 | `event-transit` | event train + exception/projection cars |
| 12 | `ops-pulse` | ECG/export heads + trace reveal + waterfall scanner |

---

## Requirements

The bundled SVG/PNG scripts require **cairosvg** (recommended) or `rsvg-convert`. Optional SVG-to-GIF export requires FFmpeg/FFprobe, Chrome/Chromium, and `puppeteer` or `puppeteer-core`.

```bash
# Recommended: cairosvg (best CSS support)
python3 -m pip install cairosvg

# Fallback: rsvg-convert (system package; may drop CSS / <foreignObject>)
brew install librsvg                   # macOS
sudo apt install librsvg2-bin          # Ubuntu/Debian

# Optional semantic motion export. Install beside every copied Skill because the
# renderer intentionally does not load modules from the caller's directory.
brew install ffmpeg                    # macOS; use your system package manager elsewhere
for SKILL_ROOT in \
  "$HOME/.agents/skills/fireworks-tech-graph" \
  "$HOME/.claude/skills/fireworks-tech-graph"
do
  [ -d "$SKILL_ROOT" ] || continue
  npm install --prefix "$SKILL_ROOT" --ignore-scripts --no-save --package-lock=false puppeteer-core@25.3.0
  python3 "$SKILL_ROOT/scripts/fireworks.py" doctor
done

# Verify either supported script renderer
python3 -c "import cairosvg; print(cairosvg.__version__)"
rsvg-convert --version
```

| Renderer | Quality | Install Cost | Use When |
|----------|---------|--------------|----------|
| **cairosvg** | ✅ Good | Single `python3 -m pip install` | Default — best balance |
| rsvg-convert | ⚠️ Fair | System package | No Python available, simple flat diagrams |
| puppeteer | ✅✅ Best | Node + Chromium | Manual browser-rendering path for D3, Mermaid, or pixel-perfect output |

---

## Why Not Mermaid or draw.io?

| | Mermaid | draw.io | **fireworks-tech-graph** |
|--|---------|---------|--------------------------|
| Natural language input | ✗ | ✗ | ✅ |
| AI/Agent domain patterns | ✗ | ✗ | ✅ |
| Multiple visual styles | ✗ | manual | ✅ 12 built-in |
| High-res PNG export | ✗ | manual | ✅ auto 1920px |
| Semantic arrow colors | ✗ | manual | ✅ auto |
| No online tool needed | ✅ | ✗ | ✅ |

Mermaid is great for quick inline diagrams in markdown. draw.io is great for manual polishing. `fireworks-tech-graph` is optimized for **describing a system and getting a polished diagram immediately**, without writing DSL syntax or clicking around a GUI.

---

## Usage

### Trigger phrases

The skill auto-triggers on:

```
generate diagram / draw diagram / create chart / visualize
architecture diagram / flowchart / sequence diagram / data flow
Generate a GIF / animate this diagram / animate this SVG as a GIF / 生成 GIF / 制作 GIF / 让这张图动起来 / 把刚才的 SVG 转成 GIF
```

### Basic usage

```
Draw a RAG pipeline flowchart
```

```
Generate an Agentic Search architecture diagram
```

### Specify style

```
Draw a microservices architecture diagram, style 2 (dark terminal)
```

```
Draw a multi-agent collaboration diagram --style glassmorphism
```

### Specify output path

```
Generate a Mem0 architecture diagram, output to ~/Desktop/
```

```
Create a tool call flow diagram --output /tmp/diagrams/
```

---

## Example Prompts by Scenario

### AI/Agent Systems

```
Compare Agentic RAG vs standard RAG in a feature matrix, Notion clean style
```
→ Comparison matrix: RAG vs Agentic RAG, covering retrieval strategy, agent loop, tool use

```
Generate a Mem0 memory architecture diagram with vector store, graph DB, KV store, and memory manager
```
→ Memory Architecture with swim lanes: Input → Memory Manager → Storage tiers → Retrieval

```
Draw a Multi-Agent diagram: Orchestrator dispatches 3 SubAgents (search / compute / code execution), results aggregated
```
→ Agent Architecture with hexagons, tool layers, and result aggregation

```
Visualize the Tool Call execution flow: LLM → Tool Selector → Execution → Parser → back to LLM
```
→ Flowchart with decision loop showing tool invocation cycle

```
Draw the 5 agent memory types: Sensory, Working, Episodic, Semantic, Procedural
```
→ Mind map or layered architecture showing memory tiers from sensory to procedural

### Infrastructure & Cloud

```
Draw a microservices architecture: Client → API Gateway → [User Service / Order Service / Payment Service] → PostgreSQL + Redis
```
→ Architecture diagram with horizontal layers, swim lanes per service cluster

```
Generate a data pipeline diagram: Kafka → Spark processing → write to S3 → Athena query
```
→ Data flow diagram with labeled arrows (stream / batch / query)

```
Draw a Kubernetes deployment: Ingress → Service → [Pod × 3] → ConfigMap + PersistentVolume
```
→ Architecture with dashed containers per namespace, solid arrows for traffic flow

### API & Sequence Flows

```
Draw an OAuth2 authorization code flow sequence diagram: User → Client → Auth Server → Resource Server
```
→ Sequence diagram with vertical lifelines and activation boxes

```
Draw the ChatGPT Plugin call sequence diagram
```
→ Sequence: User → ChatGPT → Plugin Manifest → API → Response chain

### Decision & Process Flows

```
Draw a pre-launch QA flowchart for an AI app: Code Review → Security Scan → Performance Test → Manual Approval → Deploy
```
→ Flowchart with diamond decision nodes and parallel branches

```
Generate a feature comparison matrix: RAG vs Fine-tuning vs Prompt Engineering
```
→ Comparison matrix with checked/unchecked cells across cost, latency, accuracy, flexibility

### Concept Maps

```
Visualize the LLM application tech stack: from foundation model to SDK to app framework to deployment
```
→ Layered architecture or mind map from model layer to product layer

```
Draw an AI Agent capability map: Perception / Memory / Reasoning / Action / Learning
```
→ Mind map with central "AI Agent" node and 5 radial branches

---

## 12 Styles

| # | Name | Background | Font | Best For |
|---|------|-----------|------|----------|
| 1 | **Flat Icon** *(default)* | `#ffffff` | Helvetica | Blogs, slides, docs |
| 2 | **Dark Terminal** | `#0f0f1a` | SF Mono / Fira Code | GitHub README, dev articles |
| 3 | **Blueprint** | `#0a1628` | Courier New | Architecture docs, engineering |
| 4 | **Notion Clean** | `#ffffff` | system-ui | Notion, Confluence, wikis |
| 5 | **Glassmorphism** | `#0d1117` gradient | Inter | Product sites, keynotes |
| 6 | **Claude Official** | `#f8f6f3` | system-ui | Anthropic-style diagrams, warm aesthetic |
| 7 | **OpenAI Official** | `#ffffff` | system-ui | OpenAI-style diagrams, clean modern look |
| 8 | **Dark Luxury** *(AI-authored)* | `#0a0a0a` | Georgia + system-ui | Premium docs, README heroes, conference slides |
| 9 | **C4 Review Canvas** | `#f7f2e8` | Avenir / system-ui | C4 design reviews, ADRs, responsibilities |
| 10 | **Cloud Fabric** | `#edf5fb` | Inter / system-ui | Multi-region deployments, VPC/network ownership |
| 11 | **Event Transit** | `#fbf7ee` | Avenir / system-ui | Kafka/event streams, consumer groups, DLQ |
| 12 | **Ops Pulse** | `#07111f` | SF Mono / Fira Code | SRE reviews, golden signals, critical traces |

Each style has a dedicated reference file in `references/` with exact color tokens and SVG patterns. Styles 1–7 and 9–12 are generator-backed; Style 8 uses AI-authored composition plus a static regression fixture.
The generator consumes structure fields such as `containers`, semantic `nodes[].kind`, `arrows[].flow`, and explicit port anchors. Styles 9–12 additionally validate domain-specific fields before layout.

Useful high-leverage fields for style-specific polish:
- `style_overrides` to nudge title alignment or palette tokens without forking a full style
- `containers[].header_prefix` / `containers[].header_text` for blueprint-style numbered section headers such as `01 // EDGE`
- `containers[].side_label` for Claude-style left layer labels
- `window_controls`, `meta_left`, `meta_center`, `meta_right` for terminal / document chrome
- `blueprint_title_block` for engineering title boxes in style 3

### Style Selection Guide

**For UML Diagrams:**
- **Class/Component/Package**: Style 1 (Flat Icon) or Style 4 (Notion Clean) — clear structure, easy to read
- **Sequence/Timing**: Style 2 (Dark Terminal) — monospace fonts help with alignment
- **State Machine/Activity**: Style 3 (Blueprint) — engineering aesthetic fits process flows
- **Use Case/Interview**: Style 1 (Flat Icon) — colorful, accessible

**For AI/Agent Diagrams:**
- **RAG/Agentic Search**: Style 2 (Dark Terminal) or Style 5 (Glassmorphism) — tech-forward aesthetic
- **Memory Architecture**: Style 3 (Blueprint) — emphasizes layered storage tiers
- **Multi-Agent**: Style 5 (Glassmorphism) — frosted cards distinguish agent boundaries

**For Documentation:**
- **Internal docs**: Style 4 (Notion Clean) — minimal, wiki-friendly
- **Blog posts**: Style 1 (Flat Icon) — colorful, engaging
- **GitHub README**: Style 2 (Dark Terminal) — matches dark theme
- **Presentations**: Style 5 (Glassmorphism) or Style 6 (Claude Official) — polished

**For Engineering Reviews:**
- **C4/ADR review**: Style 9 (C4 Review Canvas) — one declared abstraction level with responsibilities and protocols
- **Cloud deployment review**: Style 10 (Cloud Fabric) — explicit region/network ownership and cross-boundary mechanisms
- **Event-driven system review**: Style 11 (Event Transit) — topic rails, processors, consumer groups, state, and DLQ
- **Reliability/incident review**: Style 12 (Ops Pulse) — golden signals, one critical path, and a correlated trace

**Brand-Specific:**
- **Anthropic/Claude projects**: Style 6 (Claude Official) — warm cream background, brand colors
- **OpenAI projects**: Style 7 (OpenAI Official) — clean white, OpenAI palette
- **Premium editorial diagrams**: Style 8 (Dark Luxury) — deep black canvas, champagne-gold hierarchy, semantic color buckets

---

## Diagram Types

| Type | Description | Key Layout Rule |
|------|-------------|-----------------|
| **Architecture** | Services, components, cloud infra | Horizontal layers top→bottom |
| **Data Flow** | What data moves where | Label every arrow with data type |
| **Flowchart** | Decisions, process steps | Diamond = decision, top→bottom |
| **Agent Architecture** | LLM + tools + memory | 5-layer model: Input/Agent/Memory/Tool/Output |
| **Memory Architecture** | Mem0, MemGPT-style | Separate read/write paths, memory tiers |
| **Sequence** | API call chains, time-ordered | Vertical lifelines, horizontal messages |
| **Comparison** | Feature matrix, side-by-side | Column = system, row = attribute |
| **Mind Map** | Concept maps, radial | Central node, bezier branches |

### UML Diagram Support (14 Types)

| UML Type | Description | Best Style |
|----------|-------------|------------|
| **Class Diagram** | Classes, attributes, methods, relationships | Style 1, 4 |
| **Component Diagram** | Software components and dependencies | Style 1, 3 |
| **Deployment Diagram** | Hardware nodes and software deployment | Style 3 |
| **Package Diagram** | Package organization and dependencies | Style 1, 4 |
| **Composite Structure** | Internal structure of classes/components | Style 1, 3 |
| **Object Diagram** | Object instances and relationships | Style 1, 4 |
| **Use Case Diagram** | Actors, use cases, system boundaries | Style 1 |
| **Activity Diagram** | Workflows, parallel processes | Style 3 |
| **State Machine** | State transitions and events | Style 2, 3 |
| **Sequence Diagram** | Message exchanges over time | Style 2 |
| **Communication Diagram** | Object interactions and messages | Style 1, 2 |
| **Timing Diagram** | State changes over time | Style 2 |
| **Interaction Overview** | High-level interaction flow | Style 1, 2 |
| **ER Diagram** | Entity-relationship data models | Style 1, 3 |

---

## AI/Agent Domain Patterns

Built-in pattern knowledge:

```
RAG Pipeline         → Query → Embed → VectorSearch → Retrieve → LLM → Response
Agentic RAG          → adds Agent loop + Tool use
Agentic Search       → Query → Planner → [Search/Calc/Code] → Synthesizer
Mem0 Memory Layer    → Input → Memory Manager → [VectorDB + GraphDB] → Context
Agent Memory Types   → Sensory → Working → Episodic → Semantic → Procedural
Multi-Agent          → Orchestrator → [SubAgent×N] → Aggregator → Output
Tool Call Flow       → LLM → Tool Selector → Execution → Parser → LLM (loop)
```

---

## Shape Vocabulary

Shapes encode semantic meaning consistently across all styles:

| Concept | Shape |
|---------|-------|
| User / Human | Circle + body |
| LLM / Model | Rounded rect, double border, ⚡ |
| Agent / Orchestrator | Hexagon |
| Memory (short-term) | Dashed-border rounded rect |
| Memory (long-term) | Solid cylinder |
| Vector Store | Cylinder with inner rings |
| Graph DB | 3-circle cluster |
| Tool / Function | Rect with ⚙ |
| API / Gateway | Hexagon (single border) |
| Queue / Stream | Horizontal pipe/tube |
| Document / File | Folded-corner rect |
| Browser / UI | Rect with 3-dot titlebar |
| Decision | Diamond |
| External Service | Dashed-border rect |

---

## Arrow Semantics

| Flow Type | Stroke | Dash | Meaning |
|-----------|--------|------|---------|
| Primary data flow | 2px solid | — | Main request/response |
| Control / trigger | 1.5px solid | — | System A triggers B |
| Memory read | 1.5px solid | — | Retrieve from store |
| Memory write | 1.5px | `5,3` | Write/store operation |
| Async / event | 1.5px | `4,2` | Non-blocking |
| Feedback / loop | 1.5px curved | — | Iterative reasoning |

---

## File Structure

```
fireworks-tech-graph/
├── SKILL.md                      # Main skill — diagram types, layout rules, shape vocab
├── README.md                     # This file (English)
├── README.zh.md                  # Chinese version
├── references/
│   ├── style-1-flat-icon.md      # White background, colored accents
│   ├── style-2-dark-terminal.md  # Dark bg, neon accents, monospace
│   ├── style-3-blueprint.md      # Blueprint grid, cyan lines
│   ├── style-4-notion-clean.md   # Minimal, white, single arrow color
│   ├── style-5-glassmorphism.md  # Dark gradient, frosted glass cards
│   ├── style-6-claude-official.md # Warm cream background, Anthropic brand
│   ├── style-7-openai.md         # Clean white, OpenAI brand palette
│   ├── style-8-dark-luxury.md    # Deep black, champagne gold, AI-authored layout
│   ├── style-9-c4-review-canvas.md # C4 review semantics + deterministic rough marks
│   ├── style-10-cloud-fabric.md  # Deployment ownership + neutral cloud glyphs
│   ├── style-11-event-transit.md # Topic rails, stations, junctions, and DLQ
│   ├── style-12-ops-pulse.md     # Golden signals, critical path, and trace waterfall
│   ├── png-export.md             # Renderer selection and manual export paths
│   └── icons.md                  # 40+ product icons + semantic shapes
├── agents/
│   └── openai.yaml              # Optional Codex UI metadata
├── schemas/                      # Versioned diagram JSON Schemas
├── docs/                         # Capability contract and roadmap
├── examples/
│   └── interactive-architecture.html # Offline pan/zoom/export demo
├── fixtures/
│   ├── mem0-style1.json          # Style 1 · Mem0 memory scene
│   ├── tool-call-style2.json     # Style 2 · grounded tool-call scene
│   ├── microservices-style3.json # Style 3 · microservices blueprint
│   ├── agent-memory-types-style4.json # Style 4 · memory hierarchy
│   ├── multi-agent-style5.json   # Style 5 · specialist collaboration
│   ├── system-architecture-style6.json # Style 6 · layered system
│   ├── api-flow-style7.json      # Style 7 · API integration
│   ├── dark-luxury-style8.svg    # Style 8 · AI-authored runtime scene
│   ├── c4-review-canvas-style9.json # Style 9 · Checkout C4 review
│   ├── cloud-fabric-style10.json # Style 10 · Active-active deployment
│   ├── event-transit-style11.json # Style 11 · Checkout event line
│   ├── ops-pulse-style12.json    # Style 12 · Reliability pulse
│   └── quality-baseline/         # Internal same-topology regression set
├── scripts/
│   ├── fireworks.py              # Unified validate/render/check/animate/export CLI
│   ├── diagram_ir.py             # Typed schema-v1 normalization
│   ├── fireworks_geometry.py     # Shared routing and collision semantics
│   ├── interactive_html.py       # Sanitized offline HTML exporter
│   ├── generate-diagram.sh       # Validate SVG + export PNG
│   ├── generate-from-template.py # Create starter SVGs from templates
│   ├── motion.py                 # SVG-to-GIF validation, encoding, and atomic reports
│   ├── svg2gif.js                # Manual-timeline Chromium frame renderer
│   ├── svg2png.js                 # High-fidelity Puppeteer exporter
│   ├── validate-svg.sh           # Validation and render-check entrypoint
│   ├── validate_svg.py           # XML, marker, transform, and path collision checks
│   └── test-all-styles.sh        # Batch test all styles
├── tests/
│   ├── test_geometry_contracts.py # Router and artifact geometry gates
│   └── ...                       # IR, CLI, exporter, installer compatibility
├── tools/                         # Distribution, consistency, install canary
├── skills/fireworks-tech-graph/  # Complete npx-compatible physical mirror
├── assets/
│   └── samples/                  # Showcase diagram PNGs
├── templates/
│   ├── architecture.svg         # Architecture starter template
│   ├── data-flow.svg            # Data-flow starter template
│   └── ...                      # Additional diagram templates
└── agentloop-core.svg           # Included sample SVG
```

---

## Product Icon Coverage

**AI/ML:** OpenAI, Anthropic/Claude, Google Gemini, Meta LLaMA, Mistral, Cohere, Groq, Hugging Face

**AI Frameworks:** Mem0, LangChain, LlamaIndex, LangGraph, CrewAI, AutoGen, DSPy, Haystack

**Vector DBs:** Pinecone, Weaviate, Qdrant, Chroma, Milvus, pgvector, Faiss

**Databases:** PostgreSQL, MySQL, MongoDB, Redis, Elasticsearch, Neo4j, Cassandra

**Messaging:** Kafka, RabbitMQ, NATS, Pulsar

**Cloud:** AWS, GCP, Azure, Cloudflare, Vercel, Docker, Kubernetes

**Observability:** Grafana, Prometheus, Datadog, LangSmith, Langfuse, Arize

---

## Troubleshooting

| Symptom | Cause | Fix |
|---------|-------|-----|
| PNG is blank or all-black | `@import url()` in SVG — neither cairosvg nor rsvg-convert can fetch external fonts | Remove `@import`, use system font stack |
| PNG not generated | No renderer installed | `python3 -m pip install cairosvg` (recommended), or `brew install librsvg` / `apt install librsvg2-bin` |
| Borders or text missing in PNG | Using `rsvg-convert` on SVG with CSS / `<foreignObject>` | Switch to `cairosvg` (`python3 -m pip install cairosvg`) — much better CSS support |
| Diagram cut off at bottom | ViewBox height too short | Increase `height` in `viewBox="0 0 960 <height>"` |
| Text overflowing boxes | Labels too long | Add `text-anchor="middle"` + `<clipPath>` or shorten label |
| Icons not rendering | External CDN URL | Use inline SVG paths from `references/icons.md` |
| Browser-generated SVG renders incorrectly | cairosvg / rsvg can't replay all CSS/JS-injected styles | Use `scripts/svg2png.js` as described in `references/png-export.md` |

---

## License

MIT © 2025 fireworks-tech-graph contributors
README.zh.md
[English](README.md) | [中文](README.zh.md)

[版本历史](docs/releases/README.md) · [更新日志](CHANGELOG.md)

# fireworks-tech-graph

> 不用手画图了。用中文描述你的系统,直接得到通过几何门禁的 SVG、PNG、聚焦的 SVG 转 GIF 动效与离线交互技术图。

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![GitHub Release](https://img.shields.io/github/v/release/yizhiyanhua-ai/fireworks-tech-graph)](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/releases)
[![Codex Skill](https://img.shields.io/badge/Codex-Skill-10a37f)](https://learn.chatgpt.com/docs/build-skills)
[![Claude Code Skill](https://img.shields.io/badge/Claude%20Code-Skill-d97757)](https://code.claude.com/docs/zh-CN/skills)
[![12 种视觉风格](https://img.shields.io/badge/风格-12种-purple)]()
[![14 种图类型](https://img.shields.io/badge/图类型-14种-green)]()
[![UML 支持](https://img.shields.io/badge/UML-完整支持-orange)]()

## 概述

`fireworks-tech-graph` 是一份可由 **Codex 和 Claude Code 共用**的 Agent Skill。它将自然语言描述转化为经过几何校验的 SVG、高分辨率 PNG、经过媒体探测验证的 SVG 转 GIF 语义动效与离线交互 HTML。聚焦后的动效链路只接收生成器产出的语义 SVG,只输出一个紧凑、可验证的 GIF。项目内置 **11 种生成器风格** + **1 种 AI 手绘风格(Dark Luxury)**;新增的四种工程风格分别为 C4 评审、云部署、事件流和可靠性排查提供可执行语义契约,同时保留 AI/Agent Pattern 与全部 14 种 UML 图类型。

```
用户: "画一张 Mem0 的架构图,暗黑风格"
  → Skill 识别:Memory Architecture Diagram,Style 2
  → 生成含泳道、圆柱体、语义箭头的 SVG
  → 导出 1920px PNG
  → 输出路径:mem0-architecture.svg / mem0-architecture.png
```

---

## 效果展示

> 动态样例统一使用已验收的 5.75 秒 settled-flow 时间线:先逐步绘制线路,再让最终拓扑中的数据流额外持续 2 秒。每张完整 GIF 为 960px 宽、20fps / 115 帧;3×4 总览压缩为 1200px 动态预览。`assets/samples/` 中仍保留 1920px 无损 PNG,作为静态回归基线。

![动态 12 风格总览 — 每种风格使用独立工程场景](assets/samples/showcase-12-styles.gif)

上方 v1.2.0 总览与下方每张完整动态样例均来自已验收回归集。12 种风格保留各自独立场景,同时统一通过几何、文字适配、线束路由和语义动效质量门禁。

### 风格 1 — 扁平图标风(默认)
*Mem0 Memory Architecture — 个人记忆抽取、冲突消解、存储与检索*
![风格 1 — 扁平图标风](assets/samples/sample-style1-flat.gif)

### 风格 2 — 暗黑极客风
*Tool Call Flow — 暗黑终端执行、来源 Grounding、检索与回答合成*
![风格 2 — 暗黑极客风](assets/samples/sample-style2-dark.gif)

### 风格 3 — 工程蓝图风
*Microservices Architecture — 工程网格、领域服务、数据存储、事件与遥测*
![风格 3 — 工程蓝图风](assets/samples/sample-style3-blueprint.gif)

### 风格 4 — Notion 极简风
*Agent Memory Types — 从感知和工作上下文到长期记忆的极简层级*
![风格 4 — Notion 极简风](assets/samples/sample-style4-notion.gif)

### 风格 5 — 玻璃态卡片风
*Multi-Agent Collaboration — 协调器、专家 Agent、共享状态、评审与合成*
![风格 5 — 玻璃态卡片风](assets/samples/sample-style5-glass.gif)

### 风格 6 — Claude 官方风格
*System Architecture — 温暖的界面、Runtime、安全、记忆、工具和运维分层*
![风格 6 — Claude 官方风格](assets/samples/sample-style6-claude.gif)

### 风格 7 — OpenAI 官方风格
*API Integration Flow — 清晰的 SDK、Prompt、Model、Tool、交付与发布阶段*
![风格 7 — OpenAI 官方风格](assets/samples/sample-style7-openai.gif)

### 风格 8 — 暗黑奢华风 *(AI 手绘)*
*Agent Runtime Architecture — 控制平面、执行与状态分层,香槟金结构线和语义色桶*
![风格 8 — 暗黑奢华风](assets/samples/sample-style8-dark-luxury.gif)

### 风格 9 — C4 评审画布
*Checkout Container Review — 单一抽象层级、明确职责、技术栈与协议*
![风格 9 — C4 评审画布](assets/samples/sample-style9-c4-review-canvas.gif)

### 风格 10 — Cloud Fabric
*Active–Active Checkout Deployment — 全局入口、Region、VPC 归属与跨区复制*
![风格 10 — Cloud Fabric](assets/samples/sample-style10-cloud-fabric.gif)

### 风格 11 — Event Transit
*Checkout Event Line — Topic 轨道、处理站点、显式 Junction、DLQ 与状态投影*
![风格 11 — Event Transit](assets/samples/sample-style11-event-transit.gif)

### 风格 12 — Ops Pulse
*Checkout Reliability Pulse — Golden Signals、关键路径、OTel 导出与关联 Trace*
![风格 12 — Ops Pulse](assets/samples/sample-style12-ops-pulse.gif)

---

## 稳定输出提示词

公开展示保留 12 个互不重复的领域场景;它们通过同一套可执行构图契约保证质量可比。同拓扑回归样例仅保留在 `fixtures/quality-baseline/` 内部使用。

```text
按 style N 对应的场景出图:
1 Mem0 Memory Architecture;2 Tool Call Flow;3 Microservices Architecture;
4 Agent Memory Types;5 Multi-Agent Collaboration;6 System Architecture;
7 API Integration Flow;8 Agent Runtime Architecture;9 C4 Checkout Review;
10 Active–Active Cloud Deployment;11 Checkout Event Line;12 Checkout Reliability Pulse。
保留该场景自己的节点、分区和阅读方向。
应用 showcase 构图质量契约:零交叉、零跨线桥、每条线最多 2 个折点、
全图最多 8 个折点、节点间距至少 40px、容器内边距至少 20px,
正交线段保持简短,标签避开节点、线路和分区标题。
保留所选风格的字体、配色、卡片材质和品牌化细节。
```

新增的四种工程风格可以直接使用下面的“提示词指纹”,让路由同时选中
对应的视觉语言与领域语义契约:

```text
风格 9 · C4 评审画布:只展示一个 C4 层级,包含职责、技术栈、评审状态,以及“动作 + 协议”关系标签。
风格 10 · 多区域云部署图:展示全局入口、Region/VPC 归属、中立云图标、部署模式,以及具名的跨边界机制。
风格 11 · 事件地铁图:使用细 Topic 轨道、编号处理站、显式 Junction、Consumer Group、DLQ 与状态投影。
风格 12 · 可靠性脉冲:固定观察窗口,每个服务展示四个 Golden Signals、编号关键跳、遥测导出和一条关联 Trace。
```

把 `N` 替换为 `1`–`12`。Style 8 仍由 AI 读取 `references/style-8-dark-luxury.md` 手工绘制;Style 9–12 还会执行对应的工程语义契约;所有风格同时加载 `references/composition-quality-contract.md`。

---

## 功能特性

- **12 种视觉风格** — 11 种生成器驱动 + 1 种 AI 手绘(Dark Luxury)
- **工程语义契约** — C4 抽象层级、Deployment 归属、事件轨道拓扑、精确 Golden Signals 在渲染前 fail closed
- **可执行风格系统** — 风格约束不仅写在文档里,也真正进入生成器逻辑
- **几何安全布线** — 确定性的正交路径、强制 waypoint、端口分流、图例自动避让、标签画布约束,以及带遮罩验证的跨线桥
- **版本化 Diagram IR** — 旧 JSON 会归一化为 schema v1;重复 ID、悬空引用、非法 waypoint 和非有限坐标在渲染前直接失败
- **统一 CLI 与交互导出** — 支持 render、validate、inspect,并可导出单文件离线 HTML,包含平移缩放、主题切换、复制和 1×–4× SVG/PNG/JPEG/WebP 导出
- **14 种图类型** — 完整支持全部 UML 图类型(类图、组件图、部署图、包图、复合结构图、对象图、用例图、活动图、状态机图、序列图、通信图、时序图、交互概览图、ER 图)以及 AI/Agent 领域图
- **AI/Agent 领域内建知识** — RAG、Agentic Search、Mem0、Multi-Agent、Tool Call 等常见 Pattern 开箱即用
- **语义形状词汇表** — LLM = 双边框圆角矩形,Agent = 六边形,Vector Store = 带内环圆柱
- **语义箭头系统** — 颜色 + 虚线样式编码含义(写入/读取/异步/循环)
- **结构化 SVG 校验** — XML 解析、`marker-start/mid/end` 完整性,以及 `M/L/H/V/Q/C/S/T` 路径的箭头穿框检测
- **视觉复核门禁** — 交付前回读 PNG,检查裁切、重叠、标签位置和走线回归
- **产品图标库** — 40+ 产品品牌色:OpenAI、Anthropic、Pinecone、Weaviate、Kafka、PostgreSQL……
- **泳道分组** — 自动为复杂架构添加层级标签
- **SVG + PNG 双输出** — SVG 可编辑,1920px PNG 可直接嵌入文章
- **聚焦的语义 GIF 动效** — 只支持生成 SVG 输入和 GIF 输出;连接线从无到有并按语义顺序绘制。Style 1–12 的数据包头、终端证据流、Blueprint bead、14×10 Notion memory card、玻璃任务胶囊、治理印章、API token train、宝石 tracer、评审 cursor、双活区域流、事件列车和运维瀑布 scanner 均已验收;共享 `+2s-settled-flow` 时间修订也已成为正式默认
- **渲染器友好** — 纯内联 SVG,不依赖外部字体;在 cairosvg、rsvg-convert、headless Chrome 下都能稳定渲染

---

## Loop Engineering 设计理念

首轮渲染会被视为候选结果,交付前还要经过一条由 Agent 驱动、轮次受限的 validation feedback loop:

```text
Prompt
  → Diagram Contract
  → Semantic IR
  → Style Spec
  → Route Planner
  → SVG Build
  → Structural Validation
  → PNG Visual Readback
  → Targeted Revision
  → Verified SVG + PNG
```

这条闭环遵循五项原则:

1. **Evaluate, don't assert** — 完成状态必须有 validator 和实际渲染证据,不能只依赖模型对结果的主观判断。
2. **先确定性校验** — 依次检查 XML 结构、marker 引用、路径几何、箭头穿框和渲染可用性,再进入视觉判断。
3. **再做感知验证** — 回读导出的 PNG,检查语法工具无法识别的裁切、标签碰撞、视觉层级、留白和走线质量。
4. **定向修正** — 每轮只修改已诊断的标签、坐标、corridor 或间距,随后重新运行 validator 和 render check。
5. **有界收敛** — 默认最多执行两轮 focused correction,避免进入无上限的自我修改循环。

最终状态会明确报告闭环结果:

```text
validation: passed
visual_review: passed
```

当运行环境无法读取图片时,Skill 会明确报告 `visual_review: skipped (image reader unavailable)`。整个流程保持可观察、可审计,也不会在缺少图片证据时宣称已经完成视觉验证。

---

## 安装

### 推荐:一次安装到 Codex 与 Claude Code

必须使用真正的嵌套 Skill 路径。末尾的 `/skills/fireworks-tech-graph` 不能省略;当前版本的 `skills` CLI 在裸仓库路径下可能只选择根目录的 `SKILL.md`。

```bash
npx -y skills@1.5.17 add \
  yizhiyanhua-ai/fireworks-tech-graph/skills/fireworks-tech-graph \
  --agent codex claude-code -g -y --copy
```

命令会把完整 Skill 分别复制到 Codex 的 `~/.agents/skills/fireworks-tech-graph` 与 Claude Code 的 `~/.claude/skills/fireworks-tech-graph`,其中包含脚本、schema、fixture、模板、测试、参考资料与元数据。

### Codex 的可编辑 Git 安装

```bash
mkdir -p ~/.agents/skills
git clone https://github.com/yizhiyanhua-ai/fireworks-tech-graph.git ~/.agents/skills/fireworks-tech-graph
```

Codex 从 `~/.agents/skills` 发现个人 Skill,并会读取仓库中的可选元数据 `agents/openai.yaml`。

### Claude Code 的可编辑 Git 安装

```bash
mkdir -p ~/.claude/skills
git clone https://github.com/yizhiyanhua-ai/fireworks-tech-graph.git ~/.claude/skills/fireworks-tech-graph
```

Claude Code 从 `~/.claude/skills` 发现个人 Skill,会忽略只供 Codex 使用的 UI 元数据。

### Codex 与 Claude Code 共用一份可编辑仓库

首次安装且 Claude Code 版本不低于 2.1.203 时,可以只保留一份仓库,再把两个发现路径链接到它。创建链接前,先把已有目标目录移开。

```bash
mkdir -p ~/.local/share/agent-skills ~/.agents/skills ~/.claude/skills
git clone https://github.com/yizhiyanhua-ai/fireworks-tech-graph.git ~/.local/share/agent-skills/fireworks-tech-graph
ln -s ~/.local/share/agent-skills/fireworks-tech-graph ~/.agents/skills/fireworks-tech-graph
ln -s ~/.local/share/agent-skills/fireworks-tech-graph ~/.claude/skills/fireworks-tech-graph
```

这样 `SKILL.md`、参考资料、脚本、模板和后续更新在两端始终一致。npm registry 是独立分发渠道,版本可能晚于 GitHub Release。当前 Skill 版本请使用上面的 GitHub 嵌套路径;npm 页面继续用于查看包元数据:

```text
https://www.npmjs.com/package/@yizhiyanhua-ai/fireworks-tech-graph
```

## 更新

通过 `skills` CLI 安装时,重新执行上面的嵌套路径命令。通过 Git 安装时,更新实际安装的那一份仓库:

```bash
git -C ~/.agents/skills/fireworks-tech-graph pull
# 或
git -C ~/.claude/skills/fireworks-tech-graph pull
# 或:共享仓库方式
git -C ~/.local/share/agent-skills/fireworks-tech-graph pull
```

首次安装后重启 Codex 和 Claude Code,让两端重新发现 Skill。后续修改 `SKILL.md` 会自动生效;如果改的是脚本或参考资料而运行时没有看到更新,重启对应运行时。

以上 Shell 命令适用于 macOS、Linux、WSL 和 Git Bash。原生 Windows 请使用 `%USERPROFILE%\.agents\skills` 与 `%USERPROFILE%\.claude\skills` 对应路径。运行脚本需要 Python 3.9+;可选的 Puppeteer 路径需要 Node.js 18+。

---

## 统一 CLI

```bash
SKILL_ROOT="${CLAUDE_SKILL_DIR:-$HOME/.agents/skills/fireworks-tech-graph}"

python3 "$SKILL_ROOT/scripts/fireworks.py" doctor
python3 "$SKILL_ROOT/scripts/fireworks.py" validate architecture "$SKILL_ROOT/fixtures/api-flow-style7.json"
python3 "$SKILL_ROOT/scripts/fireworks.py" render architecture "$SKILL_ROOT/fixtures/api-flow-style7.json" diagram.svg --report layout.json
python3 "$SKILL_ROOT/scripts/fireworks.py" check diagram.svg
python3 "$SKILL_ROOT/scripts/fireworks.py" export-html diagram.svg diagram.html --title "API Integration Flow"
python3 "$SKILL_ROOT/scripts/fireworks.py" animate diagram.svg diagram.gif
```

HTML 导出是单文件离线产物。导出器会清洗 SVG,并提供平移、缩放、复位、明暗主题、复制 SVG 源码,以及 1×–4× 的 SVG/PNG/JPEG/WebP 下载。

动效可以直接说 **“生成 GIF”**、**“制作 GIF”** 或 **“让这张图动起来”**。命令只接收带有 12 套已验收动效契约之一的生成器语义 SVG。它不锁定源文件的精确字节,因此同一受支持拓扑下通过校验的标题和内容变化可以正常使用;缺失或改变 role/stage/order 覆盖、线路方向、必要颜色或几何时会 fail closed。动效媒体格式只允许 GIF,默认还会写出 `<output>.motion.json` 验证报告。正式默认参数为 960px、5.75 秒、20fps、115 个帧中心采样:第 1–36 帧完成线路构建,第 36–38 帧淡入运行流,第 38–109 帧保持完整稳定数据流,第 110–114 帧执行五档 reset。Style 1–12 的 signature、速度、路径、几何、构建节奏均已验收,包括 packet head、terminal evidence trace、Blueprint bead、14×10 Notion memory card 和八种场景专属 signature;共享的 `+2s-settled-flow` 时间修订也已验收,默认新包的相关状态统一记录为 `user-approved`。75 帧及以下继续要求全部帧唯一;更长时间线允许完整不透明区间内出现非相邻重复。frame 110 因 reset opacity 精确保留 1.00,成为唯一边界例外,并显式分类为 `intentional_reset_boundary_repeat`;frame 111–114 必须全局不同。长时间线至少保留 75 个唯一 raster,并禁止相邻重复。全样式 75-vs-115 gate 分开统计 binary-exact 与 decoded-RGBA-exact;仅当 AE ≤ 128、normalized RMSE ≤ 0.001、差异 component 宽或高不超过 2px 且只落在 edge/node border 时,才接受 compositor 抗锯齿等价,DOM 与 signature geometry 始终 strict-exact。显式 3.75 秒/75 帧与 2.75 秒/55 帧调用继续兼容。详见 [聚焦的 SVG 转 GIF 动效](references/motion-effects.md)。

| Style | Preset | 运行态 signature |
|---:|---|---|
| 5 | `agent-orchestration` | 玻璃任务胶囊 + coordinator halo |
| 6 | `governed-runtime` | governance thread + policy seal |
| 7 | `token-stream` | API rail + 三格 token train |
| 8 | `golden-circuit` | luxury circuit rail + gem tracer |
| 9 | `review-trace` | review rail + moving review cursor |
| 10 | `cloud-flow` | region chevrons + replication capsule |
| 11 | `event-transit` | event train + exception/projection cars |
| 12 | `ops-pulse` | ECG/export heads + trace reveal + waterfall scanner |

---

## 安装依赖

仓库自带的 SVG/PNG 校验与导出脚本需要 **cairosvg**(推荐)或 `rsvg-convert`。可选的 SVG 转 GIF 动效导出需要 FFmpeg/FFprobe、Chrome/Chromium,以及 `puppeteer` 或 `puppeteer-core`。

```bash
# 推荐:cairosvg(CSS 支持最好)
python3 -m pip install cairosvg

# 备选:rsvg-convert(系统包,可能丢失 CSS / <foreignObject>)
brew install librsvg                   # macOS
sudo apt install librsvg2-bin          # Ubuntu/Debian

# 可选:语义动效导出。依赖分别安装在两份 Skill 旁边,因为渲染器会
# 刻意忽略调用者当前目录中的同名 Node 模块。
brew install ffmpeg                    # macOS;其他平台使用系统包管理器
for SKILL_ROOT in \
  "$HOME/.agents/skills/fireworks-tech-graph" \
  "$HOME/.claude/skills/fireworks-tech-graph"
do
  [ -d "$SKILL_ROOT" ] || continue
  npm install --prefix "$SKILL_ROOT" --ignore-scripts --no-save --package-lock=false puppeteer-core@25.3.0
  python3 "$SKILL_ROOT/scripts/fireworks.py" doctor
done

# 验证脚本支持的任一渲染器
python3 -c "import cairosvg; print(cairosvg.__version__)"
rsvg-convert --version
```

| 渲染器 | 渲染质量 | 安装成本 | 适用场景 |
|--------|---------|---------|---------|
| **cairosvg** | ✅ 好 | 一行 `python3 -m pip install` | 默认推荐,平衡最佳 |
| rsvg-convert | ⚠️ 一般 | 系统包 | 没有 Python 环境,简单图形够用 |
| puppeteer | ✅✅ 最佳 | Node + Chromium | D3、Mermaid 或像素级还原的手动浏览器渲染方案 |

> 渲染器对比和 Puppeteer 用法见 [references/png-export.md](references/png-export.md),浏览器导出脚本位于 `scripts/svg2png.js`。

---

## 使用方式

### 触发词

以下关键词会自动触发 Skill:

```
画图 / 帮我画 / 生成图 / 做个图 / 架构图 / 流程图 / 可视化一下 / 出图
generate diagram / draw diagram / create chart / visualize
生成 GIF / 制作 GIF / 让这张图动起来 / 把刚才的 SVG 转成 GIF / Generate a GIF / animate this diagram / animate this SVG as a GIF
```

### 基本用法

```
画一张 RAG 流程图
```

```
生成一张 Agentic Search 架构图
```

### 指定风格

```
画一张微服务架构图,风格2(暗黑极客风)
```

```
生成 Multi-Agent 协作图,玻璃态风格
```

### 指定输出路径

```
生成 Mem0 架构图,输出到 ~/Desktop/
```

```
画一张 Tool Call 流程图 --output /tmp/diagrams/
```

---

## 场景示例集

### AI/Agent 系统

```
画一张 Agentic RAG 和普通 RAG 的对比图,用 Notion 极简风
```
→ 功能矩阵对比:检索策略、Agent 循环、工具调用、延迟、成本

```
生成一张 Mem0 记忆架构图,包含向量库、图数据库、KV 存储和记忆管理器
```
→ 分泳道记忆架构:Input → Memory Manager → 存储层 → 检索输出

```
画一张 Multi-Agent 协作图:Orchestrator 调度 3 个 SubAgent(搜索/计算/代码执行),汇聚到 Aggregator
```
→ Agent 架构,六边形节点 + 工具层 + 结果聚合

```
可视化一下 Tool Call 的执行流程:LLM → Tool Selector → Execution → Parser → 回到 LLM
```
→ 含决策循环的流程图,展示工具调用的完整生命周期

```
画一张 Agent 的 5 种记忆类型图:感知记忆、工作记忆、情景记忆、语义记忆、程序记忆
```
→ 思维导图或分层架构,从感官输入到程序技能的记忆层级

### 基础设施与云架构

```
帮我画一张微服务架构图:Client → API Gateway → [用户服务 / 订单服务 / 支付服务] → PostgreSQL + Redis
```
→ 水平分层架构,每个服务集群一个泳道

```
生成一张数据管道图:Kafka 消费数据 → Spark 处理 → 写入 S3 → Athena 查询
```
→ 数据流图,每条箭头标注数据类型(stream / batch / query)

```
画一张 Kubernetes 部署架构:Ingress → Service → [Pod × 3] → ConfigMap + PersistentVolume
```
→ 架构图,Namespace 用虚线框,流量用实线箭头

### API 与时序流程

```
画一张 OAuth2 授权码流程的序列图:用户 → 客户端 → 授权服务器 → 资源服务器
```
→ 序列图,垂直生命线 + 激活框

```
帮我画一张 ChatGPT Plugin 的调用时序图
```
→ 时序:User → ChatGPT → Plugin Manifest → API → 响应链

### 决策与流程图

```
画一张 AI 应用上线前的质检流程图:代码审查 → 安全扫描 → 性能测试 → 人工审核 → 发布
```
→ 流程图,含菱形决策节点和并行分支

```
生成一张 RAG vs Fine-tuning vs Prompt Engineering 的功能对比图
```
→ 功能矩阵,对比成本、延迟、准确率、灵活性

### 概念图与知识图谱

```
帮我可视化一下 LLM 应用的技术栈:从底层模型到 SDK 到应用框架到部署层
```
→ 分层架构图或思维导图,从模型层到产品层

```
画一张 AI Agent 的核心能力地图:感知 / 记忆 / 推理 / 行动 / 学习
```
→ 以"AI Agent"为中心的放射状思维导图,5 个核心能力分支

---

## 12 种风格

| # | 名称 | 背景色 | 字体 | 适用场景 |
|---|------|--------|------|----------|
| 1 | **扁平图标风** *(默认)* | `#ffffff` | Helvetica | 博客、幻灯片、技术文档 |
| 2 | **暗黑极客风** | `#0f0f1a` | SF Mono / Fira Code | GitHub README、开发者文章 |
| 3 | **工程蓝图风** | `#0a1628` | Courier New | 架构设计文档、工程规范 |
| 4 | **Notion 极简风** | `#ffffff` | system-ui | Notion、Confluence、内部 Wiki |
| 5 | **玻璃态卡片风** | `#0d1117` 渐变 | Inter | 产品官网、演讲 Keynote |
| 6 | **Claude 官方风格** | `#f8f6f3` | system-ui | Anthropic 风格图表,温暖专业美学 |
| 7 | **OpenAI 官方风格** | `#ffffff` | system-ui | OpenAI 风格图表,简洁现代设计 |
| 8 | **暗黑奢华风** *(AI 手绘)* | `#0a0a0a` | Georgia + system-ui | 高级文档、README Hero、技术演讲 |
| 9 | **C4 评审画布** | `#f7f2e8` | Avenir / system-ui | C4 设计评审、ADR、职责边界 |
| 10 | **Cloud Fabric** | `#edf5fb` | Inter / system-ui | 多 Region 部署、VPC/网络归属 |
| 11 | **Event Transit** | `#fbf7ee` | Avenir / system-ui | Kafka/Event Stream、Consumer Group、DLQ |
| 12 | **Ops Pulse** | `#07111f` | SF Mono / Fira Code | SRE 评审、Golden Signals、关键 Trace |

每种风格在 `references/` 目录下都有专属参考文件,包含精确的颜色 Token 和 SVG Pattern。风格 1–7 与 9–12 由生成器驱动;风格 8 使用 AI 手绘构图和静态回归 fixture。
生成器会直接消费 `containers`、语义化 `nodes[].kind`、`arrows[].flow` 以及显式端口锚点;Style 9–12 还会在布局前校验领域字段。

几个很有用的增强字段:
- `style_overrides`:在不复制整套 style 的前提下微调标题对齐或配色 token
- `containers[].header_prefix` / `containers[].header_text`:用于 style 3 这种 `01 // EDGE` 的工程编号分区标题
- `containers[].side_label`:用于 style 6 这类左侧 Layer Label
- `window_controls`、`meta_left`、`meta_center`、`meta_right`:用于终端 / 文档风格的顶部 chrome
- `blueprint_title_block`:用于 style 3 的蓝图标题信息框

### 风格选择指南

**UML 图类型:**
- **类图/组件图/包图**:风格 1(扁平图标风)或风格 4(Notion 极简风)— 结构清晰,易于阅读
- **序列图/时序图**:风格 2(暗黑极客风)— 等宽字体有助于对齐
- **状态机图/活动图**:风格 3(工程蓝图风)— 工程美学适合流程图
- **用例图/交互图**:风格 1(扁平图标风)— 彩色,易于理解

**AI/Agent 图类型:**
- **RAG/Agentic Search**:风格 2(暗黑极客风)或风格 5(玻璃态卡片风)— 科技感强
- **记忆架构**:风格 3(工程蓝图风)— 强调分层存储结构
- **Multi-Agent**:风格 5(玻璃态卡片风)— 磨砂卡片区分 Agent 边界

**文档类型:**
- **内部文档**:风格 4(Notion 极简风)— 极简,适合 Wiki
- **技术博客**:风格 1(扁平图标风)— 彩色,吸引眼球
- **GitHub README**:风格 2(暗黑极客风)— 匹配暗色主题
- **演示文稿**:风格 5(玻璃态卡片风)或风格 6(Claude 官方风格)— 精致专业

**品牌特定:**
- **Anthropic/Claude 项目**:风格 6(Claude 官方风格)— 温暖奶油色背景,品牌感强且克制
- **OpenAI 项目**:风格 7(OpenAI 官方风格)— 简洁白色,OpenAI 配色
- **高级编辑感技术图**:风格 8(暗黑奢华风)— 深黑画布、香槟金层级和语义色桶

**工程评审:**
- **C4/ADR 评审**:风格 9(C4 评审画布)— 单一抽象层级、职责与协议明确
- **云部署评审**:风格 10(Cloud Fabric)— Region/Network 归属与跨边界机制明确
- **事件驱动系统**:风格 11(Event Transit)— Topic、Processor、Consumer Group、状态与 DLQ
- **可靠性/事故复盘**:风格 12(Ops Pulse)— Golden Signals、唯一关键路径与关联 Trace

---

## 支持的图类型

| 类型 | 描述 | 关键布局规则 |
|------|------|-------------|
| **架构图** | 服务、组件、云基础设施 | 水平分层,自上而下 |
| **数据流图** | 数据在系统中的流向 | 每条箭头标注数据类型 |
| **流程图** | 决策树、流程步骤 | 菱形 = 决策,自上而下 |
| **Agent 架构图** | LLM + 工具 + 记忆 | 五层模型:输入/Agent/记忆/工具/输出 |
| **记忆架构图** | Mem0、MemGPT 风格 | 读/写路径分离,记忆层级分明 |
| **序列图** | API 调用链、时序交互 | 垂直生命线,水平消息箭头 |
| **对比图** | 功能矩阵、方案比较 | 列 = 系统,行 = 属性 |
| **思维导图** | 概念地图、发散思维 | 中心节点,贝塞尔曲线分支 |

### UML 图类型支持(14 种)

| UML 类型 | 描述 | 推荐风格 |
|----------|------|----------|
| **类图** | 类、属性、方法、关系 | 风格 1, 4 |
| **组件图** | 软件组件和依赖关系 | 风格 1, 3 |
| **部署图** | 硬件节点和软件部署 | 风格 3 |
| **包图** | 包组织和依赖关系 | 风格 1, 4 |
| **复合结构图** | 类/组件的内部结构 | 风格 1, 3 |
| **对象图** | 对象实例和关系 | 风格 1, 4 |
| **用例图** | 参与者、用例、系统边界 | 风格 1 |
| **活动图** | 工作流、并行流程 | 风格 3 |
| **状态机图** | 状态转换和事件 | 风格 2, 3 |
| **序列图** | 时间顺序的消息交换 | 风格 2 |
| **通信图** | 对象交互和消息 | 风格 1, 2 |
| **时序图** | 状态随时间的变化 | 风格 2 |
| **交互概览图** | 高层交互流程 | 风格 1, 2 |
| **ER 图** | 实体关系数据模型 | 风格 1, 3 |

---

## AI/Agent 领域内建 Pattern

Skill 内置以下领域知识,可直接描述场景生成:

```
RAG Pipeline         → Query → Embed → VectorSearch → Retrieve → LLM → Response
Agentic RAG          → 在 RAG 基础上加入 Agent 循环 + 工具调用
Agentic Search       → Query → Planner → [Search/Calc/Code] → Synthesizer
Mem0 记忆层          → Input → Memory Manager → [VectorDB + GraphDB] → Context
Agent 记忆类型       → 感知记忆 → 工作记忆 → 情景记忆 → 语义记忆 → 程序记忆
Multi-Agent          → Orchestrator → [SubAgent×N] → Aggregator → Output
Tool Call 流程       → LLM → Tool Selector → Execution → Parser → LLM (循环)
```

---

## 形状词汇表

形状在所有风格中保持一致的语义:

| 概念 | 形状 |
|------|------|
| 用户 / 人类 | 圆形 + 身体路径 |
| LLM / 模型 | 圆角矩形,双边框,⚡ |
| Agent / 编排器 | 六边形 |
| 短期记忆 | 虚线边框圆角矩形 |
| 长期记忆 | 实线圆柱体 |
| Vector Store | 带内环圆柱 |
| Graph DB | 三圆簇 |
| 工具 / 函数 | 带 ⚙ 的矩形 |
| API / 网关 | 六边形(单边框) |
| 消息队列 / 流 | 横向管道 |
| 文档 / 文件 | 折角矩形 |
| 浏览器 / UI | 带三点标题栏的矩形 |
| 决策节点 | 菱形 |
| 外部服务 | 虚线边框矩形 |

---

## 箭头语义

| 流类型 | 线宽 | 虚线 | 含义 |
|--------|------|------|------|
| 主数据流 | 2px 实线 | — | 主要请求/响应路径 |
| 控制 / 触发 | 1.5px 实线 | — | 系统 A 触发 B |
| 记忆读取 | 1.5px 实线 | — | 从存储检索 |
| 记忆写入 | 1.5px | `5,3` | 写入/存储操作 |
| 异步 / 事件 | 1.5px | `4,2` | 非阻塞 |
| 反馈 / 循环 | 1.5px 曲线 | — | 迭代推理 |

---

## 文件结构

```
fireworks-tech-graph/
├── SKILL.md                      # 主 Skill 文件 — 图类型、布局规则、形状词汇
├── README.md                     # 英文文档
├── README.zh.md                  # 本文件(中文)
├── references/
│   ├── style-1-flat-icon.md      # 白底风格 — 彩色强调色
│   ├── style-2-dark-terminal.md  # 暗黑风格 — Neon 配色,等宽字体
│   ├── style-3-blueprint.md      # 蓝图风格 — 网格底纹,青色线条
│   ├── style-4-notion-clean.md   # 极简风格 — 白底,单色箭头
│   ├── style-5-glassmorphism.md  # 玻璃态风格 — 深色渐变,磨砂卡片
│   ├── style-6-claude-official.md # Claude 官方风格 — 温暖奶油色,Anthropic 品牌
│   ├── style-7-openai.md         # OpenAI 官方风格 — 简洁白色,OpenAI 品牌配色
│   ├── style-8-dark-luxury.md    # 深黑画布、香槟金、AI 手绘布局
│   ├── style-9-c4-review-canvas.md # C4 评审语义 + 确定性手绘纹理
│   ├── style-10-cloud-fabric.md  # Deployment 归属 + 中性 Cloud Glyph
│   ├── style-11-event-transit.md # Topic 轨道、站点、Junction 与 DLQ
│   ├── style-12-ops-pulse.md     # Golden Signals、关键路径与 Trace Waterfall
│   ├── png-export.md             # 渲染器选择与手动导出方案
│   └── icons.md                  # 40+ 产品图标 + 语义形状模板
├── agents/
│   └── openai.yaml              # Codex 可选 UI 元数据
├── schemas/                      # 版本化 Diagram JSON Schema
├── docs/                         # 能力契约与路线图
├── examples/
│   └── interactive-architecture.html # 离线平移/缩放/导出演示
├── fixtures/
│   ├── mem0-style1.json          # Style 1 · Mem0 记忆场景
│   ├── tool-call-style2.json     # Style 2 · Tool Call 场景
│   ├── microservices-style3.json # Style 3 · 微服务蓝图
│   ├── agent-memory-types-style4.json # Style 4 · 记忆层级
│   ├── multi-agent-style5.json   # Style 5 · 多 Agent 协作
│   ├── system-architecture-style6.json # Style 6 · 系统分层
│   ├── api-flow-style7.json      # Style 7 · API 集成
│   ├── dark-luxury-style8.svg    # Style 8 · AI 手绘 Runtime 场景
│   ├── c4-review-canvas-style9.json # Style 9 · Checkout C4 评审
│   ├── cloud-fabric-style10.json # Style 10 · Active-Active Deployment
│   ├── event-transit-style11.json # Style 11 · Checkout Event Line
│   ├── ops-pulse-style12.json    # Style 12 · Reliability Pulse
│   └── quality-baseline/         # 内部同拓扑质量回归基线
├── scripts/
│   ├── fireworks.py              # 统一 validate/render/check/animate/export CLI
│   ├── diagram_ir.py             # Schema v1 类型化归一层
│   ├── fireworks_geometry.py     # 路由与碰撞共享语义
│   ├── interactive_html.py       # 安全的离线 HTML 导出器
│   ├── generate-diagram.sh       # SVG 校验与 PNG 导出
│   ├── generate-from-template.py # 基于模板生成 SVG 起始文件
│   ├── motion.py                 # SVG 转 GIF 校验、编码与原子报告
│   ├── svg2gif.js                # 手动时间轴 Chromium 逐帧渲染器
│   ├── svg2png.js                 # Puppeteer 高保真导出脚本
│   ├── validate-svg.sh           # 校验与渲染检查入口
│   ├── validate_svg.py           # XML、marker、transform 与路径碰撞检测
│   └── test-all-styles.sh        # 批量测试所有风格
├── tests/
│   ├── test_geometry_contracts.py # 路由与产物几何门禁
│   └── ...                       # IR、CLI、导出器、安装兼容测试
├── tools/                         # 分发、项目一致性、安装 canary
├── skills/fireworks-tech-graph/  # 完整的 npx 兼容物理镜像
├── assets/
│   └── samples/                  # 示例图 PNG
├── templates/
│   ├── architecture.svg         # 架构图模板
│   ├── data-flow.svg            # 数据流模板
│   └── ...                      # 其他图类型模板
└── agentloop-core.svg           # 仓库自带示例 SVG
```

---

## 产品图标覆盖范围

**AI/ML 模型:** OpenAI、Anthropic/Claude、Google Gemini、Meta LLaMA、Mistral、Cohere、Groq、Hugging Face

**AI 框架:** Mem0、LangChain、LlamaIndex、LangGraph、CrewAI、AutoGen、DSPy、Haystack

**向量数据库:** Pinecone、Weaviate、Qdrant、Chroma、Milvus、pgvector、Faiss

**关系型/NoSQL 数据库:** PostgreSQL、MySQL、MongoDB、Redis、Elasticsearch、Neo4j、Cassandra

**消息队列:** Kafka、RabbitMQ、NATS、Pulsar

**云服务 & 基础设施:** AWS、GCP、Azure、Cloudflare、Vercel、Docker、Kubernetes

**可观测性:** Grafana、Prometheus、Datadog、LangSmith、Langfuse、Arize

---

## 故障排查

| 现象 | 原因 | 处理方式 |
|------|------|----------|
| `npx skills add` 后只有 `SKILL.md` | 使用了裸仓库路径,CLI 选中了根 Skill | 使用 `yizhiyanhua-ai/fireworks-tech-graph/skills/fireworks-tech-graph` 嵌套路径重新安装 |
| SVG 校验报告 `edge_node` / `edge_reserved` | 业务线穿过节点、图例或标题区 | 调整端口、`corridor_x/y` 或 waypoint,再重新渲染;不要手工删除 geometry gate |
| SVG 校验报告 `edge_overlap` | 两条业务线共用同一段轨道 | 给边分配独立 corridor 或 waypoint;跨线桥不能修复共线重叠 |
| PNG 为空或纯黑 | SVG 中含外部字体 `@import url()` | 移除 `@import`,使用系统字体栈 |
| PNG 未生成 | 未安装 raster renderer | 推荐 `python3 -m pip install cairosvg`,也可安装 `rsvg-convert` |
| 交互 HTML 导出拒绝 SVG | SVG 含脚本、事件属性、外部链接或 `foreignObject` | 将资源内联并移除 active content 后重新导出 |
| 图底部被截断 | `viewBox` 高度不足 | 增加画布 `height`,再执行严格 geometry check |

---

## License

MIT © 2025 fireworks-tech-graph contributors
.nojekyll
CONTRIBUTING.md
# Contributing

Fireworks Tech Graph accepts fixes, fixtures, style documentation, and renderer improvements.

## Local checks

Requirements: Python 3.9+, Node.js 18+, and either CairoSVG or `rsvg-convert`.

```bash
python3 -m unittest discover -s tests -v
TEST_OUTPUT_DIR="$(mktemp -d)" ./scripts/test-all-styles.sh
python3 tools/check_project_consistency.py
python3 tools/distribution.py --check
./tools/install-canary.sh "$PWD/skills/fireworks-tech-graph"
```

Geometry changes must include a failing fixture or unit test before the fix. Generated SVGs must pass the `geometry` check; declared jumps require a matching bridge mask.

Run `python3 tools/distribution.py --sync` after changing any packaged file. Commit the canonical root files and the refreshed nested mirror together.

Please keep secrets, tokens, cookies, generated caches, and local test output out of commits.
LICENSE
MIT License

Copyright (c) 2025 fireworks-tech-graph contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
agents/openai.yaml
interface:
  display_name: "Fireworks Tech Graph"
  short_description: "Generate geometry-safe technical diagrams"
  default_prompt: "Use $fireworks-tech-graph to turn the user's system or workflow description into a geometry-checked SVG, then export PNG, controlled offline HTML, or a validated semantic GIF. Treat direct GIF requests as the approved 5.75s/115-frame SVG-to-GIF default."
assets/icons/cloud/manifest-v1.json
{
  "schema_version": 1,
  "version": "2026.07-neutral.1",
  "policy": "Built-in provider-neutral glyphs. Official vendor packs remain external, versioned adapters.",
  "official_sources": {
    "aws": "https://aws.amazon.com/architecture/icons/",
    "azure": "https://learn.microsoft.com/azure/architecture/icons/",
    "gcp": "https://cloud.google.com/icons"
  },
  "icons": [
    {
      "id": "generic:traffic",
      "aliases": ["global traffic", "dns", "cdn"],
      "badge": "EDGE",
      "category": "edge",
      "color": "#2563eb",
      "glyph": "globe"
    },
    {
      "id": "generic:gateway",
      "aliases": ["load balancer", "api gateway"],
      "badge": "GW",
      "category": "network",
      "color": "#7c3aed",
      "glyph": "gateway"
    },
    {
      "id": "generic:compute",
      "aliases": ["service", "container", "kubernetes workload"],
      "badge": "APP",
      "category": "compute",
      "color": "#059669",
      "glyph": "compute"
    },
    {
      "id": "generic:database",
      "aliases": ["relational database", "managed database"],
      "badge": "DB",
      "category": "data",
      "color": "#ea580c",
      "glyph": "database"
    },
    {
      "id": "generic:stream",
      "aliases": ["event stream", "message bus"],
      "badge": "BUS",
      "category": "integration",
      "color": "#db2777",
      "glyph": "stream"
    },
    {
      "id": "generic:observability",
      "aliases": ["metrics", "telemetry"],
      "badge": "OBS",
      "category": "operations",
      "color": "#0891b2",
      "glyph": "observe"
    }
  ]
}
docs/releases/README.md
# Release history

This directory is the source of truth for the curated GitHub Release notes. Version dates describe when each version originally became available, not when a historical GitHub Release page was backfilled.

| Version | Version date | Tag commit | npm `gitHead` | Distribution record | Notes |
| --- | --- | --- | --- | --- | --- |
| [`v1.2.0`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/releases/tag/v1.2.0) | 2026-07-17 | [`ea534c2d`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/ea534c2d2817d43e4c02fe0ea9f67dfced6dece3) | — | Latest GitHub release | [Release notes](v1.2.0.md) |
| [`v1.1.0`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/releases/tag/v1.1.0) | 2026-07-15 | [`1c7ba0fe`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/1c7ba0fe69ef5b166821eefb6fe447a0e5d70be9) | — | GitHub release | [Release notes](v1.1.0.md) |
| [`v1.0.5`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/releases/tag/v1.0.5) | 2026-07-11 | [`14be3ad3`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/14be3ad3b05389a5d603562c207eb37157637127) | — | Repository version; not published to npm | [Release notes](v1.0.5.md) |
| [`v1.0.4`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/releases/tag/v1.0.4) | 2026-04-12 | [`c8668407`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/c8668407ebfa72e00719be8f4312b71ab90c3c3e) | [`c3726124`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/c37261248ca674d5d94e24d9d7d85e50a88c77d1) | [npm `1.0.4`](https://www.npmjs.com/package/@yizhiyanhua-ai/fireworks-tech-graph/v/1.0.4) | [Release notes](v1.0.4.md) |
| [`v1.0.3`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/releases/tag/v1.0.3) | 2026-04-12 | [`c3726124`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/c37261248ca674d5d94e24d9d7d85e50a88c77d1) | [`921fb308`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/921fb30855cd76e03e1b96ff3463028ddcd36deb) | [npm `1.0.3`](https://www.npmjs.com/package/@yizhiyanhua-ai/fireworks-tech-graph/v/1.0.3) | [Release notes](v1.0.3.md) |
| [`v1.0.2`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/releases/tag/v1.0.2) | 2026-04-12 | [`921fb308`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/921fb30855cd76e03e1b96ff3463028ddcd36deb) | [`fee15924`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/fee15924e5d9a273d270067dce6380f27de3d033) | [npm `1.0.2`](https://www.npmjs.com/package/@yizhiyanhua-ai/fireworks-tech-graph/v/1.0.2) | [Release notes](v1.0.2.md) |
| [`v1.0.1`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/releases/tag/v1.0.1) | 2026-04-12 | [`27f41d98`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/27f41d98883113affbb28a2ebfff4b566e3cc819) | [`27f41d98`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/27f41d98883113affbb28a2ebfff4b566e3cc819) | [npm `1.0.1`](https://www.npmjs.com/package/@yizhiyanhua-ai/fireworks-tech-graph/v/1.0.1) | [Release notes](v1.0.1.md) |
| [`v1.0.0`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/releases/tag/v1.0.0) | 2026-04-12 | [`27f41d98`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/27f41d98883113affbb28a2ebfff4b566e3cc819) | [`27f41d98`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/27f41d98883113affbb28a2ebfff4b566e3cc819) | [npm `1.0.0`](https://www.npmjs.com/package/@yizhiyanhua-ai/fireworks-tech-graph/v/1.0.0) | [Release notes](v1.0.0.md) |

## Historical provenance

GitHub Release pages for `v1.0.0` through `v1.0.5` were backfilled on 2026-07-17. The npm packages were sometimes published from a working tree immediately before its matching commit: npm `1.0.2`, `1.0.3`, and `1.0.4` retain the preceding `gitHead`, while their complete published file sets match the subsequent commits shown in the Tag commit column. No fully committed tree reproduces every packaging change in npm `1.0.0` or `1.0.1`; those tags use the registry source anchor, and their notes state the limitation.
docs/releases/v1.0.0.md
# Fireworks Tech Graph v1.0.0

The first public npm release established Fireworks Tech Graph as a reusable Agent Skill for generating polished technical diagrams from natural-language requirements.

## Version information

- Version: `v1.0.0` / npm `1.0.0`
- Version date: 2026-04-12; npm published at `2026-04-12T00:09:17.387Z`
- Tag commit: [`27f41d98`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/27f41d98883113affbb28a2ebfff4b566e3cc819)
- npm registry `gitHead`: [`27f41d98`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/27f41d98883113affbb28a2ebfff4b566e3cc819)
- Previous version: initial public release
- Runtime metadata: Node.js 14 or newer
- Distribution: [npm package `1.0.0`](https://www.npmjs.com/package/@yizhiyanhua-ai/fireworks-tech-graph/v/1.0.0)

## Highlights

- Shipped seven built-in visual styles for architecture, flow, UML, ER, sequence, and related technical diagrams.
- Included SVG generation, high-resolution PNG export through `rsvg-convert`, validation scripts, and bilingual documentation.
- Included style references and the first seven-style showcase.

## Provenance

This GitHub Release was backfilled on 2026-07-17. Of the 26 files in the published npm tarball, all 25 Git-backed files match the registry-recorded `gitHead` byte-for-byte; `package.json` was publication metadata that had not yet been committed. The tag is therefore the closest source provenance anchor, but GitHub's generated source archive is not byte-identical to the npm artifact.
docs/releases/v1.0.1.md
# Fireworks Tech Graph v1.0.1

This patch release tightened the diagram-quality prompt contract and corrected npm repository metadata.

## Version information

- Version: `v1.0.1` / npm `1.0.1`
- Version date: 2026-04-12; npm published at `2026-04-12T00:12:37.149Z`
- Tag commit: [`27f41d98`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/27f41d98883113affbb28a2ebfff4b566e3cc819)
- npm registry `gitHead`: [`27f41d98`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/27f41d98883113affbb28a2ebfff4b566e3cc819)
- Previous version: [`v1.0.0`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/releases/tag/v1.0.0)
- Runtime metadata: Node.js 14 or newer
- Distribution: [npm package `1.0.1`](https://www.npmjs.com/package/@yizhiyanhua-ai/fireworks-tech-graph/v/1.0.1)

## Highlights

- Added an explicit validation checklist for connector-node collisions and orthogonal rerouting.
- Added text-overflow, endpoint-alignment, and connector-label background checks.
- Normalized the npm repository URL metadata.

## Provenance

The npm registry records the same `gitHead` for `1.0.0` and `1.0.1`. The published tarball contains two working-tree-only changes relative to that commit: `package.json` and an expanded `SKILL.md` layout-validation checklist; the other 24 files match byte-for-byte. No historical commit exactly reproduces this npm artifact, so the backfilled tag records the registry provenance anchor.
docs/releases/v1.0.3.md
# Fireworks Tech Graph v1.0.3

This patch release made the installation source explicit and separated the Agent Skill installer path from the npm distribution page.

## Version information

- Version: `v1.0.3` / npm `1.0.3`
- Version date: 2026-04-12; npm published at `2026-04-12T13:28:10.195Z`
- Tag commit: [`c3726124`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/c37261248ca674d5d94e24d9d7d85e50a88c77d1), whose versioned content matches the npm tarball
- npm registry `gitHead`: [`921fb308`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/921fb30855cd76e03e1b96ff3463028ddcd36deb)
- Previous version: [`v1.0.2`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/releases/tag/v1.0.2)
- Runtime metadata: Node.js 14 or newer
- Distribution: [npm package `1.0.3`](https://www.npmjs.com/package/@yizhiyanhua-ai/fireworks-tech-graph/v/1.0.3)

## Highlights

- Standardized installation on `npx skills add yizhiyanhua-ai/fireworks-tech-graph`.
- Clarified that `skills add` resolves the GitHub repository while npm provides package distribution and README discovery.

## Provenance

This GitHub Release was backfilled on 2026-07-17. npm `1.0.3` was published 13 seconds before the matching repository commit, so the registry retained the preceding `gitHead`. The historical tag uses the subsequent commit because its README, Chinese README, Skill instructions, and `package.json` match the published versioned content.
docs/releases/v1.0.2.md
# Fireworks Tech Graph v1.0.2

This release introduced the style-driven generator and a reproducible fixture-and-template workflow for the original seven styles.

## Version information

- Version: `v1.0.2` / npm `1.0.2`
- Version date: 2026-04-12; npm published at `2026-04-12T13:24:35.073Z`
- Tag commit: [`921fb308`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/921fb30855cd76e03e1b96ff3463028ddcd36deb), whose complete file set matches the npm tarball
- npm registry `gitHead`: [`fee15924`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/fee15924e5d9a273d270067dce6380f27de3d033)
- Previous version: [`v1.0.1`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/releases/tag/v1.0.1)
- Runtime metadata: Node.js 14 or newer
- Distribution: [npm package `1.0.2`](https://www.npmjs.com/package/@yizhiyanhua-ai/fireworks-tech-graph/v/1.0.2)

## Highlights

- Added a Python style-driven generator, seven scenario fixtures, and ten reusable SVG diagram templates.
- Added formal npm package metadata for repository, license, files, runtime, and discovery keywords.
- Expanded the package payload to include `fixtures/` and `templates/`.
- Refreshed all seven showcase renders and strengthened SVG validation and all-style test scripts.
- Added a style-to-diagram selection matrix for more reliable prompting.

## Provenance

This GitHub Release was backfilled on 2026-07-17. Every one of the 44 files in the published npm tarball matches commit `921fb308` byte-for-byte. The registry records the preceding `gitHead` because publication occurred about 34 seconds before the packaging commit was created; the historical tag therefore uses the content-matching commit.
docs/releases/v1.0.4.md
# Fireworks Tech Graph v1.0.4

This patch release documented a deterministic update path for existing Skill installations.

## Version information

- Version: `v1.0.4` / npm `1.0.4`
- Version date: 2026-04-12; npm published at `2026-04-12T13:32:37.231Z`
- Tag commit: [`c8668407`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/c8668407ebfa72e00719be8f4312b71ab90c3c3e), whose versioned content matches the npm tarball
- npm registry `gitHead`: [`c3726124`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/c37261248ca674d5d94e24d9d7d85e50a88c77d1)
- Previous version: [`v1.0.3`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/releases/tag/v1.0.3)
- Runtime metadata: Node.js 14 or newer
- Distribution: [npm package `1.0.4`](https://www.npmjs.com/package/@yizhiyanhua-ai/fireworks-tech-graph/v/1.0.4); this remains the npm registry's `latest` version for the legacy package line

## Highlights

- Added `npx skills add yizhiyanhua-ai/fireworks-tech-graph --force -g -y` as the documented update command.
- Synchronized the update workflow across the English README, Chinese README, and Skill instructions.

## Provenance

This GitHub Release was backfilled on 2026-07-17. npm `1.0.4` was published 11 seconds before the matching repository commit, so the registry retained the preceding `gitHead`. The historical tag uses the subsequent commit because its README, Chinese README, Skill instructions, and `package.json` match the published versioned content.
docs/releases/v1.0.5.md
# Fireworks Tech Graph v1.0.5

This repository release consolidated three months of diagram-quality work and made the Skill directly usable by both Codex and Claude Code.

## Version information

- Version: `v1.0.5`
- Version date: 2026-07-11
- Tag commit: [`14be3ad3`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/14be3ad3b05389a5d603562c207eb37157637127), the mainline merge of version commit [`55916c55`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/55916c552300a7475b8f8a57cc62c53e2034b016); both have the same tree
- Previous version: [`v1.0.4`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/releases/tag/v1.0.4)
- Runtime metadata: Node.js 14 or newer
- Distribution: repository version only; npm publishing stopped at `1.0.4`

## Highlights

- Added shared Codex and Claude Code metadata, instructions, compatibility tests, and installation paths.
- Added Style 8 Dark Luxury while preserving the seven original style contracts.
- Added a bounded generate-validate-repair loop, structural SVG checks, and optional visual self-review.
- Improved connector routing, label placement, overlap prevention, filter and viewport clipping guidance, and CJK font fallbacks.
- Switched the default PNG renderer to CairoSVG, hardened helper scripts, and documented renderer edge cases.
- Added the first GitHub Pages product showcase.

## Provenance

This GitHub Release was backfilled on 2026-07-17. Unlike npm versions `1.0.0` through `1.0.4`, the `v1.0.5` tag points to a committed mainline tree whose `package.json` declares `1.0.5`; no npm `1.0.5` package was published.
docs/releases/v1.2.0.md
# Fireworks Tech Graph v1.2.0

This release adds a focused, production-validated SVG-to-GIF workflow while preserving the twelve distinct diagram styles and their engineering scenarios.

## Version information

- Version: `v1.2.0`
- Version date: 2026-07-17
- Tag commit: [`ea534c2d`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/ea534c2d2817d43e4c02fe0ea9f67dfced6dece3)
- Previous version: [`v1.1.0`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/releases/tag/v1.1.0)
- Release channel: latest GitHub stable release; the npm registry remains on the legacy `1.0.4` package line.

## Highlights

- Adds user-approved semantic motion contracts for all 12 styles: connectors start absent, build in semantic order, then keep the completed topology alive with style-specific data flow.
- Makes the 5.75-second / 115-frame settled-flow timeline the default for natural-language GIF requests, while retaining explicit 3.75-second and 2.75-second compatibility timelines.
- Replaces every README showcase image with an approved GIF, including a synchronized 3×4 overview optimized below 2 MiB. Static 1920px PNGs remain available as regression baselines.
- Refreshes the GitHub Pages product site with the animated overview and all twelve per-style GIFs, while retaining static Open Graph media and reduced-motion fallbacks.
- Adds source-DOM immutability guards, bounded runtime probes, secure Puppeteer resolution, atomic GIF/report installation, FFprobe media checks, binary infinite-loop readback, and motion reports.
- Validates all 12 installed Skill styles in CI and before tag releases, including 852 Chromium compatibility comparisons across the extended timeline.
- Keeps motion reusable for valid title and content changes: the semantic topology, route roles, directions, stages, geometry, and style signature are pinned; the exact source SVG bytes are not.
- Includes the post-v1.1.0 SVG validator fixes for multi-subpath geometry and transformed bounds.

## Compatibility

- Python 3.9 or newer is required.
- Node.js 18 or newer is required for the optional Chromium motion renderer.
- GIF generation requires FFmpeg and `puppeteer-core@25.3.0`; static SVG/PNG/HTML workflows continue to work without them.
- The npm registry is a separate distribution channel and may lag the GitHub Release. Install the current Skill from `yizhiyanhua-ai/fireworks-tech-graph/skills/fireworks-tech-graph`.

See the [v1.2.0 changelog](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/blob/v1.2.0/CHANGELOG.md) for the full change list and the release assets below for checksum-verified archives.
examples/README.md
# Interactive example

Open `interactive-architecture.html` directly in a browser. It is a single offline file generated from `fixtures/api-flow-style7.json` and contains no remote runtime dependencies.

Regenerate it with:

```bash
tmp_dir="$(mktemp -d)"
trap 'rm -rf "$tmp_dir"' EXIT
tmp_svg="$tmp_dir/api-flow.svg"
python3 scripts/fireworks.py render architecture fixtures/api-flow-style7.json "$tmp_svg"
python3 scripts/fireworks.py export-html "$tmp_svg" examples/interactive-architecture.html --title "API Integration Flow" --slug api-integration-flow
```
docs/ROADMAP.md
# Roadmap

## Shipped in 1.1

- Versioned diagram IR and schemas
- Geometry-safe renderer and artifact audit
- Deterministic layout reports
- Complete Agent Skill installer mirror
- Unified CLI and offline interactive export
- CI, release archives, parity checks, and public install canary

## Next candidates

- Incremental layout for diagrams above 100 edges
- Pluggable text metrics using browser font measurement
- Interactive edge/node inspection panels
- Optional theme token packs without changing diagram semantics
- Visual-diff baselines for browser engines and platform fonts

Roadmap items remain proposals until they have tests, a compatibility plan, and a measured maintenance cost.
assets/samples/showcase-gif-manifest.json
{
  "schema_version": 1,
  "approved_at": "2026-07-17",
  "source_contract": "user-approved +2s-settled-flow",
  "approval": {
    "status": "user-approved",
    "style_count": 12,
    "full_size_frame_count": 1380,
    "compatibility_frames_accepted": 852,
    "compatibility_frames_total": 852
  },
  "overview": {
    "layout": "3x4",
    "tile_box": "400x320",
    "preview_fps": "12/1",
    "palette_colors": 160,
    "frame_delays_centiseconds": {
      "8": 46,
      "9": 23
    }
  },
  "assets": [
    {
      "file": "showcase-12-styles.gif",
      "sha256": "361c06f415e14915198e37302beb884c3e6d223f252c163e8d0c4956a8076b08",
      "bytes": 1020419,
      "width": 1200,
      "height": 1280,
      "fps": "12/1",
      "duration_seconds": 5.75,
      "frames": 69
    },
    {
      "file": "sample-style1-flat.gif",
      "sha256": "db22b38f0c54d4d77c464085c66e81558fcca1ece9d352fb17f96ff06244b25e",
      "bytes": 269337,
      "width": 960,
      "height": 680,
      "fps": "20/1",
      "duration_seconds": 5.75,
      "frames": 115
    },
    {
      "file": "sample-style2-dark.gif",
      "sha256": "54ff54b2694ae12b995f59f7bfac658d733298e72ac0238c948fc61e41dec9d4",
      "bytes": 388446,
      "width": 960,
      "height": 720,
      "fps": "20/1",
      "duration_seconds": 5.75,
      "frames": 115
    },
    {
      "file": "sample-style3-blueprint.gif",
      "sha256": "94405804ba16bd6dd1b6b77c0acee576e119ef80909037418cdd9851b66033cb",
      "bytes": 283449,
      "width": 960,
      "height": 720,
      "fps": "20/1",
      "duration_seconds": 5.75,
      "frames": 115
    },
    {
      "file": "sample-style4-notion.gif",
      "sha256": "ef74b2582e24b9aeda41dfc1cac00aa4e45754a2da83949615b5c01719af04f5",
      "bytes": 211403,
      "width": 960,
      "height": 620,
      "fps": "20/1",
      "duration_seconds": 5.75,
      "frames": 115
    },
    {
      "file": "sample-style5-glass.gif",
      "sha256": "c89b0278b3792b179941f4efda300690eb769783b35b2bd055705f73cdda5314",
      "bytes": 376688,
      "width": 960,
      "height": 700,
      "fps": "20/1",
      "duration_seconds": 5.75,
      "frames": 115
    },
    {
      "file": "sample-style6-claude.gif",
      "sha256": "390fda2e36af77f8fe798ed93136bb9874975cffa9eb91842aa2fa430d35e67b",
      "bytes": 246718,
      "width": 960,
      "height": 700,
      "fps": "20/1",
      "duration_seconds": 5.75,
      "frames": 115
    },
    {
      "file": "sample-style7-openai.gif",
      "sha256": "672f9f62d0800f25918db5c411014b1fbfdde64cd5cd700f2fcb16105d4cc97e",
      "bytes": 165225,
      "width": 960,
      "height": 700,
      "fps": "20/1",
      "duration_seconds": 5.75,
      "frames": 115
    },
    {
      "file": "sample-style8-dark-luxury.gif",
      "sha256": "186e28cc8a35ad0434558308562c7a2c743055e3b8bbc2b3865758af39080056",
      "bytes": 234825,
      "width": 960,
      "height": 600,
      "fps": "20/1",
      "duration_seconds": 5.75,
      "frames": 115
    },
    {
      "file": "sample-style9-c4-review-canvas.gif",
      "sha256": "ebc1d337a38d55ba2010191c90e534d94f394dd64205a36d3a205c78771c0b5e",
      "bytes": 205572,
      "width": 960,
      "height": 611,
      "fps": "20/1",
      "duration_seconds": 5.75,
      "frames": 115
    },
    {
      "file": "sample-style10-cloud-fabric.gif",
      "sha256": "d02fa0d67cd4fd621fe94635938de8b6066594b7060e08cf92bbcc008c54e29f",
      "bytes": 340823,
      "width": 960,
      "height": 760,
      "fps": "20/1",
      "duration_seconds": 5.75,
      "frames": 115
    },
    {
      "file": "sample-style11-event-transit.gif",
      "sha256": "f3995d0269732277b6e80f5f24849b26d710f9a6c34642e94c61de000cd08637",
      "bytes": 184537,
      "width": 960,
      "height": 590,
      "fps": "20/1",
      "duration_seconds": 5.75,
      "frames": 115
    },
    {
      "file": "sample-style12-ops-pulse.gif",
      "sha256": "26f5657f4430f337daec744a761bc27f8554f4708bd9a564bff198fc83507800",
      "bytes": 356902,
      "width": 960,
      "height": 860,
      "fps": "20/1",
      "duration_seconds": 5.75,
      "frames": 115
    }
  ]
}
.gitignore
# macOS
.DS_Store
.AppleDouble
.LSOverride

# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python

# Test outputs
test-output/
dist/

# Backup files
*.backup
*.bak
*~

# Temporary/broken files
*.tmp
*.temp
temlass.svg
agentloture.svg

# IDE
.vscode/
.idea/
*.swp
*.swo

# Node (if any)
node_modules/
package-lock.json
CHANGELOG.md
# Changelog

All notable changes use semantic versioning.

Historical `1.0.0` through `1.0.4` entries were reconstructed from the published npm artifacts and their registry-recorded `gitHead` values. See [`docs/releases/`](docs/releases/) for provenance details.

## 1.2.0 — 2026-07-17

- Added one focused motion contract: generated semantic SVG in, validated GIF plus a JSON motion report out. The renderer rejects raster inputs and unsupported media formats.
- Added twelve user-approved style signatures: headed memory packets, terminal evidence traces, blueprint registration beads, Notion memory cards, glass task capsules, governance seals, API token trains, gem tracers, C4 review cursors, active-active cloud chevrons, event trains, and an ops-pulse scanner.
- Changed the default GIF timeline to 5.75s/115 frames at 20fps. Routes construct on frames 1–36, live flow reaches full opacity at frame 38, settled flow continues through frame 109, and reset remains frames 110–114. Explicit 3.75s/75-frame and 2.75s/55-frame timelines remain supported.
- Replaced all 13 README showcase positions with approved GIFs, including a synchronized 3×4 overview below 2 MiB. Added a public SHA-256/metadata manifest and binary GIF parsing for dimensions, cadence, frame count, duration, and infinite-loop readback. Retained regenerated 1920px PNGs as static regression baselines.
- Updated the GitHub Pages product site to showcase the animated overview and all twelve style GIFs, with static social-card media and reduced-motion fallbacks retained.
- Added bounded runtime probes, secure Puppeteer resolution, source-DOM immutability guards, frame uniqueness and periodic-motion gates, FFprobe validation, atomic GIF/report installation, explicit encoder requirements, and a 500KB per-style guidance target.
- Kept valid same-style title/content variants animatable by pinning semantic topology, route roles, order, direction, stage, geometry, and style signatures instead of exact source bytes.
- Added a real-Chromium 12-style 75-vs-115 regression covering 852 comparisons, plus installed-copy all-style motion canaries in CI and the tag-release workflow.
- Fixed transformed SVG bounds to inspect all four corners and corrected multi-subpath bridge counting. Refreshed the Style 6 and Style 7 PNG baselines to match the canonical renderer.
- Updated Codex and Claude Code installation guidance so optional motion dependencies are installed into each copied Skill root.

## 1.1.0 — 2026-07-15

- Added four engineering-first styles: C4 Review Canvas, Cloud Fabric, Event Transit, and Ops Pulse, each with an executable semantic contract, distinct prompt fingerprints, and showcase fixtures.
- Added schema v1 normalization, typed node/edge IR, deterministic layout reports, and a unified CLI.
- Added geometry-safe orthogonal routing, mandatory waypoint validation, port fan-out, label/canvas checks, legend relocation, edge-crossing bridges, and semantic SVG metadata.
- Added strict SVG geometry validation and regression coverage for line crossings, reserved regions, clipping, and bridge masks.
- Added safe offline interactive HTML export with pan, zoom, theme switching, copy, and SVG/PNG/JPEG/WebP export up to 4×.
- Added a complete nested Agent Skill distribution for reliable `npx skills add` installation.
- Added CI, release archives, archive parity checks, project consistency checks, and isolated install canaries.
- Refreshed the official 12-style PNG showcase and product website with one distinct engineering scenario per style.
- Raised the supported Node.js runtime baseline to Node.js 18+.

## 1.0.5 — 2026-07-11

- Added shared Codex and Claude Code support, compatibility metadata, tests, and installation guidance.
- Added Style 8 Dark Luxury and expanded the gallery to eight distinct visual styles.
- Added a bounded generate-validate-repair loop, structural SVG validation, and optional visual self-review.
- Improved connector routing, label placement, overlap prevention, clipping guidance, CJK font fallbacks, and helper-script robustness.
- Switched the default PNG renderer to CairoSVG and documented renderer-specific edge cases.
- Added the first GitHub Pages product showcase.

## 1.0.4 — 2026-04-12

- Added an explicit `npx skills add ... --force -g -y` update workflow to the English README, Chinese README, and Skill instructions.
- Published npm package `1.0.4` with Node.js 14+ metadata.

## 1.0.3 — 2026-04-12

- Clarified that `skills add` installs from the GitHub repository while npm remains the package and distribution page.
- Replaced the legacy install example with `npx skills add yizhiyanhua-ai/fireworks-tech-graph`.
- Published npm package `1.0.3` with the corrected install-source guidance.

## 1.0.2 — 2026-04-12

- Added the style-driven generator, fixtures, reusable SVG templates, and a seven-style diagram matrix.
- Refreshed the seven official showcase images and expanded SVG validation and all-style test scripts.
- Added formal npm package metadata for repository, license, files, runtime, and discovery keywords.
- Expanded the npm package payload to include fixtures and templates.

## 1.0.1 — 2026-04-12

- Added an explicit layout validation checklist covering connector-node collisions, text overflow, endpoint alignment, and label backgrounds.
- Normalized npm repository metadata.

## 1.0.0 — 2026-04-12

- Published the first npm package for the Agent Skill.
- Shipped seven built-in diagram styles, SVG and PNG generation, architecture and UML guidance, validation scripts, and bilingual documentation.
docs/CAPABILITIES.md
# Capability contract

## Input

- Legacy JSON remains supported and normalizes to schema v1.
- Versioned input uses `schema_version: 1` and a `mode` matching the selected renderer.
- Duplicate IDs, dangling references, non-finite coordinates, malformed waypoints, and unknown schema versions fail before layout.
- Style-specific fields and unknown renderer extensions are preserved.
- `style` and `visual_theme` resolve through one catalog covering Styles 1–12; unknown or conflicting selectors fail closed.

## Engineering semantics

- Style 9 defaults to `c4-review`: one C4 level, typed elements, responsibilities, technologies, and protocols.
- Style 10 defaults to `cloud-fabric`: acyclic deployment boundaries, explicit workload ownership, versioned neutral icons, and named cross-boundary mechanisms.
- Style 11 defaults to `event-transit`: ordered topic rails, declared junctions, consumer groups, state projections, and real DLQ targets.
- Style 12 defaults to `ops-pulse`: exact golden signals, one contiguous business critical path, separate telemetry semantics, and a valid trace tree.
- `semantic_profile: "generic"` keeps any generator-backed visual theme available for internal topology regression without pretending domain semantics are present.

## Geometry

- All generated business edges are rectilinear outside declared bridge arcs.
- `route_points` are exact ordered waypoints; every leg is routed safely.
- Nodes, section headers, legends, title blocks, footers, labels, and the canvas boundary participate in layout checks.
- Distinct ports are allocated deterministically for shared node sides.
- Collinear edge overlap is fatal. Proper crossings receive a visible bridge and background mask.
- Layout output and reports are deterministic for the same input.

## Output

- SVG is the canonical artifact and carries `data-graph-role`, style, diagram-type, semantic-profile, semantic-role, edge-kind, topic, flow, station-order, status, critical-hop, and trace-timing metadata.
- PNG export uses CairoSVG or `rsvg-convert` in the shell workflow.
- Optional motion export has one media contract: a generated semantic SVG carrying one of the 12 approved role/stage/order scene contracts becomes a validated GIF plus its JSON verification report. Exact source bytes are not pinned, but unsupported same-style topologies fail closed. The user-approved 5.75s/115-frame default keeps construction at 1–36, live fade at 36–38, full settled flow at 38–109, and reset at 110–114. Styles 1–12 and the shared `+2s-settled-flow` timing revision retain `user-approved` status. Timelines through 75 frames remain all-unique. Longer timelines require at least 75 unique rasters and zero adjacent duplicates. Repeats stay inside the full-opacity interval except frame 110, the sole `intentional_reset_boundary_repeat` allowed by its unchanged 1.00 reset opacity; frames 111–114 remain globally distinct. The 75-vs-115 compatibility report separates binary-exact, decoded-RGBA-exact, and guarded antialias-equivalent counts. The guarded tier requires AE ≤ 128, normalized RMSE ≤ 0.001, components no thicker than 2px, and edge/node-border-only scope while DOM and signature geometry remain strict-exact. Direction/source-DOM guards, FFprobe validation, infinite looping, and atomic GIF/report installation remain mandatory.
- The single-SVG offline HTML viewer remains independent of motion and supports pan, zoom, reset, themes, copy, and static SVG/PNG/JPEG/WebP export.
- Interactive export rejects active elements, event handlers, external references, `foreignObject`, and external CSS.

## Distribution

- Git clone and npm archives contain the complete skill.
- The committed `skills/fireworks-tech-graph/` mirror supports deterministic `npx skills add` subpath installation for Codex and Claude Code.
- npm `.tgz` and GitHub release `.zip` are built from the same payload and checked by file hash.
examples/interactive-architecture.html
<!doctype html>
<html lang="en" data-theme="dark">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; img-src data: blob:">
  <title>API Integration Flow</title>
  <style>
    :root { color-scheme: dark; --page:#0f0f1a; --panel:#0f172a; --border:#334155; --text:#e2e8f0; --muted:#94a3b8; --accent:#a855f7; }
    html[data-theme="light"] { color-scheme:light; --page:#f8fafc; --panel:#fff; --border:#cbd5e1; --text:#0f172a; --muted:#475569; --accent:#7c3aed; }
    * { box-sizing:border-box; }
    html,body { width:100%; height:100%; margin:0; overflow:hidden; background:linear-gradient(135deg,var(--page),#1a1a2e); color:var(--text); font-family:'SF Mono','Fira Code',ui-monospace,monospace; }
    body { display:grid; grid-template-rows:auto 1fr; }
    .toolbar { display:flex; flex-wrap:wrap; align-items:center; gap:8px; padding:10px 14px; background:color-mix(in srgb,var(--panel) 92%,transparent); border-bottom:1px solid var(--border); z-index:2; }
    .title { margin-right:auto; font-size:13px; font-weight:700; }
    button,select { min-height:34px; padding:6px 10px; color:var(--text); background:var(--panel); border:1px solid var(--border); border-radius:8px; font:inherit; cursor:pointer; }
    button:hover,button:focus-visible,select:focus-visible { border-color:var(--accent); outline:2px solid color-mix(in srgb,var(--accent) 35%,transparent); outline-offset:1px; }
    #stage { position:relative; overflow:hidden; touch-action:none; cursor:grab; }
    #stage.dragging { cursor:grabbing; }
    #canvas { width:100%; height:100%; display:grid; place-items:center; transform-origin:0 0; will-change:transform; }
    #canvas svg { max-width:calc(100vw - 40px); max-height:calc(100vh - 92px); width:auto; height:auto; filter:drop-shadow(0 22px 60px rgba(0,0,0,.28)); user-select:none; }
    #status { min-width:76px; color:var(--muted); font-size:12px; text-align:center; }
    .sr-only { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; }
    @media (prefers-reduced-motion:reduce) { * { scroll-behavior:auto!important; transition:none!important; } }
  </style>
</head>
<body>
  <header class="toolbar" aria-label="Diagram controls">
    <span class="title">API Integration Flow</span>
    <button type="button" data-action="zoom-out" aria-label="Zoom out">−</button>
    <button type="button" data-action="reset" aria-label="Reset view">Reset</button>
    <button type="button" data-action="zoom-in" aria-label="Zoom in">+</button>
    <span id="status" aria-live="polite">100%</span>
    <button type="button" data-action="theme" aria-label="Toggle theme">Theme</button>
    <button type="button" data-action="copy" aria-label="Copy SVG source">Copy SVG</button>
    <select id="scale" aria-label="Raster export scale"><option value="1">1×</option><option value="2" selected>2×</option><option value="3">3×</option><option value="4">4×</option></select>
    <select id="format" aria-label="Export format"><option>SVG</option><option>PNG</option><option>JPEG</option><option>WebP</option></select>
    <button type="button" data-action="download">Export</button>
  </header>
  <main id="stage" tabindex="0" aria-label="Interactive diagram. Drag to pan; use plus and minus to zoom.">
    <div id="canvas"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 700" width="960" height="700" data-generator="fireworks-tech-graph" data-schema-version="1" data-text-metrics="heuristic-v1" data-style-id="7" data-visual-theme="OpenAI" data-diagram-type="architecture" data-motion-scene="token-stream" data-semantic-profile="generic" data-semantic-valid="true" data-quality-profile="showcase" data-max-bends-per-edge="2" data-max-total-bends="8" data-max-route-stretch="1.35" data-max-bridged-crossings="0" data-min-node-gap="40.0" data-min-container-gutter="20.0" data-min-label-clearance="4.0" data-min-segment-length="16.0" role="img" focusable="false">
  <defs>
    <marker id="arrowA" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
      <polygon points="0 0, 10 3.5, 0 7" fill="#10a37f" />
    </marker>
    <marker id="arrowB" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
      <polygon points="0 0, 10 3.5, 0 7" fill="#0f766e" />
    </marker>
    <marker id="arrowC" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
      <polygon points="0 0, 10 3.5, 0 7" fill="#0891b2" />
    </marker>
    <marker id="arrowE" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
      <polygon points="0 0, 10 3.5, 0 7" fill="#f59e0b" />
    </marker>
    <marker id="arrowF" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
      <polygon points="0 0, 10 3.5, 0 7" fill="#64748b" />
    </marker>
    <marker id="arrowG" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
      <polygon points="0 0, 10 3.5, 0 7" fill="#475569" />
    </marker>
    <marker id="arrowH" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
      <polygon points="0 0, 10 3.5, 0 7" fill="#94a3b8" />
    </marker>
    <style>
    text { font-family: 'Helvetica Neue', Helvetica, Arial, 'PingFang SC', 'Microsoft YaHei', 'Microsoft JhengHei', 'SimHei', sans-serif; }
    .title { font-size: 24px; font-weight: 700; fill: #0f172a; }
    .subtitle { font-size: 13px; font-weight: 500; fill: #64748b; }
    .section { font-size: 13px; font-weight: 700; fill: #10a37f; letter-spacing: 1.4px; }
    .section-sub { font-size: 12px; font-weight: 500; fill: #94a3b8; }
    .node-title { font-weight: 700; fill: #0f172a; }
    .node-sub { font-size: 12px; font-weight: 500; fill: #475569; }
    .node-type { font-size: 11px; font-weight: 700; fill: #94a3b8; letter-spacing: 0.08em; }
    .arrow-label { font-size: 12px; font-weight: 600; fill: #475569; }
    .legend { font-size: 12px; font-weight: 500; fill: #475569; }
    .footnote { font-size: 12px; font-weight: 500; fill: #94a3b8; }
    .metric-label { font-size: 8.5px; font-weight: 700; fill: #94a3b8; text-transform: uppercase; }
    .metric-value { font-size: 9.5px; font-weight: 700; fill: #0f172a; }
    </style>
  </defs>
  <rect data-graph-role="background" width="960.0" height="700.0" fill="#ffffff" />
  <text x="48.0" y="48" text-anchor="start" class="title">API Integration Flow</text>
  <text x="48.0" y="72" text-anchor="start" class="subtitle">clean application integration from SDK input to governed delivery</text>
  <line x1="48" y1="100" x2="912.0" y2="100" stroke="#e2e8f0" stroke-width="1" />
  <g id="entry" data-graph-role="container" data-container-id="entry" data-semantic-role="boundary" data-graph-bounds="40,120,920,230">
  <rect data-graph-role="container" x="40.0" y="120.0" width="880.0" height="110.0" rx="16.0" fill="none" stroke="#e2e8f0" stroke-width="1.4" stroke-dasharray="5 4" />
  <text x="58.0" y="144.0" class="section">INTEGRATION</text>
    <rect id="entry-header" data-graph-role="reserved" data-reserved-kind="container-header" x="48" y="126" width="106.18" height="30" fill="none" stroke="none" />
  </g>
  <g id="runtime" data-graph-role="container" data-container-id="runtime" data-semantic-role="boundary" data-graph-bounds="40,280,920,410">
  <rect data-graph-role="container" x="40.0" y="280.0" width="880.0" height="130.0" rx="16.0" fill="none" stroke="#e2e8f0" stroke-width="1.4" stroke-dasharray="5 4" />
  <text x="58.0" y="304.0" class="section">MODEL + TOOLS</text>
    <rect id="runtime-header" data-graph-role="reserved" data-reserved-kind="container-header" x="48" y="286" width="125.42" height="30" fill="none" stroke="none" />
  </g>
  <g id="delivery" data-graph-role="container" data-container-id="delivery" data-semantic-role="boundary" data-graph-bounds="40,450,920,590">
  <rect data-graph-role="container" x="40.0" y="450.0" width="880.0" height="140.0" rx="16.0" fill="none" stroke="#e2e8f0" stroke-width="1.4" stroke-dasharray="5 4" />
  <text x="58.0" y="474.0" class="section">DELIVERY</text>
    <rect id="delivery-header" data-graph-role="reserved" data-reserved-kind="container-header" x="48" y="456" width="86.94" height="30" fill="none" stroke="none" />
  </g>
  <path id="connect" data-graph-role="edge" data-edge-id="connect" data-source="app" data-target="sdk" data-edge-kind="flow" data-topic-id="" data-flow="control" data-motion-role="connect" data-motion-stage="1" data-motion-order="0" data-protocol="" data-via="" data-critical-path-id="" data-critical-hop="" data-critical-hops="" data-critical="false" data-bends="0" data-route-stretch="1.0" data-bridges="" d="M 260,183 L 370,183" fill="none" stroke="#10a37f" stroke-width="2.0" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrowA)" />
  <path id="prepare" data-graph-role="edge" data-edge-id="prepare" data-source="sdk" data-target="prompt" data-edge-kind="flow" data-topic-id="" data-flow="read" data-motion-role="prepare" data-motion-stage="2" data-motion-order="0" data-protocol="" data-via="" data-critical-path-id="" data-critical-hop="" data-critical-hops="" data-critical="false" data-bends="2" data-route-stretch="1.0" data-bridges="" d="M 480,210 L 480,316 L 170,316 L 170,340" fill="none" stroke="#0891b2" stroke-width="2.0" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrowC)" />
  <path id="invoke" data-graph-role="edge" data-edge-id="invoke" data-source="prompt" data-target="model" data-edge-kind="flow" data-topic-id="" data-flow="control" data-motion-role="invoke" data-motion-stage="3" data-motion-order="0" data-protocol="" data-via="" data-critical-path-id="" data-critical-hop="" data-critical-hops="" data-critical="false" data-bends="0" data-route-stretch="1.0" data-bridges="" d="M 260,365 L 370,365" fill="none" stroke="#10a37f" stroke-width="2.0" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrowA)" />
  <path id="tool-call" data-graph-role="edge" data-edge-id="tool-call" data-source="model" data-target="tools" data-edge-kind="flow" data-topic-id="" data-flow="read" data-motion-role="tool-call" data-motion-stage="4" data-motion-order="0" data-protocol="" data-via="" data-critical-path-id="" data-critical-hop="" data-critical-hops="" data-critical="false" data-bends="0" data-route-stretch="1.0" data-bridges="" d="M 590,365 L 700,365" fill="none" stroke="#0891b2" stroke-width="2.0" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrowC)" />
  <path id="stream" data-graph-role="edge" data-edge-id="stream" data-source="model" data-target="formatter" data-edge-kind="flow" data-topic-id="" data-flow="control" data-motion-role="token-stream" data-motion-stage="4" data-motion-order="1" data-protocol="" data-via="" data-critical-path-id="" data-critical-hop="" data-critical-hops="" data-critical="false" data-bends="2" data-route-stretch="1.0" data-bridges="" d="M 480,390 L 480,414 L 170,414 L 170,510" fill="none" stroke="#10a37f" stroke-width="2.0" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrowA)" />
  <path id="measure" data-graph-role="edge" data-edge-id="measure" data-source="formatter" data-target="observability" data-edge-kind="flow" data-topic-id="" data-flow="feedback" data-motion-role="measure" data-motion-stage="5" data-motion-order="1" data-protocol="" data-via="" data-critical-path-id="" data-critical-hop="" data-critical-hops="" data-critical="false" data-bends="0" data-route-stretch="1.0" data-bridges="" d="M 260,540 L 390,540" fill="none" stroke="#475569" stroke-width="2.0" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrowG)" />
  <path id="govern" data-graph-role="edge" data-edge-id="govern" data-source="tools" data-target="release" data-edge-kind="flow" data-topic-id="" data-flow="feedback" data-motion-role="govern" data-motion-stage="5" data-motion-order="0" data-protocol="" data-via="" data-critical-path-id="" data-critical-hop="" data-critical-hops="" data-critical="false" data-bends="0" data-route-stretch="1.0" data-bridges="" d="M 790,390 L 790,510" fill="none" stroke="#475569" stroke-width="2.0" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrowG)" />
  <path id="promote" data-graph-role="edge" data-edge-id="promote" data-source="observability" data-target="release" data-edge-kind="flow" data-topic-id="" data-flow="feedback" data-motion-role="promote" data-motion-stage="6" data-motion-order="0" data-protocol="" data-via="" data-critical-path-id="" data-critical-hop="" data-critical-hops="" data-critical="false" data-bends="0" data-route-stretch="1.0" data-bridges="" d="M 570,540 L 700,540" fill="none" stroke="#475569" stroke-width="2.0" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrowG)" />
  <g id="node-app" data-graph-role="node" data-node-id="app" data-semantic-role="rect" data-motion-role="" data-motion-stage="" data-motion-order="" data-parent="" data-deployment-id="" data-topic-id="" data-span-id="" data-station-order="" data-status="" data-start-ms="" data-duration-ms="" data-parent-span="" data-graph-bounds="80,156,260,210">
  <rect x="80.0" y="156.0" width="180.0" height="54.0" rx="14.0" fill="#ffffff" stroke="#dce5e3" stroke-width="1.8" />
  <text x="170.0" y="174.0" text-anchor="middle" class="node-type">CLIENT</text>
  <text x="170.0" y="189.0" text-anchor="middle" class="node-title" font-size="18.0">Application</text>
  </g>
  <g id="node-sdk" data-graph-role="node" data-node-id="sdk" data-semantic-role="double_rect" data-motion-role="" data-motion-stage="" data-motion-order="" data-parent="" data-deployment-id="" data-topic-id="" data-span-id="" data-station-order="" data-status="" data-start-ms="" data-duration-ms="" data-parent-span="" data-graph-bounds="370,156,590,210">
  <rect x="370.0" y="156.0" width="220.0" height="54.0" rx="14.0" fill="#ffffff" stroke="#10a37f" stroke-width="2.0" />
  <rect x="376.0" y="162.0" width="208.0" height="42.0" rx="11.0" fill="none" stroke="#10a37f" stroke-width="1.2" opacity="0.65" />
  <text x="480.0" y="174.0" text-anchor="middle" class="node-type">SDK</text>
  <text x="480.0" y="189.0" text-anchor="middle" class="node-title" font-size="18.0">OpenAI SDK Layer</text>
  </g>
  <g id="node-prompt" data-graph-role="node" data-node-id="prompt" data-semantic-role="rect" data-motion-role="" data-motion-stage="" data-motion-order="" data-parent="" data-deployment-id="" data-topic-id="" data-span-id="" data-station-order="" data-status="" data-start-ms="" data-duration-ms="" data-parent-span="" data-graph-bounds="80,340,260,390">
  <rect x="80.0" y="340.0" width="180.0" height="50.0" rx="14.0" fill="#ffffff" stroke="#dce5e3" stroke-width="1.8" />
  <text x="170.0" y="358.0" text-anchor="middle" class="node-type">INPUT</text>
  <text x="170.0" y="371.0" text-anchor="middle" class="node-title" font-size="18.0">Prompt Builder</text>
  </g>
  <g id="node-model" data-graph-role="node" data-node-id="model" data-semantic-role="double_rect" data-motion-role="" data-motion-stage="" data-motion-order="" data-parent="" data-deployment-id="" data-topic-id="" data-span-id="" data-station-order="" data-status="" data-start-ms="" data-duration-ms="" data-parent-span="" data-graph-bounds="370,340,590,390">
  <rect x="370.0" y="340.0" width="220.0" height="50.0" rx="14.0" fill="#ffffff" stroke="#10a37f" stroke-width="2.0" />
  <rect x="376.0" y="346.0" width="208.0" height="38.0" rx="11.0" fill="none" stroke="#10a37f" stroke-width="1.2" opacity="0.65" />
  <text x="480.0" y="358.0" text-anchor="middle" class="node-type">REASONING</text>
  <text x="480.0" y="371.0" text-anchor="middle" class="node-title" font-size="18.0">Model Runtime</text>
  </g>
  <g id="node-tools" data-graph-role="node" data-node-id="tools" data-semantic-role="rect" data-motion-role="" data-motion-stage="" data-motion-order="" data-parent="" data-deployment-id="" data-topic-id="" data-span-id="" data-station-order="" data-status="" data-start-ms="" data-duration-ms="" data-parent-span="" data-graph-bounds="700,340,880,390">
  <rect x="700.0" y="340.0" width="180.0" height="50.0" rx="14.0" fill="#ffffff" stroke="#dce5e3" stroke-width="1.8" />
  <text x="790.0" y="358.0" text-anchor="middle" class="node-type">ACTIONS</text>
  <text x="790.0" y="371.0" text-anchor="middle" class="node-title" font-size="18.0">Tool Calls</text>
  </g>
  <g id="node-formatter" data-graph-role="node" data-node-id="formatter" data-semantic-role="rect" data-motion-role="" data-motion-stage="" data-motion-order="" data-parent="" data-deployment-id="" data-topic-id="" data-span-id="" data-station-order="" data-status="" data-start-ms="" data-duration-ms="" data-parent-span="" data-graph-bounds="80,510,260,570">
  <rect x="80.0" y="510.0" width="180.0" height="60.0" rx="14.0" fill="#ffffff" stroke="#dce5e3" stroke-width="1.8" />
  <text x="170.0" y="528.0" text-anchor="middle" class="node-type">OUTPUT</text>
  <text x="170.0" y="546.0" text-anchor="middle" class="node-title" font-size="14.13">Response Formatter</text>
  </g>
  <g id="node-observability" data-graph-role="node" data-node-id="observability" data-semantic-role="rect" data-motion-role="" data-motion-stage="" data-motion-order="" data-parent="" data-deployment-id="" data-topic-id="" data-span-id="" data-station-order="" data-status="" data-start-ms="" data-duration-ms="" data-parent-span="" data-graph-bounds="390,510,570,570">
  <rect x="390.0" y="510.0" width="180.0" height="60.0" rx="14.0" fill="#ffffff" stroke="#dce5e3" stroke-width="1.8" />
  <text x="480.0" y="528.0" text-anchor="middle" class="node-type">METRICS</text>
  <text x="480.0" y="546.0" text-anchor="middle" class="node-title" font-size="18.0">Observability</text>
  </g>
  <g id="node-release" data-graph-role="node" data-node-id="release" data-semantic-role="rect" data-motion-role="" data-motion-stage="" data-motion-order="" data-parent="" data-deployment-id="" data-topic-id="" data-span-id="" data-station-order="" data-status="" data-start-ms="" data-duration-ms="" data-parent-span="" data-graph-bounds="700,510,880,570">
  <rect x="700.0" y="510.0" width="180.0" height="60.0" rx="14.0" fill="#ffffff" stroke="#dce5e3" stroke-width="1.8" />
  <text x="790.0" y="528.0" text-anchor="middle" class="node-type">CONFIG</text>
  <text x="790.0" y="546.0" text-anchor="middle" class="node-title" font-size="18.0">Release Control</text>
  </g>
  <g id="connect-label" data-graph-role="label" data-owner="connect" data-graph-bounds="283.64,152,346.36,172">
  <rect x="283.64" y="152.0" width="62.72" height="20" rx="6" fill="#ffffff" opacity="0.96" />
  <text x="315.0" y="166.0" text-anchor="middle" class="arrow-label">connect</text>
  </g>
  <g id="prepare-label" data-graph-role="label" data-owner="prepare" data-graph-bounds="293.64,285,356.36,305">
  <rect x="293.64" y="285.0" width="62.72" height="20" rx="6" fill="#ffffff" opacity="0.96" />
  <text x="325.0" y="299.0" text-anchor="middle" class="arrow-label">prepare</text>
  </g>
  <g id="invoke-label" data-graph-role="label" data-owner="invoke" data-graph-bounds="288.68,334,341.32,354">
  <rect x="288.68" y="334.0" width="52.64" height="20" rx="6" fill="#ffffff" opacity="0.96" />
  <text x="315.0" y="348.0" text-anchor="middle" class="arrow-label">invoke</text>
  </g>
  <g id="tool-call-label" data-graph-role="label" data-owner="tool-call" data-graph-bounds="612.68,334,677.32,354">
  <rect x="612.68" y="334.0" width="64.64" height="20" rx="6" fill="#ffffff" opacity="0.96" />
  <text x="645.0" y="348.0" text-anchor="middle" class="arrow-label">tool call</text>
  </g>
  <g id="stream-label" data-graph-role="label" data-owner="stream" data-graph-bounds="297.12,383,352.88,403">
  <rect x="297.12" y="383.0" width="55.76" height="20" rx="6" fill="#ffffff" opacity="0.96" />
  <text x="325.0" y="397.0" text-anchor="middle" class="arrow-label">stream</text>
  </g>
  <g id="measure-label" data-graph-role="label" data-owner="measure" data-graph-bounds="293.64,509,356.36,529">
  <rect x="293.64" y="509.0" width="62.72" height="20" rx="6" fill="#ffffff" opacity="0.96" />
  <text x="325.0" y="523.0" text-anchor="middle" class="arrow-label">measure</text>
  </g>
  <g id="govern-label" data-graph-role="label" data-owner="govern" data-graph-bounds="731.24,436,787,456">
  <rect x="731.24" y="436.0" width="55.76" height="20" rx="6" fill="#ffffff" opacity="0.96" />
  <text x="759.12" y="450.0" text-anchor="middle" class="arrow-label">govern</text>
  </g>
  <g id="promote-label" data-graph-role="label" data-owner="promote" data-graph-bounds="603.64,509,666.36,529">
  <rect x="603.64" y="509.0" width="62.72" height="20" rx="6" fill="#ffffff" opacity="0.96" />
  <text x="635.0" y="523.0" text-anchor="middle" class="arrow-label">promote</text>
  </g>
  <g id="legend" data-graph-role="legend">
    <rect id="legend-zone" data-graph-role="reserved" data-reserved-kind="legend" x="38" y="611" width="464.48" height="46" rx="10" fill="none" stroke="none" />
    <line data-graph-role="decoration" x1="48.0" y1="625.0" x2="78.0" y2="625.0" stroke="#10a37f" stroke-width="2.0" marker-end="url(#arrowA)" />
    <text data-graph-role="decoration" x="88.0" y="629.0" class="legend">primary API path</text>
    <line data-graph-role="decoration" x1="215.84" y1="625.0" x2="245.84" y2="625.0" stroke="#0891b2" stroke-width="2.0" marker-end="url(#arrowC)" />
    <text data-graph-role="decoration" x="255.84" y="629.0" class="legend">prompt / tools</text>
    <line data-graph-role="decoration" x1="372.88" y1="625.0" x2="402.88" y2="625.0" stroke="#475569" stroke-width="2.0" marker-end="url(#arrowG)" />
    <text data-graph-role="decoration" x="412.88" y="629.0" class="legend">governance</text>
  </g>
  <g id="footer" data-graph-role="reserved" data-graph-bounds="48,668,426,684">
  <text x="48.0" y="680.0" class="footnote">Style 7 · OpenAI Official · precise integration stages</text>
  </g>
</svg></div>
    <p class="sr-only">Keyboard shortcuts: plus and minus zoom, zero resets, T toggles theme, S exports.</p>
  </main>
  <script>
  (() => {
    'use strict';
    const metadata = {"slug": "api-integration-flow"};
    const stage = document.getElementById('stage');
    const canvas = document.getElementById('canvas');
    const svg = canvas.querySelector('svg');
    const status = document.getElementById('status');
    const scaleSelect = document.getElementById('scale');
    const formatSelect = document.getElementById('format');
    let view = { x:0, y:0, scale:1 };
    let drag = null;
    const clamp = value => Math.max(.2, Math.min(8, value));
    const render = () => {
      canvas.style.transform = `translate(${view.x}px,${view.y}px) scale(${view.scale})`;
      status.textContent = `${Math.round(view.scale * 100)}%`;
    };
    const zoom = (factor, originX=stage.clientWidth/2, originY=stage.clientHeight/2) => {
      const next = clamp(view.scale * factor);
      const ratio = next / view.scale;
      view.x = originX - (originX - view.x) * ratio;
      view.y = originY - (originY - view.y) * ratio;
      view.scale = next; render();
    };
    const reset = () => { view = {x:0,y:0,scale:1}; render(); };
    stage.addEventListener('wheel', event => { event.preventDefault(); const rect=stage.getBoundingClientRect(); zoom(event.deltaY < 0 ? 1.12 : .89, event.clientX-rect.left, event.clientY-rect.top); }, {passive:false});
    stage.addEventListener('pointerdown', event => { drag={id:event.pointerId,x:event.clientX,y:event.clientY,vx:view.x,vy:view.y}; stage.setPointerCapture(event.pointerId); stage.classList.add('dragging'); });
    stage.addEventListener('pointermove', event => { if(!drag||drag.id!==event.pointerId)return; view.x=drag.vx+event.clientX-drag.x; view.y=drag.vy+event.clientY-drag.y; render(); });
    const endDrag = () => { drag=null; stage.classList.remove('dragging'); };
    stage.addEventListener('pointerup', endDrag); stage.addEventListener('pointercancel', endDrag);
    const source = () => new XMLSerializer().serializeToString(svg);
    const saveBlob = (blob, extension) => { const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download=`${(metadata.slug||'fireworks-tech-graph')}.${extension}`; a.click(); setTimeout(()=>URL.revokeObjectURL(a.href),1000); };
    const copySource = async () => {
      const value=source();
      try { await navigator.clipboard.writeText(value); status.textContent='Copied'; return; } catch {}
      const area=document.createElement('textarea'); area.value=value; area.setAttribute('readonly',''); area.style.position='fixed'; area.style.opacity='0'; document.body.appendChild(area); area.select();
      status.textContent=document.execCommand('copy')?'Copied':'Copy failed'; area.remove();
    };
    const exportDiagram = async () => {
      const format = formatSelect.value.toLowerCase();
      if(format==='svg') { saveBlob(new Blob([source()],{type:'image/svg+xml;charset=utf-8'}),'svg'); return; }
      const scale = Math.max(1,Math.min(4,Number(scaleSelect.value)||2));
      const box = svg.viewBox.baseVal; const width=box.width||svg.clientWidth; const height=box.height||svg.clientHeight;
      const image = new Image(); const url=URL.createObjectURL(new Blob([source()],{type:'image/svg+xml'}));
      await new Promise((resolve,reject)=>{ image.onload=resolve; image.onerror=reject; image.src=url; });
      const raster=document.createElement('canvas'); raster.width=Math.round(width*scale); raster.height=Math.round(height*scale);
      const ctx=raster.getContext('2d'); if(format==='jpeg'){ctx.fillStyle='#ffffff';ctx.fillRect(0,0,raster.width,raster.height);} ctx.drawImage(image,0,0,raster.width,raster.height); URL.revokeObjectURL(url);
      const mime=format==='jpeg'?'image/jpeg':format==='webp'?'image/webp':'image/png';
      const blob=await new Promise(resolve=>raster.toBlob(resolve,mime,.94)); if(blob) saveBlob(blob,format==='jpeg'?'jpg':format);
    };
    document.querySelector('.toolbar').addEventListener('click', async event => {
      const action=event.target.closest('[data-action]')?.dataset.action; if(!action)return;
      if(action==='zoom-in')zoom(1.2); else if(action==='zoom-out')zoom(.83); else if(action==='reset')reset();
      else if(action==='theme')document.documentElement.dataset.theme=document.documentElement.dataset.theme==='dark'?'light':'dark';
      else if(action==='download')await exportDiagram();
      else if(action==='copy') await copySource();
    });
    document.addEventListener('keydown', event => {
      if(event.target.matches('select'))return;
      if(event.key==='+'||event.key==='=')zoom(1.2); else if(event.key==='-')zoom(.83); else if(event.key==='0'||event.key.toLowerCase()==='r')reset();
      else if(event.key.toLowerCase()==='t')document.querySelector('[data-action=theme]').click(); else if(event.key.toLowerCase()==='s'){event.preventDefault();exportDiagram();}
    });
    render();
  })();
  </script>
</body>
</html>
fixtures/agent-memory-types-style4.json
{
  "schema_version": 1,
  "mode": "memory",
  "template_type": "memory",
  "style": 4,
  "motion_scene": "memory-lifecycle",
  "quality_profile": "showcase",
  "width": 960,
  "height": 620,
  "title": "Agent Memory Types",
  "subtitle": "from active perception to durable knowledge and reusable procedures",
  "containers": [
    { "id": "active-context", "x": 40, "y": 120, "width": 880, "height": 165, "label": "Active Context" },
    { "id": "long-term", "x": 40, "y": 335, "width": 880, "height": 185, "label": "Long-term Memory" }
  ],
  "nodes": [
    {
      "id": "sensory", "kind": "rect", "x": 60, "y": 165, "width": 160, "height": 90,
      "type_label": "SENSORY MEMORY", "label": "Raw Input",
      "fill": "#ffffff", "stroke": "#e5e7eb", "flat": true,
      "tags": [{ "label": "RAM / context", "fill": "#eff6ff", "stroke": "#bfdbfe", "text_fill": "#3b82f6" }]
    },
    {
      "id": "working", "kind": "rect", "x": 270, "y": 165, "width": 160, "height": 90,
      "type_label": "WORKING MEMORY", "label": "Active Context",
      "fill": "#ffffff", "stroke": "#e5e7eb", "flat": true,
      "tags": [{ "label": "Context Window", "fill": "#eff6ff", "stroke": "#bfdbfe", "text_fill": "#3b82f6" }]
    },
    {
      "id": "agent", "kind": "hexagon", "x": 490, "y": 165, "width": 160, "height": 90,
      "type_label": "AGENT CORE", "label": "LLM + Planner", "sublabel": "reason + decide",
      "fill": "#ffffff", "stroke": "#3b82f6", "stroke_width": 1.6, "flat": true
    },
    {
      "id": "procedural", "kind": "rect", "x": 700, "y": 165, "width": 180, "height": 90,
      "type_label": "PROCEDURAL MEMORY", "label": "Skills + How-to",
      "fill": "#ffffff", "stroke": "#e5e7eb", "flat": true,
      "tags": [{ "label": "Code / Skills", "fill": "#f5f3ff", "stroke": "#ddd6fe", "text_fill": "#7c3aed" }]
    },
    {
      "id": "episodic", "kind": "rect", "x": 270, "y": 400, "width": 160, "height": 90,
      "type_label": "EPISODIC MEMORY", "label": "Past Interactions",
      "fill": "#ffffff", "stroke": "#e5e7eb", "flat": true,
      "tags": [{ "label": "Vector DB + time", "fill": "#ecfdf5", "stroke": "#a7f3d0", "text_fill": "#059669" }]
    },
    {
      "id": "semantic", "kind": "rect", "x": 490, "y": 400, "width": 160, "height": 90,
      "type_label": "SEMANTIC MEMORY", "label": "Facts + Knowledge",
      "fill": "#ffffff", "stroke": "#e5e7eb", "flat": true,
      "tags": [{ "label": "Vector + Graph", "fill": "#fff7ed", "stroke": "#fed7aa", "text_fill": "#ea580c" }]
    }
  ],
  "arrows": [
    { "id": "sample", "source": "sensory", "target": "working", "source_port": "right", "target_port": "left", "flow": "read", "motion_role": "sample", "motion_stage": 1, "motion_order": 0 },
    { "id": "attend", "source": "working", "target": "agent", "source_port": "right", "target_port": "left", "flow": "control", "motion_role": "attend", "motion_stage": 2, "motion_order": 0 },
    { "id": "invoke", "source": "agent", "target": "procedural", "source_port": "right", "target_port": "left", "flow": "control", "motion_role": "invoke", "motion_stage": 3, "motion_order": 0 },
    { "id": "remember", "source": "working", "target": "episodic", "source_port": "bottom", "target_port": "top", "flow": "write", "motion_role": "remember", "motion_stage": 4, "motion_order": 0, "label": "remember" },
    { "id": "recall", "source": "semantic", "target": "agent", "source_port": "top", "target_port": "bottom", "flow": "read", "motion_role": "recall", "motion_stage": 6, "motion_order": 0, "label": "recall" },
    { "id": "consolidate", "source": "episodic", "target": "semantic", "source_port": "right", "target_port": "left", "flow": "write", "motion_role": "consolidate", "motion_stage": 5, "motion_order": 0 }
  ],
  "footer": "Style 4 · Notion Clean · five complementary memory systems",
  "footer_x": 48,
  "footer_y": 590
}
fixtures/api-flow-style7.json
{
  "schema_version": 1,
  "mode": "architecture",
  "template_type": "architecture",
  "style": 7,
  "motion_scene": "token-stream",
  "quality_profile": "showcase",
  "width": 960,
  "height": 700,
  "title": "API Integration Flow",
  "subtitle": "clean application integration from SDK input to governed delivery",
  "containers": [
    { "id": "entry", "x": 40, "y": 120, "width": 880, "height": 110, "label": "Integration", "stroke": "#e2e8f0", "fill": "none" },
    { "id": "runtime", "x": 40, "y": 280, "width": 880, "height": 130, "label": "Model + Tools", "stroke": "#e2e8f0", "fill": "none" },
    { "id": "delivery", "x": 40, "y": 450, "width": 880, "height": 140, "label": "Delivery", "stroke": "#e2e8f0", "fill": "none" }
  ],
  "nodes": [
    { "id": "app", "kind": "rect", "x": 80, "y": 156, "width": 180, "height": 54, "type_label": "CLIENT", "label": "Application", "fill": "#ffffff", "stroke": "#dce5e3", "flat": true },
    { "id": "sdk", "kind": "double_rect", "x": 370, "y": 156, "width": 220, "height": 54, "type_label": "SDK", "label": "OpenAI SDK Layer", "fill": "#ffffff", "stroke": "#10a37f" },
    { "id": "prompt", "kind": "rect", "x": 80, "y": 340, "width": 180, "height": 50, "type_label": "INPUT", "label": "Prompt Builder", "fill": "#ffffff", "stroke": "#dce5e3", "flat": true },
    { "id": "model", "kind": "double_rect", "x": 370, "y": 340, "width": 220, "height": 50, "type_label": "REASONING", "label": "Model Runtime", "fill": "#ffffff", "stroke": "#10a37f" },
    { "id": "tools", "kind": "rect", "x": 700, "y": 340, "width": 180, "height": 50, "type_label": "ACTIONS", "label": "Tool Calls", "fill": "#ffffff", "stroke": "#dce5e3", "flat": true },
    { "id": "formatter", "kind": "rect", "x": 80, "y": 510, "width": 180, "height": 60, "type_label": "OUTPUT", "label": "Response Formatter", "fill": "#ffffff", "stroke": "#dce5e3", "flat": true },
    { "id": "observability", "kind": "rect", "x": 390, "y": 510, "width": 180, "height": 60, "type_label": "METRICS", "label": "Observability", "fill": "#ffffff", "stroke": "#dce5e3", "flat": true },
    { "id": "release", "kind": "rect", "x": 700, "y": 510, "width": 180, "height": 60, "type_label": "CONFIG", "label": "Release Control", "fill": "#ffffff", "stroke": "#dce5e3", "flat": true }
  ],
  "arrows": [
    { "id": "connect", "source": "app", "target": "sdk", "source_port": "right", "target_port": "left", "flow": "control", "motion_role": "connect", "motion_stage": 1, "motion_order": 0, "label": "connect" },
    { "id": "prepare", "source": "sdk", "target": "prompt", "source_port": "bottom", "target_port": "top", "flow": "read", "motion_role": "prepare", "motion_stage": 2, "motion_order": 0, "label": "prepare" },
    { "id": "invoke", "source": "prompt", "target": "model", "source_port": "right", "target_port": "left", "flow": "control", "motion_role": "invoke", "motion_stage": 3, "motion_order": 0, "label": "invoke" },
    { "id": "tool-call", "source": "model", "target": "tools", "source_port": "right", "target_port": "left", "flow": "read", "motion_role": "tool-call", "motion_stage": 4, "motion_order": 0, "label": "tool call" },
    { "id": "stream", "source": "model", "target": "formatter", "source_port": "bottom", "target_port": "top", "flow": "control", "motion_role": "token-stream", "motion_stage": 4, "motion_order": 1, "label": "stream" },
    { "id": "measure", "source": "formatter", "target": "observability", "source_port": "right", "target_port": "left", "flow": "feedback", "motion_role": "measure", "motion_stage": 5, "motion_order": 1, "label": "measure" },
    { "id": "govern", "source": "tools", "target": "release", "source_port": "bottom", "target_port": "top", "flow": "feedback", "motion_role": "govern", "motion_stage": 5, "motion_order": 0, "label": "govern" },
    { "id": "promote", "source": "observability", "target": "release", "source_port": "right", "target_port": "left", "flow": "feedback", "motion_role": "promote", "motion_stage": 6, "motion_order": 0, "label": "promote" }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 625,
  "legend_locked": true,
  "legend": [
    { "flow": "control", "label": "primary API path" },
    { "flow": "read", "label": "prompt / tools" },
    { "flow": "feedback", "label": "governance" }
  ],
  "footer": "Style 7 · OpenAI Official · precise integration stages",
  "footer_x": 48,
  "footer_y": 680
}
fixtures/cloud-fabric-style10.json
{
  "schema_version": 1,
  "mode": "architecture",
  "template_type": "architecture",
  "style": 10,
  "semantic_profile": "cloud-fabric",
  "diagram_type": "deployment",
  "platform_profile": "aws",
  "deployment_mode": "ACTIVE–ACTIVE",
  "icon_manifest_version": "2026.07-neutral.1",
  "quality_profile": "showcase",
  "width": 960,
  "height": 760,
  "title": "Active–Active Checkout Deployment",
  "subtitle": "global ingress, explicit region/VPC ownership, and inter-region data flow",
  "containers": [
    {
      "id": "global-edge",
      "deployment_kind": "global",
      "x": 30,
      "y": 120,
      "width": 900,
      "height": 100,
      "label": "Global Edge"
    },
    {
      "id": "region-a",
      "deployment_kind": "region",
      "x": 30,
      "y": 260,
      "width": 420,
      "height": 350,
      "label": "us-east-1"
    },
    {
      "id": "region-b",
      "deployment_kind": "region",
      "x": 510,
      "y": 260,
      "width": 420,
      "height": 350,
      "label": "eu-west-1"
    },
    {
      "id": "vpc-a",
      "deployment_kind": "network",
      "parent": "region-a",
      "x": 55,
      "y": 310,
      "width": 370,
      "height": 260,
      "label": "VPC A"
    },
    {
      "id": "vpc-b",
      "deployment_kind": "network",
      "parent": "region-b",
      "x": 535,
      "y": 310,
      "width": 370,
      "height": 260,
      "label": "VPC B"
    }
  ],
  "nodes": [
    {
      "id": "edge-a",
      "kind": "cloud_service",
      "deployment_kind": "edge",
      "deployment_id": "global-edge",
      "icon_id": "generic:traffic",
      "provider": "AWS",
      "x": 182,
      "y": 142,
      "width": 176,
      "height": 56,
      "label": "NA Edge",
      "sublabel": "health routing"
    },
    {
      "id": "edge-b",
      "kind": "cloud_service",
      "deployment_kind": "edge",
      "deployment_id": "global-edge",
      "icon_id": "generic:traffic",
      "provider": "AWS",
      "x": 662,
      "y": 142,
      "width": 176,
      "height": 56,
      "label": "EU Edge",
      "sublabel": "health routing"
    },
    {
      "id": "app-a",
      "kind": "cloud_service",
      "deployment_kind": "compute",
      "deployment_id": "vpc-a",
      "icon_id": "generic:compute",
      "provider": "AWS",
      "x": 182,
      "y": 350,
      "width": 176,
      "height": 72,
      "label": "App A",
      "sublabel": "checkout service"
    },
    {
      "id": "db-a",
      "kind": "cloud_service",
      "deployment_kind": "database",
      "deployment_id": "vpc-a",
      "icon_id": "generic:database",
      "provider": "AWS",
      "x": 182,
      "y": 470,
      "width": 176,
      "height": 72,
      "label": "Orders A",
      "sublabel": "regional primary"
    },
    {
      "id": "app-b",
      "kind": "cloud_service",
      "deployment_kind": "compute",
      "deployment_id": "vpc-b",
      "icon_id": "generic:compute",
      "provider": "AWS",
      "x": 662,
      "y": 350,
      "width": 176,
      "height": 72,
      "label": "App B",
      "sublabel": "checkout service"
    },
    {
      "id": "db-b",
      "kind": "cloud_service",
      "deployment_kind": "database",
      "deployment_id": "vpc-b",
      "icon_id": "generic:database",
      "provider": "AWS",
      "x": 662,
      "y": 470,
      "width": 176,
      "height": 72,
      "label": "Orders B",
      "sublabel": "regional primary"
    }
  ],
  "arrows": [
    {
      "id": "route-a",
      "source": "edge-a",
      "target": "app-a",
      "source_port": "bottom",
      "target_port": "top",
      "flow": "read",
      "motion_role": "global-route",
      "motion_stage": 1,
      "motion_order": 0,
      "via": "Route 53 health policy",
      "label_dy": -34
    },
    {
      "id": "route-b",
      "source": "edge-b",
      "target": "app-b",
      "source_port": "bottom",
      "target_port": "top",
      "flow": "read",
      "motion_role": "global-route",
      "motion_stage": 1,
      "motion_order": 1,
      "via": "Route 53 health policy",
      "label_dy": -34
    },
    {
      "id": "write-a",
      "source": "app-a",
      "target": "db-a",
      "source_port": "bottom",
      "target_port": "top",
      "flow": "write",
      "motion_role": "regional-write",
      "motion_stage": 2,
      "motion_order": 0
    },
    {
      "id": "write-b",
      "source": "app-b",
      "target": "db-b",
      "source_port": "bottom",
      "target_port": "top",
      "flow": "write",
      "motion_role": "regional-write",
      "motion_stage": 2,
      "motion_order": 1
    },
    {
      "id": "replicate-orders",
      "source": "db-a",
      "target": "db-b",
      "source_port": "right",
      "target_port": "left",
      "flow": "async",
      "motion_role": "cross-region",
      "motion_stage": 3,
      "motion_order": 0,
      "via": "inter-region peering",
      "dashed": true
    }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 660,
  "legend_locked": true,
  "legend": [
    { "flow": "read", "label": "global routing" },
    { "flow": "write", "label": "regional write" },
    { "flow": "async", "label": "cross-region replication" }
  ],
  "footer": "Style 10 · Cloud Fabric · provider-neutral glyphs · explicit deployment ownership",
  "footer_x": 48,
  "footer_y": 735
}
fixtures/multi-agent-style5.json
{
  "schema_version": 1,
  "mode": "agent",
  "template_type": "agent",
  "style": 5,
  "motion_scene": "agent-orchestration",
  "quality_profile": "showcase",
  "width": 960,
  "height": 700,
  "title": "Multi-Agent Collaboration",
  "subtitle": "coordinator-led delegation, shared state, quality review, and synthesis",
  "containers": [
    { "id": "mission", "x": 40, "y": 110, "width": 880, "height": 110, "label": "Mission Control", "stroke": "rgba(255,255,255,0.20)", "fill": "rgba(255,255,255,0.05)" },
    { "id": "specialists", "x": 40, "y": 270, "width": 880, "height": 140, "label": "Specialist Agents", "stroke": "rgba(255,255,255,0.20)", "fill": "rgba(255,255,255,0.05)" },
    { "id": "delivery", "x": 40, "y": 450, "width": 880, "height": 140, "label": "Delivery", "stroke": "rgba(255,255,255,0.20)", "fill": "rgba(255,255,255,0.05)" }
  ],
  "nodes": [
    { "id": "request", "kind": "speech", "x": 60, "y": 146, "width": 160, "height": 54, "label": "User Brief", "sublabel": "goal + constraints", "fill": "rgba(255,255,255,0.12)", "stroke": "rgba(255,255,255,0.35)", "flat": true },
    { "id": "coordinator", "kind": "double_rect", "x": 390, "y": 146, "width": 200, "height": 54, "type_label": "ORCHESTRATOR", "label": "Coordinator Agent", "fill": "rgba(255,255,255,0.12)", "stroke": "#c084fc" },
    { "id": "research", "kind": "rect", "x": 80, "y": 330, "width": 180, "height": 60, "type_label": "SPECIALIST", "label": "Research Agent", "fill": "rgba(255,255,255,0.12)", "stroke": "#60a5fa" },
    { "id": "coding", "kind": "terminal", "x": 390, "y": 330, "width": 200, "height": 60, "label": "Coding Agent", "fill": "#0f172a", "stroke": "rgba(255,255,255,0.24)", "header_fill": "rgba(255,255,255,0.12)" },
    { "id": "review", "kind": "rect", "x": 700, "y": 330, "width": 180, "height": 60, "type_label": "QUALITY GATE", "label": "Review Agent", "fill": "rgba(255,255,255,0.12)", "stroke": "#34d399" },
    { "id": "memory", "kind": "cylinder", "x": 80, "y": 510, "width": 180, "height": 60, "label": "Shared Memory", "sublabel": "facts + plans", "fill": "rgba(255,255,255,0.08)", "stroke": "#34d399" },
    { "id": "synthesis", "kind": "double_rect", "x": 390, "y": 510, "width": 200, "height": 60, "type_label": "MERGE", "label": "Synthesis Engine", "fill": "rgba(255,255,255,0.12)", "stroke": "#c084fc" },
    { "id": "response", "kind": "speech", "x": 700, "y": 510, "width": 180, "height": 60, "label": "Final Response", "sublabel": "reviewed result", "fill": "rgba(255,255,255,0.12)", "stroke": "#f59e0b", "flat": true }
  ],
  "arrows": [
    { "id": "brief", "source": "request", "target": "coordinator", "source_port": "right", "target_port": "left", "flow": "read", "motion_role": "ingress", "motion_stage": 1, "motion_order": 0, "label": "brief" },
    { "id": "a-delegate-research", "source": "coordinator", "target": "research", "source_port": "bottom", "target_port": "top", "flow": "control", "motion_role": "delegate", "motion_stage": 2, "motion_order": 0, "label": "research" },
    { "id": "b-delegate-coding", "source": "coordinator", "target": "coding", "source_port": "bottom", "target_port": "top", "flow": "control", "motion_role": "delegate", "motion_stage": 2, "motion_order": 1, "label": "build" },
    { "id": "c-delegate-review", "source": "coordinator", "target": "review", "source_port": "bottom", "target_port": "top", "flow": "control", "motion_role": "delegate", "motion_stage": 2, "motion_order": 2, "label": "review" },
    { "id": "research-write", "source": "research", "target": "memory", "source_port": "bottom", "target_port": "top", "flow": "write", "motion_role": "evidence", "motion_stage": 3, "motion_order": 0, "label": "evidence" },
    { "id": "memory-merge", "source": "memory", "target": "synthesis", "source_port": "right", "target_port": "left", "flow": "read", "motion_role": "context", "motion_stage": 4, "motion_order": 0, "label": "context" },
    { "id": "coding-merge", "source": "coding", "target": "synthesis", "source_port": "bottom", "target_port": "top", "flow": "write", "motion_role": "artifact", "motion_stage": 3, "motion_order": 1, "label": "artifact" },
    { "id": "review-output", "source": "review", "target": "response", "source_port": "bottom", "target_port": "top", "flow": "feedback", "motion_role": "approval", "motion_stage": 5, "motion_order": 1, "label": "approval" },
    { "id": "deliver", "source": "synthesis", "target": "response", "source_port": "right", "target_port": "left", "flow": "feedback", "motion_role": "deliver", "motion_stage": 5, "motion_order": 0, "label": "deliver" }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 625,
  "legend_locked": true,
  "legend": [
    { "flow": "read", "label": "input / context" },
    { "flow": "control", "label": "delegation" },
    { "flow": "write", "label": "shared work" },
    { "flow": "feedback", "label": "reviewed output" }
  ],
  "footer": "Style 5 · Glassmorphism · coordinated specialist workflow",
  "footer_x": 48,
  "footer_y": 680
}
docs/releases/v1.1.0.md
# Fireworks Tech Graph v1.1.0

This release turns the project into an engineering-grade diagram system while preserving the distinct scenarios and visual identity of all 12 styles.

## Version information

- Version: `v1.1.0`
- Version date: 2026-07-15
- Tag commit: [`1c7ba0fe`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/1c7ba0fe69ef5b166821eefb6fe447a0e5d70be9)
- Previous version: [`v1.0.5`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/releases/tag/v1.0.5)
- Release channel: GitHub stable release; the npm registry remains on the legacy `1.0.4` package line.

## Highlights

- Adds Style 9 C4 Review Canvas, Style 10 Cloud Fabric, Style 11 Event Transit, and Style 12 Ops Pulse.
- Enforces semantic contracts for C4 abstraction, deployment ownership, event rails, Golden Signals, observation windows, critical paths, and correlated traces.
- Adds geometry-safe orthogonal routing, port fan-out, waypoint validation, label and reserved-region checks, bridge masks, and fail-closed composition gates.
- Adds a unified CLI, versioned Diagram IR, deterministic layout reports, and safe offline interactive HTML export.
- Refreshes the official gallery with 12 distinct engineering scenarios and fixed-width 1920px regression renders.
- Ships a complete nested Agent Skill payload for Codex and Claude Code, plus reproducible tgz/zip archives and `SHA256SUMS`.

## Compatibility

- Node.js 18 or newer is required.
- Python source compatibility is checked against Python 3.9 grammar; CI runs the test suite on Python 3.9 and 3.12.
- The npm registry is a separate distribution channel and may lag this GitHub Release. Install the current Skill from `yizhiyanhua-ai/fireworks-tech-graph/skills/fireworks-tech-graph`.

See the [v1.1.0 changelog](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/blob/v1.1.0/CHANGELOG.md) for the full change list and the release assets below for checksum-verified archives.
fixtures/mem0-style1.json
{
  "schema_version": 1,
  "mode": "memory",
  "template_type": "memory",
  "style": 1,
  "motion_scene": "memory-weave",
  "quality_profile": "showcase",
  "width": 960,
  "height": 680,
  "title": "Mem0 Memory Architecture",
  "subtitle": "personal memory extraction, conflict resolution, storage, and retrieval",
  "containers": [
    { "id": "input-layer", "x": 40, "y": 110, "width": 880, "height": 110, "label": "Input Layer" },
    { "id": "memory-core", "x": 40, "y": 260, "width": 880, "height": 140, "label": "Memory Manager" },
    { "id": "storage-output", "x": 40, "y": 440, "width": 880, "height": 140, "label": "Storage and Retrieval" }
  ],
  "nodes": [
    { "id": "user", "kind": "user_avatar", "x": 60, "y": 145, "width": 120, "height": 54, "label": "User", "fill": "#eff6ff", "stroke": "#3b82f6", "flat": true },
    { "id": "app", "kind": "rect", "x": 280, "y": 145, "width": 160, "height": 54, "label": "AI App / Agent", "sublabel": "conversation input", "fill": "#ffffff", "stroke": "#cbd5e1", "flat": true },
    { "id": "llm", "kind": "double_rect", "x": 560, "y": 145, "width": 140, "height": 54, "label": "LLM", "sublabel": "reason + extract", "fill": "#faf5ff", "stroke": "#8b5cf6", "flat": true },
    { "id": "extractor", "kind": "rect", "x": 100, "y": 320, "width": 170, "height": 60, "label": "Fact Extractor", "sublabel": "salient memories", "fill": "#fff7ed", "stroke": "#f97316", "flat": true },
    { "id": "manager", "kind": "double_rect", "x": 390, "y": 320, "width": 180, "height": 60, "label": "Memory Manager", "sublabel": "store · update · forget", "fill": "#faf5ff", "stroke": "#8b5cf6", "flat": true },
    { "id": "resolver", "kind": "rect", "x": 700, "y": 320, "width": 160, "height": 60, "label": "Conflict Resolver", "sublabel": "merge + supersede", "fill": "#fff7ed", "stroke": "#f97316", "flat": true },
    { "id": "vector", "kind": "cylinder", "x": 100, "y": 500, "width": 160, "height": 60, "label": "Vector Store", "sublabel": "semantic recall", "fill": "#ecfdf5", "stroke": "#10b981" },
    { "id": "graph", "kind": "rect", "x": 400, "y": 500, "width": 160, "height": 60, "label": "Graph Memory", "sublabel": "relations + history", "fill": "#eff6ff", "stroke": "#3b82f6", "flat": true },
    { "id": "response", "kind": "speech", "x": 700, "y": 500, "width": 160, "height": 60, "label": "Personalized Reply", "sublabel": "ranked context", "fill": "#faf5ff", "stroke": "#8b5cf6", "flat": true }
  ],
  "arrows": [
    { "id": "request", "source": "user", "target": "app", "source_port": "right", "target_port": "left", "flow": "control", "motion_role": "ingress", "motion_stage": 1, "motion_order": 0, "label": "message" },
    { "id": "reason", "source": "app", "target": "llm", "source_port": "right", "target_port": "left", "flow": "control", "motion_role": "reason", "motion_stage": 2, "motion_order": 0, "label": "reason" },
    { "id": "extract", "source": "app", "target": "extractor", "source_port": "bottom", "target_port": "top", "flow": "data", "motion_role": "extract", "motion_stage": 3, "motion_order": 0, "label": "extract", "corridor_y": [240] },
    { "id": "facts", "source": "extractor", "target": "manager", "source_port": "right", "target_port": "left", "flow": "data", "motion_role": "transform", "motion_stage": 4, "motion_order": 0, "label": "facts" },
    { "id": "resolved", "source": "resolver", "target": "manager", "source_port": "left", "target_port": "right", "flow": "feedback", "motion_role": "resolve", "motion_stage": 4, "motion_order": 1, "label": "resolved" },
    { "id": "write", "source": "manager", "target": "vector", "source_port": "bottom", "target_port": "top", "flow": "write", "motion_role": "memory-write", "motion_stage": 5, "motion_order": 0, "label": "write", "corridor_y": [420] },
    { "id": "relate", "source": "vector", "target": "graph", "source_port": "right", "target_port": "left", "flow": "read", "motion_role": "memory-read", "motion_stage": 6, "motion_order": 0, "label": "retrieve" },
    { "id": "context", "source": "graph", "target": "response", "source_port": "right", "target_port": "left", "flow": "control", "motion_role": "response-context", "motion_stage": 7, "motion_order": 0, "label": "context" }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 600,
  "legend_locked": true,
  "legend": [
    { "flow": "control", "label": "control" },
    { "flow": "write", "label": "memory write" },
    { "flow": "read", "label": "memory read" },
    { "flow": "data", "label": "data transform" }
  ],
  "footer": "Style 1 · Flat Icon · Mem0 memory lifecycle",
  "footer_x": 48,
  "footer_y": 650
}
fixtures/quality-baseline/agent-runtime-style1.json
{
  "schema_version": 1,
  "mode": "architecture",
  "template_type": "architecture",
  "style": 1,
  "quality_profile": "showcase",
  "width": 960,
  "height": 600,
  "title": "Agent Runtime Architecture",
  "subtitle": "orchestration, memory, tools, and evaluation",
  "containers": [
    { "id": "control-plane", "x": 40, "y": 120, "width": 880, "height": 116, "label": "Control Plane" },
    { "id": "execution-state", "x": 40, "y": 304, "width": 880, "height": 136, "label": "Execution and State" }
  ],
  "nodes": [
    { "id": "client", "kind": "rect", "x": 60, "y": 156, "width": 160, "height": 60, "label": "Client", "sublabel": "request + policy", "fill": "#f8fafc", "stroke": "#94a3b8", "flat": true },
    { "id": "gateway", "kind": "double_rect", "x": 300, "y": 156, "width": 180, "height": 60, "label": "Gateway", "sublabel": "auth + routing", "fill": "#fffbeb", "stroke": "#f59e0b", "flat": true },
    { "id": "agent", "kind": "double_rect", "x": 560, "y": 156, "width": 180, "height": 60, "label": "Agent Runtime", "sublabel": "plan + inference", "fill": "#faf5ff", "stroke": "#8b5cf6", "flat": true },
    { "id": "memory", "kind": "rect", "x": 300, "y": 344, "width": 180, "height": 60, "label": "Vector Memory", "sublabel": "retrieve + persist", "fill": "#f0f9ff", "stroke": "#38bdf8", "flat": true },
    { "id": "tools", "kind": "rect", "x": 560, "y": 344, "width": 180, "height": 60, "label": "Tool Runtime", "sublabel": "MCP + functions", "fill": "#f0fdf4", "stroke": "#22c55e", "flat": true },
    { "id": "eval", "kind": "rect", "x": 790, "y": 344, "width": 110, "height": 60, "label": "Trace + Eval", "sublabel": "quality gate", "fill": "#fff1f2", "stroke": "#fb7185", "flat": true }
  ],
  "arrows": [
    { "id": "request", "source": "client", "target": "gateway", "source_port": "right", "target_port": "left", "flow": "control", "label": "request" },
    { "id": "dispatch", "source": "gateway", "target": "agent", "source_port": "right", "target_port": "left", "flow": "control", "label": "dispatch" },
    { "id": "context", "source": "agent", "target": "memory", "source_port": "bottom", "target_port": "top", "flow": "read", "label": "context", "route_points": [[632, 270], [390, 270]] },
    { "id": "tool-call", "source": "agent", "target": "tools", "source_port": "bottom", "target_port": "top", "flow": "async", "label": "tool call", "dashed": true },
    { "id": "augment", "source": "memory", "target": "tools", "source_port": "right", "target_port": "left", "flow": "write", "label": "augment" },
    { "id": "trace", "source": "tools", "target": "eval", "source_port": "right", "target_port": "left", "flow": "data", "label": "trace" },
    { "id": "z-feedback", "source": "eval", "target": "agent", "source_port": "top", "target_port": "bottom", "flow": "feedback", "label": "feedback", "dashed": true, "route_points": [[845, 270], [668, 270]] }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 510,
  "legend_locked": true,
  "legend": [
    { "flow": "control", "label": "primary" },
    { "flow": "write", "label": "data" },
    { "flow": "async", "label": "dependency" }
  ],
  "footer": "Style 1 · Flat Icon · showcase composition contract",
  "footer_x": 48,
  "footer_y": 570
}
fixtures/quality-baseline/agent-runtime-style12.json
{
  "schema_version": 1,
  "mode": "architecture",
  "template_type": "architecture",
  "style": 12,
  "semantic_profile": "generic",
  "quality_profile": "showcase",
  "width": 960,
  "height": 600,
  "title": "Agent Runtime Architecture",
  "subtitle": "orchestration, memory, tools, and evaluation",
  "containers": [
    { "id": "control-plane", "x": 40, "y": 120, "width": 880, "height": 116, "label": "Control Plane" },
    { "id": "execution-state", "x": 40, "y": 304, "width": 880, "height": 136, "label": "Execution and State" }
  ],
  "nodes": [
    { "id": "client", "kind": "rect", "x": 60, "y": 156, "width": 160, "height": 60, "label": "Client", "sublabel": "request + policy", "fill": "#f8fafc", "stroke": "#94a3b8", "flat": true },
    { "id": "gateway", "kind": "double_rect", "x": 300, "y": 156, "width": 180, "height": 60, "label": "Gateway", "sublabel": "auth + routing", "fill": "#fffbeb", "stroke": "#f59e0b", "flat": true },
    { "id": "agent", "kind": "double_rect", "x": 560, "y": 156, "width": 180, "height": 60, "label": "Agent Runtime", "sublabel": "plan + inference", "fill": "#faf5ff", "stroke": "#8b5cf6", "flat": true },
    { "id": "memory", "kind": "rect", "x": 300, "y": 344, "width": 180, "height": 60, "label": "Vector Memory", "sublabel": "retrieve + persist", "fill": "#f0f9ff", "stroke": "#38bdf8", "flat": true },
    { "id": "tools", "kind": "rect", "x": 560, "y": 344, "width": 180, "height": 60, "label": "Tool Runtime", "sublabel": "MCP + functions", "fill": "#f0fdf4", "stroke": "#22c55e", "flat": true },
    { "id": "eval", "kind": "rect", "x": 790, "y": 344, "width": 110, "height": 60, "label": "Trace + Eval", "sublabel": "quality gate", "fill": "#fff1f2", "stroke": "#fb7185", "flat": true }
  ],
  "arrows": [
    { "id": "request", "source": "client", "target": "gateway", "source_port": "right", "target_port": "left", "flow": "control", "label": "request" },
    { "id": "dispatch", "source": "gateway", "target": "agent", "source_port": "right", "target_port": "left", "flow": "control", "label": "dispatch" },
    { "id": "context", "source": "agent", "target": "memory", "source_port": "bottom", "target_port": "top", "flow": "read", "label": "context", "route_points": [[632, 270], [390, 270]] },
    { "id": "tool-call", "source": "agent", "target": "tools", "source_port": "bottom", "target_port": "top", "flow": "async", "label": "tool call", "dashed": true },
    { "id": "augment", "source": "memory", "target": "tools", "source_port": "right", "target_port": "left", "flow": "write", "label": "augment" },
    { "id": "trace", "source": "tools", "target": "eval", "source_port": "right", "target_port": "left", "flow": "data", "label": "trace" },
    { "id": "z-feedback", "source": "eval", "target": "agent", "source_port": "top", "target_port": "bottom", "flow": "feedback", "label": "feedback", "dashed": true, "route_points": [[845, 270], [668, 270]] }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 510,
  "legend_locked": true,
  "legend": [
    { "flow": "control", "label": "primary" },
    { "flow": "write", "label": "data" },
    { "flow": "async", "label": "dependency" }
  ],
  "footer": "Style 12 · Ops Pulse · showcase composition contract",
  "footer_x": 48,
  "footer_y": 570
}
fixtures/quality-baseline/agent-runtime-style2.json
{
  "schema_version": 1,
  "mode": "architecture",
  "template_type": "architecture",
  "style": 2,
  "quality_profile": "showcase",
  "width": 960,
  "height": 600,
  "title": "Agent Runtime Architecture",
  "subtitle": "orchestration, memory, tools, and evaluation",
  "window_controls": true,
  "meta_center": "AGENT RUNTIME / SHOWCASE",
  "meta_right": "STYLE-2 · DARK TERMINAL",
  "containers": [
    { "id": "control-plane", "x": 40, "y": 120, "width": 880, "height": 116, "label": "Control Plane" },
    { "id": "execution-state", "x": 40, "y": 304, "width": 880, "height": 136, "label": "Execution and State" }
  ],
  "nodes": [
    { "id": "client", "kind": "rect", "x": 60, "y": 156, "width": 160, "height": 60, "label": "Client", "sublabel": "request + policy", "fill": "#111827", "stroke": "#64748b", "flat": true },
    { "id": "gateway", "kind": "double_rect", "x": 300, "y": 156, "width": 180, "height": 60, "label": "Gateway", "sublabel": "auth + routing", "fill": "#111827", "stroke": "#f59e0b", "glow": "orange" },
    { "id": "agent", "kind": "double_rect", "x": 560, "y": 156, "width": 180, "height": 60, "label": "Agent Runtime", "sublabel": "plan + inference", "fill": "#111827", "stroke": "#a855f7", "glow": "purple" },
    { "id": "memory", "kind": "rect", "x": 300, "y": 344, "width": 180, "height": 60, "label": "Vector Memory", "sublabel": "retrieve + persist", "fill": "#0c2238", "stroke": "#38bdf8", "flat": true },
    { "id": "tools", "kind": "rect", "x": 560, "y": 344, "width": 180, "height": 60, "label": "Tool Runtime", "sublabel": "MCP + functions", "fill": "#10291f", "stroke": "#22c55e", "flat": true },
    { "id": "eval", "kind": "rect", "x": 790, "y": 344, "width": 110, "height": 60, "label": "Trace + Eval", "sublabel": "quality gate", "fill": "#331a24", "stroke": "#fb7185", "flat": true }
  ],
  "arrows": [
    { "id": "request", "source": "client", "target": "gateway", "source_port": "right", "target_port": "left", "flow": "control", "label": "request" },
    { "id": "dispatch", "source": "gateway", "target": "agent", "source_port": "right", "target_port": "left", "flow": "control", "label": "dispatch" },
    { "id": "context", "source": "agent", "target": "memory", "source_port": "bottom", "target_port": "top", "flow": "read", "label": "context", "route_points": [[632, 270], [390, 270]] },
    { "id": "tool-call", "source": "agent", "target": "tools", "source_port": "bottom", "target_port": "top", "flow": "async", "label": "tool call", "dashed": true },
    { "id": "augment", "source": "memory", "target": "tools", "source_port": "right", "target_port": "left", "flow": "write", "label": "augment" },
    { "id": "trace", "source": "tools", "target": "eval", "source_port": "right", "target_port": "left", "flow": "data", "label": "trace" },
    { "id": "z-feedback", "source": "eval", "target": "agent", "source_port": "top", "target_port": "bottom", "flow": "feedback", "label": "feedback", "dashed": true, "route_points": [[845, 270], [668, 270]] }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 510,
  "legend_locked": true,
  "legend": [
    { "flow": "control", "label": "primary" },
    { "flow": "write", "label": "data" },
    { "flow": "async", "label": "dependency" }
  ],
  "footer": "Style 2 · Dark Terminal · showcase composition contract",
  "footer_x": 48,
  "footer_y": 570
}
fixtures/quality-baseline/agent-runtime-style10.json
{
  "schema_version": 1,
  "mode": "architecture",
  "template_type": "architecture",
  "style": 10,
  "semantic_profile": "generic",
  "quality_profile": "showcase",
  "width": 960,
  "height": 600,
  "title": "Agent Runtime Architecture",
  "subtitle": "orchestration, memory, tools, and evaluation",
  "containers": [
    { "id": "control-plane", "x": 40, "y": 120, "width": 880, "height": 116, "label": "Control Plane" },
    { "id": "execution-state", "x": 40, "y": 304, "width": 880, "height": 136, "label": "Execution and State" }
  ],
  "nodes": [
    { "id": "client", "kind": "rect", "x": 60, "y": 156, "width": 160, "height": 60, "label": "Client", "sublabel": "request + policy", "fill": "#f8fafc", "stroke": "#94a3b8", "flat": true },
    { "id": "gateway", "kind": "double_rect", "x": 300, "y": 156, "width": 180, "height": 60, "label": "Gateway", "sublabel": "auth + routing", "fill": "#fffbeb", "stroke": "#f59e0b", "flat": true },
    { "id": "agent", "kind": "double_rect", "x": 560, "y": 156, "width": 180, "height": 60, "label": "Agent Runtime", "sublabel": "plan + inference", "fill": "#faf5ff", "stroke": "#8b5cf6", "flat": true },
    { "id": "memory", "kind": "rect", "x": 300, "y": 344, "width": 180, "height": 60, "label": "Vector Memory", "sublabel": "retrieve + persist", "fill": "#f0f9ff", "stroke": "#38bdf8", "flat": true },
    { "id": "tools", "kind": "rect", "x": 560, "y": 344, "width": 180, "height": 60, "label": "Tool Runtime", "sublabel": "MCP + functions", "fill": "#f0fdf4", "stroke": "#22c55e", "flat": true },
    { "id": "eval", "kind": "rect", "x": 790, "y": 344, "width": 110, "height": 60, "label": "Trace + Eval", "sublabel": "quality gate", "fill": "#fff1f2", "stroke": "#fb7185", "flat": true }
  ],
  "arrows": [
    { "id": "request", "source": "client", "target": "gateway", "source_port": "right", "target_port": "left", "flow": "control", "label": "request" },
    { "id": "dispatch", "source": "gateway", "target": "agent", "source_port": "right", "target_port": "left", "flow": "control", "label": "dispatch" },
    { "id": "context", "source": "agent", "target": "memory", "source_port": "bottom", "target_port": "top", "flow": "read", "label": "context", "route_points": [[632, 270], [390, 270]] },
    { "id": "tool-call", "source": "agent", "target": "tools", "source_port": "bottom", "target_port": "top", "flow": "async", "label": "tool call", "dashed": true },
    { "id": "augment", "source": "memory", "target": "tools", "source_port": "right", "target_port": "left", "flow": "write", "label": "augment" },
    { "id": "trace", "source": "tools", "target": "eval", "source_port": "right", "target_port": "left", "flow": "data", "label": "trace" },
    { "id": "z-feedback", "source": "eval", "target": "agent", "source_port": "top", "target_port": "bottom", "flow": "feedback", "label": "feedback", "dashed": true, "route_points": [[845, 270], [668, 270]] }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 510,
  "legend_locked": true,
  "legend": [
    { "flow": "control", "label": "primary" },
    { "flow": "write", "label": "data" },
    { "flow": "async", "label": "dependency" }
  ],
  "footer": "Style 10 · Cloud Fabric · showcase composition contract",
  "footer_x": 48,
  "footer_y": 570
}
fixtures/quality-baseline/agent-runtime-style4.json
{
  "schema_version": 1,
  "mode": "architecture",
  "template_type": "architecture",
  "style": 4,
  "quality_profile": "showcase",
  "width": 960,
  "height": 600,
  "title": "Agent Runtime Architecture",
  "subtitle": "orchestration, memory, tools, and evaluation",
  "containers": [
    { "id": "control-plane", "x": 40, "y": 120, "width": 880, "height": 116, "label": "Control Plane" },
    { "id": "execution-state", "x": 40, "y": 304, "width": 880, "height": 136, "label": "Execution and State" }
  ],
  "nodes": [
    { "id": "client", "kind": "rect", "x": 60, "y": 156, "width": 160, "height": 60, "label": "Client", "sublabel": "request + policy", "fill": "#f9fafb", "stroke": "#d1d5db", "flat": true },
    { "id": "gateway", "kind": "rect", "x": 300, "y": 156, "width": 180, "height": 60, "label": "Gateway", "sublabel": "auth + routing", "fill": "#f9fafb", "stroke": "#3b82f6", "flat": true },
    { "id": "agent", "kind": "rect", "x": 560, "y": 156, "width": 180, "height": 60, "label": "Agent Runtime", "sublabel": "plan + inference", "fill": "#eff6ff", "stroke": "#3b82f6", "flat": true },
    { "id": "memory", "kind": "rect", "x": 300, "y": 344, "width": 180, "height": 60, "label": "Vector Memory", "sublabel": "retrieve + persist", "fill": "#f9fafb", "stroke": "#d1d5db", "flat": true },
    { "id": "tools", "kind": "rect", "x": 560, "y": 344, "width": 180, "height": 60, "label": "Tool Runtime", "sublabel": "MCP + functions", "fill": "#f9fafb", "stroke": "#d1d5db", "flat": true },
    { "id": "eval", "kind": "rect", "x": 790, "y": 344, "width": 110, "height": 60, "label": "Trace + Eval", "sublabel": "quality gate", "fill": "#f9fafb", "stroke": "#d1d5db", "flat": true }
  ],
  "arrows": [
    { "id": "request", "source": "client", "target": "gateway", "source_port": "right", "target_port": "left", "flow": "control", "label": "request" },
    { "id": "dispatch", "source": "gateway", "target": "agent", "source_port": "right", "target_port": "left", "flow": "control", "label": "dispatch" },
    { "id": "context", "source": "agent", "target": "memory", "source_port": "bottom", "target_port": "top", "flow": "read", "label": "context", "route_points": [[632, 270], [390, 270]] },
    { "id": "tool-call", "source": "agent", "target": "tools", "source_port": "bottom", "target_port": "top", "flow": "async", "label": "tool call", "dashed": true },
    { "id": "augment", "source": "memory", "target": "tools", "source_port": "right", "target_port": "left", "flow": "write", "label": "augment" },
    { "id": "trace", "source": "tools", "target": "eval", "source_port": "right", "target_port": "left", "flow": "data", "label": "trace" },
    { "id": "z-feedback", "source": "eval", "target": "agent", "source_port": "top", "target_port": "bottom", "flow": "feedback", "label": "feedback", "dashed": true, "route_points": [[845, 270], [668, 270]] }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 510,
  "legend_locked": true,
  "legend": [
    { "flow": "control", "label": "primary" },
    { "flow": "write", "label": "data" },
    { "flow": "async", "label": "dependency" }
  ],
  "footer": "Style 4 · Notion Clean · showcase composition contract",
  "footer_x": 48,
  "footer_y": 570
}
fixtures/quality-baseline/agent-runtime-style5.json
{
  "schema_version": 1,
  "mode": "architecture",
  "template_type": "architecture",
  "style": 5,
  "quality_profile": "showcase",
  "width": 960,
  "height": 600,
  "title": "Agent Runtime Architecture",
  "subtitle": "orchestration, memory, tools, and evaluation",
  "containers": [
    { "id": "control-plane", "x": 40, "y": 120, "width": 880, "height": 116, "label": "Control Plane" },
    { "id": "execution-state", "x": 40, "y": 304, "width": 880, "height": 136, "label": "Execution and State" }
  ],
  "nodes": [
    { "id": "client", "kind": "rect", "x": 60, "y": 156, "width": 160, "height": 60, "label": "Client", "sublabel": "request + policy", "fill": "rgba(255,255,255,0.10)", "stroke": "rgba(255,255,255,0.34)", "flat": true },
    { "id": "gateway", "kind": "double_rect", "x": 300, "y": 156, "width": 180, "height": 60, "label": "Gateway", "sublabel": "auth + routing", "fill": "rgba(255,255,255,0.12)", "stroke": "#f59e0b" },
    { "id": "agent", "kind": "double_rect", "x": 560, "y": 156, "width": 180, "height": 60, "label": "Agent Runtime", "sublabel": "plan + inference", "fill": "rgba(255,255,255,0.12)", "stroke": "#c084fc" },
    { "id": "memory", "kind": "rect", "x": 300, "y": 344, "width": 180, "height": 60, "label": "Vector Memory", "sublabel": "retrieve + persist", "fill": "rgba(56,189,248,0.10)", "stroke": "#60a5fa", "flat": true },
    { "id": "tools", "kind": "rect", "x": 560, "y": 344, "width": 180, "height": 60, "label": "Tool Runtime", "sublabel": "MCP + functions", "fill": "rgba(52,211,153,0.10)", "stroke": "#34d399", "flat": true },
    { "id": "eval", "kind": "rect", "x": 790, "y": 344, "width": 110, "height": 60, "label": "Trace + Eval", "sublabel": "quality gate", "fill": "rgba(251,113,133,0.10)", "stroke": "#fb7185", "flat": true }
  ],
  "arrows": [
    { "id": "request", "source": "client", "target": "gateway", "source_port": "right", "target_port": "left", "flow": "control", "label": "request" },
    { "id": "dispatch", "source": "gateway", "target": "agent", "source_port": "right", "target_port": "left", "flow": "control", "label": "dispatch" },
    { "id": "context", "source": "agent", "target": "memory", "source_port": "bottom", "target_port": "top", "flow": "read", "label": "context", "route_points": [[632, 270], [390, 270]] },
    { "id": "tool-call", "source": "agent", "target": "tools", "source_port": "bottom", "target_port": "top", "flow": "async", "label": "tool call", "dashed": true },
    { "id": "augment", "source": "memory", "target": "tools", "source_port": "right", "target_port": "left", "flow": "write", "label": "augment" },
    { "id": "trace", "source": "tools", "target": "eval", "source_port": "right", "target_port": "left", "flow": "data", "label": "trace" },
    { "id": "z-feedback", "source": "eval", "target": "agent", "source_port": "top", "target_port": "bottom", "flow": "feedback", "label": "feedback", "dashed": true, "route_points": [[845, 270], [668, 270]] }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 510,
  "legend_locked": true,
  "legend": [
    { "flow": "control", "label": "primary" },
    { "flow": "write", "label": "data" },
    { "flow": "async", "label": "dependency" }
  ],
  "footer": "Style 5 · Glassmorphism · showcase composition contract",
  "footer_x": 48,
  "footer_y": 570
}
fixtures/c4-review-canvas-style9.json
{
  "schema_version": 1,
  "mode": "architecture",
  "template_type": "architecture",
  "style": 9,
  "motion_scene": "review-trace",
  "semantic_profile": "c4-review",
  "diagram_type": "c4",
  "c4_level": "container",
  "review_state": "ADR READY",
  "rough_seed": 20260715,
  "scope": "Checkout Platform",
  "quality_profile": "showcase",
  "width": 1100,
  "height": 700,
  "title": "Checkout Platform · Container Review",
  "subtitle": "one abstraction level, explicit responsibilities, review-ready relationships",
  "containers": [
    {
      "id": "checkout-boundary",
      "x": 260,
      "y": 150,
      "width": 600,
      "height": 420,
      "label": "Checkout Platform",
      "subtitle": "C4 container boundary"
    }
  ],
  "nodes": [
    {
      "id": "shopper",
      "kind": "review_card",
      "c4_type": "person",
      "x": 50,
      "y": 220,
      "width": 150,
      "height": 76,
      "label": "Shopper",
      "description": "places an order",
      "stroke": "#356a8a"
    },
    {
      "id": "web-app",
      "kind": "review_card",
      "c4_type": "container",
      "parent": "checkout-boundary",
      "x": 300,
      "y": 210,
      "width": 200,
      "height": 96,
      "label": "Checkout Web",
      "description": "collects cart + payment intent",
      "technology": "Next.js"
    },
    {
      "id": "checkout-api",
      "kind": "review_card",
      "c4_type": "container",
      "parent": "checkout-boundary",
      "x": 600,
      "y": 210,
      "width": 200,
      "height": 96,
      "label": "Checkout API",
      "description": "coordinates the purchase flow",
      "technology": "Go + gRPC",
      "stroke": "#a44a3f"
    },
    {
      "id": "order-store",
      "kind": "review_card",
      "c4_type": "container",
      "parent": "checkout-boundary",
      "x": 300,
      "y": 400,
      "width": 200,
      "height": 96,
      "label": "Order Store",
      "description": "persists order state",
      "technology": "PostgreSQL",
      "stroke": "#356a8a"
    },
    {
      "id": "order-worker",
      "kind": "review_card",
      "c4_type": "container",
      "parent": "checkout-boundary",
      "x": 600,
      "y": 400,
      "width": 200,
      "height": 96,
      "label": "Order Worker",
      "description": "confirms and fulfils orders",
      "technology": "Kotlin",
      "stroke": "#c06b35"
    },
    {
      "id": "payment-provider",
      "kind": "review_card",
      "c4_type": "external_system",
      "x": 900,
      "y": 410,
      "width": 150,
      "height": 76,
      "label": "Payment PSP",
      "description": "authorizes cards",
      "stroke": "#7a5c99"
    }
  ],
  "arrows": [
    {
      "id": "browse-checkout",
      "source": "shopper",
      "target": "web-app",
      "source_port": "right",
      "target_port": "left",
      "flow": "read",
      "motion_role": "review-entry",
      "motion_stage": 1,
      "motion_order": 0,
      "label": "start",
      "protocol": "HTTPS"
    },
    {
      "id": "submit-order",
      "source": "web-app",
      "target": "checkout-api",
      "source_port": "right",
      "target_port": "left",
      "flow": "control",
      "motion_role": "review-request",
      "motion_stage": 2,
      "motion_order": 0,
      "label": "order",
      "protocol": "JSON/HTTPS"
    },
    {
      "id": "enqueue-order",
      "source": "checkout-api",
      "target": "order-worker",
      "source_port": "bottom",
      "target_port": "top",
      "flow": "async",
      "motion_role": "review-async",
      "motion_stage": 3,
      "motion_order": 0,
      "label": "enqueue",
      "protocol": "Kafka"
    },
    {
      "id": "persist-order",
      "source": "order-worker",
      "target": "order-store",
      "source_port": "left",
      "target_port": "right",
      "flow": "write",
      "motion_role": "review-state",
      "motion_stage": 4,
      "motion_order": 0,
      "label": "save",
      "protocol": "SQL"
    },
    {
      "id": "authorize-payment",
      "source": "order-worker",
      "target": "payment-provider",
      "source_port": "right",
      "target_port": "left",
      "flow": "control",
      "motion_role": "review-external",
      "motion_stage": 4,
      "motion_order": 1,
      "label": "authorize",
      "protocol": "HTTPS"
    }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 620,
  "legend_locked": true,
  "legend": [
    { "flow": "control", "label": "request" },
    { "flow": "write", "label": "state change" },
    { "flow": "async", "label": "asynchronous" }
  ],
  "footer": "Style 9 · C4 Review Canvas · deterministic review marks · container-level semantics",
  "footer_x": 48,
  "footer_y": 680
}
fixtures/event-transit-style11.json
{
  "schema_version": 1,
  "mode": "flow",
  "template_type": "flow",
  "style": 11,
  "semantic_profile": "event-transit",
  "diagram_type": "event_stream",
  "line_code": "LINE A · EVENT METRO",
  "quality_profile": "showcase",
  "width": 1172,
  "height": 720,
  "title": "Checkout Event Line",
  "subtitle": "topics as rails, processors as stations, branches only at declared junctions",
  "topics": [
    { "id": "checkout.events", "color": "#e4475b" },
    { "id": "checkout.dlq", "color": "#c62828" }
  ],
  "containers": [
    {
      "id": "checkout-stream",
      "x": 35,
      "y": 140,
      "width": 1102,
      "height": 410,
      "label": "checkout.events",
      "subtitle": "12 partitions · retention 72 h",
      "header_prefix": "LINE A",
      "header_separator": " · "
    }
  ],
  "nodes": [
    {
      "id": "checkout-api",
      "kind": "transit_terminal",
      "transit_role": "producer",
      "topic_id": "checkout.events",
      "station_order": 0,
      "x": 60,
      "y": 258,
      "width": 150,
      "height": 78,
      "label": "Checkout API",
      "sublabel": "producer",
      "rail_color": "#e4475b",
      "badge": "P12"
    },
    {
      "id": "schema-gate",
      "kind": "transit_station",
      "transit_role": "station",
      "topic_id": "checkout.events",
      "station_order": 1,
      "operation": "validate envelope",
      "x": 278,
      "y": 258,
      "width": 150,
      "height": 78,
      "label": "Schema Gate",
      "sublabel": "validate",
      "rail_color": "#e4475b",
      "badge": "v3"
    },
    {
      "id": "fraud-score",
      "kind": "transit_station",
      "transit_role": "station",
      "topic_id": "checkout.events",
      "station_order": 2,
      "operation": "score risk",
      "x": 496,
      "y": 258,
      "width": 150,
      "height": 78,
      "label": "Fraud Score",
      "sublabel": "enrich",
      "rail_color": "#e4475b",
      "badge": "p95 32ms"
    },
    {
      "id": "order-router",
      "kind": "transit_junction",
      "transit_role": "junction",
      "topic_id": "checkout.events",
      "station_order": 3,
      "operation": "route accepted orders",
      "x": 714,
      "y": 258,
      "width": 150,
      "height": 78,
      "label": "Order Router",
      "sublabel": "junction",
      "rail_color": "#e4475b",
      "badge": "fan-out"
    },
    {
      "id": "fulfilment",
      "kind": "transit_terminal",
      "transit_role": "consumer",
      "topic_id": "checkout.events",
      "station_order": 4,
      "consumer_group": "fulfilment-v2",
      "x": 932,
      "y": 258,
      "width": 180,
      "height": 78,
      "label": "Fulfilment",
      "sublabel": "consumer group",
      "rail_color": "#e4475b",
      "badge": "lag 14"
    },
    {
      "id": "checkout-dlq",
      "kind": "transit_terminal",
      "transit_role": "dlq",
      "topic_id": "checkout.dlq",
      "x": 496,
      "y": 430,
      "width": 150,
      "height": 78,
      "label": "Checkout DLQ",
      "sublabel": "manual replay",
      "stroke": "#c62828",
      "badge": "7 events"
    },
    {
      "id": "order-state",
      "kind": "transit_terminal",
      "transit_role": "state_store",
      "topic_id": "checkout.events",
      "x": 714,
      "y": 430,
      "width": 150,
      "height": 78,
      "label": "Order State",
      "sublabel": "materialized view",
      "rail_color": "#00897b",
      "badge": "RocksDB"
    }
  ],
  "arrows": [
    { "id": "rail-01", "source": "checkout-api", "target": "schema-gate", "source_port": "right", "target_port": "left", "flow": "control", "transit_type": "rail", "topic_id": "checkout.events", "motion_role": "topic-rail", "motion_stage": 1, "motion_order": 0 },
    { "id": "rail-12", "source": "schema-gate", "target": "fraud-score", "source_port": "right", "target_port": "left", "flow": "control", "transit_type": "rail", "topic_id": "checkout.events", "motion_role": "topic-rail", "motion_stage": 2, "motion_order": 0 },
    { "id": "rail-23", "source": "fraud-score", "target": "order-router", "source_port": "right", "target_port": "left", "flow": "control", "transit_type": "rail", "topic_id": "checkout.events", "motion_role": "topic-rail", "motion_stage": 3, "motion_order": 0 },
    { "id": "rail-34", "source": "order-router", "target": "fulfilment", "source_port": "right", "target_port": "left", "flow": "control", "transit_type": "rail", "topic_id": "checkout.events", "motion_role": "topic-rail", "motion_stage": 4, "motion_order": 0 },
    { "id": "to-dlq", "source": "fraud-score", "target": "checkout-dlq", "source_port": "bottom", "target_port": "top", "flow": "feedback", "transit_type": "dead_letter", "topic_id": "checkout.dlq", "motion_role": "dead-letter", "motion_stage": 5, "motion_order": 0, "dashed": true },
    { "id": "materialize", "source": "order-router", "target": "order-state", "source_port": "bottom", "target_port": "top", "flow": "write", "transit_type": "state", "topic_id": "checkout.events", "motion_role": "state-project", "motion_stage": 5, "motion_order": 1 }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 610,
  "legend_locked": true,
  "legend": [
    { "flow": "control", "label": "topic rail" },
    { "flow": "write", "label": "state projection" },
    { "flow": "feedback", "label": "dead-letter route" }
  ],
  "footer": "Style 11 · Event Transit · ordinary edges remain the semantic rails",
  "footer_x": 48,
  "footer_y": 695
}
fixtures/quality-baseline/agent-runtime-style7.json
{
  "schema_version": 1,
  "mode": "architecture",
  "template_type": "architecture",
  "style": 7,
  "quality_profile": "showcase",
  "width": 960,
  "height": 600,
  "title": "Agent Runtime Architecture",
  "subtitle": "orchestration, memory, tools, and evaluation",
  "containers": [
    { "id": "control-plane", "x": 40, "y": 120, "width": 880, "height": 116, "label": "Control Plane" },
    { "id": "execution-state", "x": 40, "y": 304, "width": 880, "height": 136, "label": "Execution and State" }
  ],
  "nodes": [
    { "id": "client", "kind": "rect", "x": 60, "y": 156, "width": 160, "height": 60, "label": "Client", "sublabel": "request + policy", "fill": "#ffffff", "stroke": "#cbd5e1", "flat": true },
    { "id": "gateway", "kind": "double_rect", "x": 300, "y": 156, "width": 180, "height": 60, "label": "Gateway", "sublabel": "auth + routing", "fill": "#ffffff", "stroke": "#10a37f" },
    { "id": "agent", "kind": "double_rect", "x": 560, "y": 156, "width": 180, "height": 60, "label": "Agent Runtime", "sublabel": "plan + inference", "fill": "#f0fdfa", "stroke": "#10a37f" },
    { "id": "memory", "kind": "rect", "x": 300, "y": 344, "width": 180, "height": 60, "label": "Vector Memory", "sublabel": "retrieve + persist", "fill": "#f0f9ff", "stroke": "#0891b2", "flat": true },
    { "id": "tools", "kind": "rect", "x": 560, "y": 344, "width": 180, "height": 60, "label": "Tool Runtime", "sublabel": "MCP + functions", "fill": "#ecfdf5", "stroke": "#10a37f", "flat": true },
    { "id": "eval", "kind": "rect", "x": 790, "y": 344, "width": 110, "height": 60, "label": "Trace + Eval", "sublabel": "quality gate", "fill": "#fff7ed", "stroke": "#f59e0b", "flat": true }
  ],
  "arrows": [
    { "id": "request", "source": "client", "target": "gateway", "source_port": "right", "target_port": "left", "flow": "control", "label": "request" },
    { "id": "dispatch", "source": "gateway", "target": "agent", "source_port": "right", "target_port": "left", "flow": "control", "label": "dispatch" },
    { "id": "context", "source": "agent", "target": "memory", "source_port": "bottom", "target_port": "top", "flow": "read", "label": "context", "route_points": [[632, 270], [390, 270]] },
    { "id": "tool-call", "source": "agent", "target": "tools", "source_port": "bottom", "target_port": "top", "flow": "async", "label": "tool call", "dashed": true },
    { "id": "augment", "source": "memory", "target": "tools", "source_port": "right", "target_port": "left", "flow": "write", "label": "augment" },
    { "id": "trace", "source": "tools", "target": "eval", "source_port": "right", "target_port": "left", "flow": "data", "label": "trace" },
    { "id": "z-feedback", "source": "eval", "target": "agent", "source_port": "top", "target_port": "bottom", "flow": "feedback", "label": "feedback", "dashed": true, "route_points": [[845, 270], [668, 270]] }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 510,
  "legend_locked": true,
  "legend": [
    { "flow": "control", "label": "primary" },
    { "flow": "write", "label": "data" },
    { "flow": "async", "label": "dependency" }
  ],
  "footer": "Style 7 · OpenAI Official · showcase composition contract",
  "footer_x": 48,
  "footer_y": 570
}
fixtures/microservices-style3.json
{
  "schema_version": 1,
  "mode": "architecture",
  "template_type": "architecture",
  "style": 3,
  "motion_scene": "service-blueprint",
  "quality_profile": "showcase",
  "width": 960,
  "height": 720,
  "title": "Microservices Architecture",
  "subtitle": "edge routing, domain services, data stores, events, and observability",
  "containers": [
    { "id": "edge", "x": 40, "y": 110, "width": 880, "height": 110, "label": "Edge", "header_prefix": "01" },
    { "id": "services", "x": 40, "y": 270, "width": 880, "height": 130, "label": "Application Services", "header_prefix": "02" },
    { "id": "data", "x": 40, "y": 420, "width": 680, "height": 130, "label": "Data" },
    { "id": "observability", "x": 740, "y": 420, "width": 180, "height": 130, "label": "Obs" }
  ],
  "nodes": [
    { "id": "clients", "kind": "rect", "x": 60, "y": 146, "width": 160, "height": 54, "label": "Client Apps", "sublabel": "web · mobile", "fill": "#0b3b5e", "stroke": "#94a3b8", "flat": true },
    { "id": "gateway", "kind": "double_rect", "x": 300, "y": 146, "width": 160, "height": 54, "label": "API Gateway", "sublabel": "route + throttle", "fill": "#0a4b78", "stroke": "#38bdf8", "flat": true },
    { "id": "auth", "kind": "hexagon", "x": 570, "y": 146, "width": 160, "height": 54, "label": "Auth / Policy", "sublabel": "identity gate", "fill": "#0b3b5e", "stroke": "#f59e0b", "flat": true },
    { "id": "order", "kind": "rect", "x": 80, "y": 330, "width": 140, "height": 50, "label": "Order Service", "sublabel": "transactions", "fill": "#0a4b78", "stroke": "#38bdf8", "flat": true },
    { "id": "catalog", "kind": "rect", "x": 310, "y": 330, "width": 140, "height": 50, "label": "Catalog Service", "sublabel": "inventory", "fill": "#0a4b78", "stroke": "#38bdf8", "flat": true },
    { "id": "billing", "kind": "rect", "x": 540, "y": 330, "width": 140, "height": 50, "label": "Billing Service", "sublabel": "payments", "fill": "#0a4b78", "stroke": "#38bdf8", "flat": true },
    { "id": "events", "kind": "hexagon", "x": 760, "y": 330, "width": 140, "height": 50, "label": "Event Router", "sublabel": "async bus", "fill": "#123c5a", "stroke": "#a855f7", "flat": true },
    { "id": "postgres", "kind": "cylinder", "x": 80, "y": 480, "width": 140, "height": 50, "label": "Postgres", "sublabel": "orders", "fill": "#082f49", "stroke": "#38bdf8" },
    { "id": "redis", "kind": "cylinder", "x": 310, "y": 480, "width": 140, "height": 50, "label": "Redis", "sublabel": "catalog cache", "fill": "#082f49", "stroke": "#38bdf8" },
    { "id": "warehouse", "kind": "cylinder", "x": 540, "y": 480, "width": 140, "height": 50, "label": "Warehouse", "sublabel": "billing facts", "fill": "#082f49", "stroke": "#38bdf8" },
    { "id": "metrics", "kind": "rect", "x": 765, "y": 480, "width": 130, "height": 50, "label": "Metrics", "sublabel": "traces + SLOs", "fill": "#123c5a", "stroke": "#10b981", "flat": true }
  ],
  "arrows": [
    { "id": "client-request", "source": "clients", "target": "gateway", "source_port": "right", "target_port": "left", "flow": "read", "motion_role": "ingress", "motion_stage": 1, "motion_order": 0, "label": "HTTPS" },
    { "id": "policy-check", "source": "gateway", "target": "auth", "source_port": "right", "target_port": "left", "flow": "control", "motion_role": "policy", "motion_stage": 2, "motion_order": 0, "label": "policy" },
    { "id": "a-route-order", "source": "gateway", "target": "order", "source_port": "bottom", "target_port": "top", "flow": "read", "motion_role": "fanout", "motion_stage": 3, "motion_order": 0 },
    { "id": "b-route-catalog", "source": "gateway", "target": "catalog", "source_port": "bottom", "target_port": "top", "flow": "read", "motion_role": "fanout", "motion_stage": 3, "motion_order": 1 },
    { "id": "c-route-billing", "source": "gateway", "target": "billing", "source_port": "bottom", "target_port": "top", "flow": "read", "motion_role": "fanout", "motion_stage": 3, "motion_order": 2 },
    { "id": "order-store", "source": "order", "target": "postgres", "source_port": "bottom", "target_port": "top", "flow": "data", "motion_role": "data-write", "motion_stage": 4, "motion_order": 0, "label": "persist" },
    { "id": "catalog-cache", "source": "catalog", "target": "redis", "source_port": "bottom", "target_port": "top", "flow": "data", "motion_role": "data-write", "motion_stage": 4, "motion_order": 1, "label": "cache" },
    { "id": "billing-store", "source": "billing", "target": "warehouse", "source_port": "bottom", "target_port": "top", "flow": "data", "motion_role": "data-write", "motion_stage": 4, "motion_order": 2, "label": "facts" },
    { "id": "publish-event", "source": "billing", "target": "events", "source_port": "right", "target_port": "left", "flow": "feedback", "motion_role": "event", "motion_stage": 5, "motion_order": 0, "label": "events" },
    { "id": "observe", "source": "events", "target": "metrics", "source_port": "bottom", "target_port": "top", "flow": "feedback", "motion_role": "telemetry", "motion_stage": 6, "motion_order": 0, "label": "telemetry" }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 600,
  "legend_locked": true,
  "legend": [
    { "flow": "read", "label": "request" },
    { "flow": "control", "label": "policy" },
    { "flow": "data", "label": "data path" },
    { "flow": "feedback", "label": "events" }
  ],
  "blueprint_title_block": {
    "title": "AI MICROSERVICES",
    "subtitle": "SYSTEM ARCHITECTURE",
    "center_caption": "STYLE 3",
    "left_caption": "REV: 1.1",
    "right_caption": "DWG: ARCH-003",
    "width": 220,
    "height": 76,
    "x": 700,
    "y": 620
  },
  "footer": "Style 3 · Blueprint · domain services and data planes",
  "footer_x": 48,
  "footer_y": 700
}
fixtures/ops-pulse-style12.json
{
  "schema_version": 1,
  "mode": "architecture",
  "template_type": "architecture",
  "style": 12,
  "semantic_profile": "ops-pulse",
  "diagram_type": "observability",
  "observation_window": "5m",
  "quality_profile": "showcase",
  "width": 960,
  "height": 860,
  "title": "Checkout Reliability Pulse",
  "subtitle": "golden signals, critical request path, and one correlated trace waterfall",
  "critical_path_id": "checkout-request",
  "critical_path": ["edge-gateway-api", "edge-api-checkout", "edge-checkout-payment"],
  "containers": [
    {
      "id": "service-health",
      "x": 20,
      "y": 120,
      "width": 920,
      "height": 310,
      "label": "Service Health",
      "subtitle": "rolling 5 minute window"
    },
    {
      "id": "trace-waterfall",
      "x": 40,
      "y": 450,
      "width": 880,
      "height": 330,
      "label": "Trace 7F3A · checkout-request",
      "subtitle": "420 ms end-to-end · sampled on elevated latency"
    }
  ],
  "nodes": [
    {
      "id": "edge-gateway",
      "kind": "ops_service",
      "ops_role": "service",
      "x": 40,
      "y": 170,
      "width": 180,
      "height": 108,
      "label": "Edge Gateway",
      "status": "ok",
      "status_label": "HEALTHY",
      "signals": {
        "latency": { "value": "18", "unit": "ms", "window": "5m", "status": "ok" },
        "traffic": { "value": "8.2k", "unit": "rps", "window": "5m", "status": "ok" },
        "errors": { "value": "0.08", "unit": "%", "window": "5m", "status": "ok" },
        "saturation": { "value": "42", "unit": "%", "window": "5m", "status": "ok" }
      }
    },
    {
      "id": "api-gateway",
      "kind": "ops_service",
      "ops_role": "service",
      "x": 270,
      "y": 170,
      "width": 180,
      "height": 108,
      "label": "API Gateway",
      "status": "warn",
      "status_label": "WATCH",
      "signals": {
        "latency": { "value": "61", "unit": "ms", "window": "5m", "status": "warn" },
        "traffic": { "value": "7.9k", "unit": "rps", "window": "5m", "status": "ok" },
        "errors": { "value": "0.15", "unit": "%", "window": "5m", "status": "ok" },
        "saturation": { "value": "68", "unit": "%", "window": "5m", "status": "warn" }
      }
    },
    {
      "id": "checkout-service",
      "kind": "ops_service",
      "ops_role": "service",
      "x": 500,
      "y": 170,
      "width": 180,
      "height": 108,
      "label": "Checkout",
      "status": "critical",
      "status_label": "DEGRADED",
      "signals": {
        "latency": { "value": "286", "unit": "ms", "window": "5m", "status": "critical" },
        "traffic": { "value": "3.1k", "unit": "rps", "window": "5m", "status": "ok" },
        "errors": { "value": "2.6", "unit": "%", "window": "5m", "status": "critical" },
        "saturation": { "value": "84", "unit": "%", "window": "5m", "status": "warn" }
      }
    },
    {
      "id": "payment-service",
      "kind": "ops_service",
      "ops_role": "service",
      "x": 730,
      "y": 170,
      "width": 180,
      "height": 108,
      "label": "Payment",
      "status": "warn",
      "status_label": "WATCH",
      "signals": {
        "latency": { "value": "94", "unit": "ms", "window": "5m", "status": "warn" },
        "traffic": { "value": "2.8k", "unit": "rps", "window": "5m", "status": "ok" },
        "errors": { "value": "0.9", "unit": "%", "window": "5m", "status": "warn" },
        "saturation": { "value": "57", "unit": "%", "window": "5m", "status": "ok" }
      }
    },
    {
      "id": "otel-collector",
      "kind": "otel_collector",
      "ops_role": "collector",
      "x": 500,
      "y": 350,
      "width": 180,
      "height": 60,
      "label": "OTel Collector"
    },
    {
      "id": "span-root",
      "kind": "trace_span",
      "ops_role": "trace_span",
      "span_id": "span-root",
      "start_ms": 0,
      "duration_ms": 420,
      "x": 100,
      "y": 520,
      "width": 760,
      "height": 28,
      "label": "edge-gateway / checkout-request",
      "status": "warn"
    },
    {
      "id": "span-api",
      "kind": "trace_span",
      "ops_role": "trace_span",
      "span_id": "span-api",
      "parent_span": "span-root",
      "start_ms": 8,
      "duration_ms": 380,
      "x": 114.5,
      "y": 588,
      "width": 687.6,
      "height": 28,
      "label": "api-gateway / authorize",
      "status": "ok"
    },
    {
      "id": "span-checkout",
      "kind": "trace_span",
      "ops_role": "trace_span",
      "span_id": "span-checkout",
      "parent_span": "span-api",
      "start_ms": 42,
      "duration_ms": 286,
      "x": 176,
      "y": 656,
      "width": 517.5,
      "height": 28,
      "label": "checkout / commit-order",
      "status": "critical"
    },
    {
      "id": "span-payment",
      "kind": "trace_span",
      "ops_role": "trace_span",
      "span_id": "span-payment",
      "parent_span": "span-checkout",
      "start_ms": 85,
      "duration_ms": 94,
      "x": 253.8,
      "y": 724,
      "width": 170.1,
      "height": 28,
      "label": "payment / auth",
      "status": "warn"
    }
  ],
  "arrows": [
    { "id": "edge-gateway-api", "source": "edge-gateway", "target": "api-gateway", "source_port": "right", "target_port": "left", "flow": "control", "edge_kind": "business", "protocol": "HTTP/2", "motion_role": "critical-request", "motion_stage": 1, "motion_order": 0 },
    { "id": "edge-api-checkout", "source": "api-gateway", "target": "checkout-service", "source_port": "right", "target_port": "left", "flow": "control", "edge_kind": "business", "protocol": "gRPC", "motion_role": "critical-request", "motion_stage": 2, "motion_order": 0 },
    { "id": "edge-checkout-payment", "source": "checkout-service", "target": "payment-service", "source_port": "right", "target_port": "left", "flow": "control", "edge_kind": "business", "protocol": "gRPC", "motion_role": "critical-request", "motion_stage": 3, "motion_order": 0 },
    { "id": "export-telemetry", "source": "checkout-service", "target": "otel-collector", "source_port": "bottom", "target_port": "top", "flow": "async", "edge_kind": "telemetry", "motion_role": "telemetry-export", "motion_stage": 4, "motion_order": 0, "dashed": true }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 798,
  "legend_locked": true,
  "legend": [
    { "flow": "control", "label": "request" },
    { "flow": "async", "label": "telemetry" },
    { "flow": "feedback", "label": "degraded" }
  ],
  "footer": "Style 12 · Ops Pulse · exact golden signals · one critical path · correlated trace",
  "footer_x": 48,
  "footer_y": 848
}
fixtures/quality-baseline/agent-runtime-style11.json
{
  "schema_version": 1,
  "mode": "architecture",
  "template_type": "architecture",
  "style": 11,
  "semantic_profile": "generic",
  "quality_profile": "showcase",
  "width": 960,
  "height": 600,
  "title": "Agent Runtime Architecture",
  "subtitle": "orchestration, memory, tools, and evaluation",
  "containers": [
    { "id": "control-plane", "x": 40, "y": 120, "width": 880, "height": 116, "label": "Control Plane" },
    { "id": "execution-state", "x": 40, "y": 304, "width": 880, "height": 136, "label": "Execution and State" }
  ],
  "nodes": [
    { "id": "client", "kind": "rect", "x": 60, "y": 156, "width": 160, "height": 60, "label": "Client", "sublabel": "request + policy", "fill": "#f8fafc", "stroke": "#94a3b8", "flat": true },
    { "id": "gateway", "kind": "double_rect", "x": 300, "y": 156, "width": 180, "height": 60, "label": "Gateway", "sublabel": "auth + routing", "fill": "#fffbeb", "stroke": "#f59e0b", "flat": true },
    { "id": "agent", "kind": "double_rect", "x": 560, "y": 156, "width": 180, "height": 60, "label": "Agent Runtime", "sublabel": "plan + inference", "fill": "#faf5ff", "stroke": "#8b5cf6", "flat": true },
    { "id": "memory", "kind": "rect", "x": 300, "y": 344, "width": 180, "height": 60, "label": "Vector Memory", "sublabel": "retrieve + persist", "fill": "#f0f9ff", "stroke": "#38bdf8", "flat": true },
    { "id": "tools", "kind": "rect", "x": 560, "y": 344, "width": 180, "height": 60, "label": "Tool Runtime", "sublabel": "MCP + functions", "fill": "#f0fdf4", "stroke": "#22c55e", "flat": true },
    { "id": "eval", "kind": "rect", "x": 790, "y": 344, "width": 110, "height": 60, "label": "Trace + Eval", "sublabel": "quality gate", "fill": "#fff1f2", "stroke": "#fb7185", "flat": true }
  ],
  "arrows": [
    { "id": "request", "source": "client", "target": "gateway", "source_port": "right", "target_port": "left", "flow": "control", "label": "request" },
    { "id": "dispatch", "source": "gateway", "target": "agent", "source_port": "right", "target_port": "left", "flow": "control", "label": "dispatch" },
    { "id": "context", "source": "agent", "target": "memory", "source_port": "bottom", "target_port": "top", "flow": "read", "label": "context", "route_points": [[632, 270], [390, 270]] },
    { "id": "tool-call", "source": "agent", "target": "tools", "source_port": "bottom", "target_port": "top", "flow": "async", "label": "tool call", "dashed": true },
    { "id": "augment", "source": "memory", "target": "tools", "source_port": "right", "target_port": "left", "flow": "write", "label": "augment" },
    { "id": "trace", "source": "tools", "target": "eval", "source_port": "right", "target_port": "left", "flow": "data", "label": "trace" },
    { "id": "z-feedback", "source": "eval", "target": "agent", "source_port": "top", "target_port": "bottom", "flow": "feedback", "label": "feedback", "dashed": true, "route_points": [[845, 270], [668, 270]] }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 510,
  "legend_locked": true,
  "legend": [
    { "flow": "control", "label": "primary" },
    { "flow": "write", "label": "data" },
    { "flow": "async", "label": "dependency" }
  ],
  "footer": "Style 11 · Event Transit · showcase composition contract",
  "footer_x": 48,
  "footer_y": 570
}
fixtures/tool-call-style2.json
{
  "schema_version": 1,
  "mode": "agent",
  "template_type": "agent",
  "style": 2,
  "motion_scene": "tool-grounding",
  "quality_profile": "showcase",
  "width": 960,
  "height": 720,
  "title": "Tool Call Flow",
  "subtitle": "retrieval, terminal execution, source grounding, and answer synthesis",
  "window_controls": true,
  "meta_center": "AGENT TOOL LOOP / GROUNDED EXECUTION",
  "meta_right": "STYLE-2 · DARK TERMINAL",
  "containers": [
    { "id": "request-path", "x": 40, "y": 120, "width": 880, "height": 110, "label": "Request Path" },
    { "id": "tooling-fabric", "x": 40, "y": 290, "width": 880, "height": 150, "label": "Tooling Fabric" },
    { "id": "source-grounding", "x": 40, "y": 480, "width": 880, "height": 140, "label": "Source Grounding" }
  ],
  "nodes": [
    { "id": "query", "kind": "speech", "x": 60, "y": 156, "width": 140, "height": 54, "label": "User Query", "fill": "#0f172a", "stroke": "#38bdf8", "flat": true },
    { "id": "retrieve", "kind": "double_rect", "x": 280, "y": 156, "width": 180, "height": 54, "label": "Retrieve Chunks", "sublabel": "select evidence", "fill": "#111827", "stroke": "#a855f7", "glow": "purple" },
    { "id": "generate", "kind": "rect", "x": 540, "y": 156, "width": 180, "height": 54, "label": "Generate Answer", "sublabel": "compose + cite", "fill": "#111827", "stroke": "#f97316" },
    { "id": "grounded", "kind": "speech", "x": 790, "y": 156, "width": 110, "height": 54, "label": "Grounded", "sublabel": "final reply", "fill": "#0f172a", "stroke": "#34d399", "flat": true, "glow": "green" },
    { "id": "agent", "kind": "double_rect", "x": 100, "y": 360, "width": 180, "height": 60, "label": "Agent", "sublabel": "plan + tool policy", "fill": "#111827", "stroke": "#34d399", "glow": "green" },
    { "id": "terminal", "kind": "terminal", "x": 390, "y": 360, "width": 180, "height": 60, "label": "Terminal", "fill": "#111111", "stroke": "#475569", "header_fill": "#222222" },
    { "id": "knowledge", "kind": "cylinder", "x": 700, "y": 360, "width": 180, "height": 60, "label": "Knowledge Base", "sublabel": "embeddings", "fill": "#0f172a", "stroke": "#38bdf8", "glow": "blue" },
    { "id": "documents", "kind": "folder", "x": 390, "y": 520, "width": 180, "height": 50, "label": "Source Documents", "fill": "#3f2a00", "stroke": "#f59e0b", "line_stroke": "#fcd34d" }
  ],
  "arrows": [
    { "id": "request", "source": "query", "target": "retrieve", "source_port": "right", "target_port": "left", "flow": "control", "motion_role": "ingress", "motion_stage": 1, "motion_order": 0, "label": "request" },
    { "id": "draft", "source": "retrieve", "target": "generate", "source_port": "right", "target_port": "left", "flow": "data", "motion_role": "grounding", "motion_stage": 6, "motion_order": 1, "label": "context" },
    { "id": "answer", "source": "generate", "target": "grounded", "source_port": "right", "target_port": "left", "flow": "feedback", "motion_role": "answer", "motion_stage": 7, "motion_order": 0, "label": "answer" },
    { "id": "delegate", "source": "retrieve", "target": "agent", "source_port": "bottom", "target_port": "top", "flow": "control", "motion_role": "delegate", "motion_stage": 2, "motion_order": 0, "label": "delegate" },
    { "id": "tool-call", "source": "agent", "target": "terminal", "source_port": "right", "target_port": "left", "flow": "read", "motion_role": "tool-call", "motion_stage": 3, "motion_order": 0, "label": "tool call" },
    { "id": "inspect", "source": "terminal", "target": "documents", "source_port": "bottom", "target_port": "top", "flow": "read", "motion_role": "inspect", "motion_stage": 4, "motion_order": 0, "label": "inspect" },
    { "id": "index", "source": "documents", "target": "knowledge", "source_port": "right", "target_port": "bottom", "flow": "write", "motion_role": "index", "motion_stage": 5, "motion_order": 0, "label": "index" },
    { "id": "ground", "source": "knowledge", "target": "generate", "source_port": "top", "target_port": "bottom", "flow": "data", "motion_role": "grounding", "motion_stage": 6, "motion_order": 0, "label": "ground" }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 650,
  "legend_locked": true,
  "legend": [
    { "flow": "control", "label": "control" },
    { "flow": "read", "label": "tool read" },
    { "flow": "write", "label": "index write" },
    { "flow": "data", "label": "grounding data" }
  ],
  "footer": "Style 2 · Dark Terminal · grounded tool execution",
  "footer_x": 48,
  "footer_y": 700
}
references/icons.md
# Icon Reference

## Rules for Renderer Compatibility (cairosvg / rsvg-convert)

**Never use** `@import url()` for icon fonts — neither cairosvg nor rsvg-convert fetches external resources.
**Always use** inline SVG `<path>`, `<circle>`, `<rect>`, `<text>` combinations.
**Font fallback**: embed font-family in `<style>` using system fonts only.

---

## Generic Semantic Shapes (No product — use these first)

### Database / Vector Store (cylinder)
```xml
<!-- cx=center-x, top=top-y, w=width, h=height -->
<!-- Typical: w=80, h=70 -->
<ellipse cx="cx" cy="top" rx="w/2" ry="w/6" fill="fill" stroke="stroke" stroke-width="1.5"/>
<rect x="cx-w/2" y="top" width="w" height="h" fill="fill" stroke="none"/>
<line x1="cx-w/2" y1="top" x2="cx-w/2" y2="top+h" stroke="stroke" stroke-width="1.5"/>
<line x1="cx+w/2" y1="top" x2="cx+w/2" y2="top+h" stroke="stroke" stroke-width="1.5"/>
<!-- Optional inner rings for Vector Store -->
<ellipse cx="cx" cy="top+h*0.33" rx="w/2" ry="w/6" fill="none" stroke="stroke" stroke-width="0.7" opacity="0.5"/>
<ellipse cx="cx" cy="top+h*0.66" rx="w/2" ry="w/6" fill="none" stroke="stroke" stroke-width="0.7" opacity="0.5"/>
<ellipse cx="cx" cy="top+h" rx="w/2" ry="w/6" fill="fill-dark" stroke="stroke" stroke-width="1.5"/>
```

### LLM / Model Node (rounded rect with spark)
```xml
<!-- Rounded rect with double border = "intelligent" signal -->
<rect x="x" y="y" width="w" height="h" rx="10" fill="fill" stroke="stroke-outer" stroke-width="2.5"/>
<rect x="x+3" y="y+3" width="w-6" height="h-6" rx="8" fill="none" stroke="stroke-inner" stroke-width="0.8" opacity="0.5"/>
<!-- Spark icon (⚡) as text or small lightning path -->
<text x="cx" y="cy-6" text-anchor="middle" font-size="14">⚡</text>
<text x="cx" y="cy+10" text-anchor="middle" fill="text-color" font-size="13" font-weight="600">GPT-4o</text>
```

### Agent / Orchestrator (hexagon)
```xml
<!-- r = circumradius, cx/cy = center -->
<!-- For r=36: points at 36,0  18,31.2  -18,31.2  -36,0  -18,-31.2  18,-31.2 -->
<polygon points="cx,cy-r  cx+r*0.866,cy-r*0.5  cx+r*0.866,cy+r*0.5  cx,cy+r  cx-r*0.866,cy+r*0.5  cx-r*0.866,cy-r*0.5"
         fill="fill" stroke="stroke" stroke-width="1.5"/>
<text x="cx" y="cy+5" text-anchor="middle" fill="text" font-size="12" font-weight="600">Agent</text>
```

### Memory Node (short-term, dashed border)
```xml
<rect x="x" y="y" width="w" height="h" rx="8"
      fill="fill" stroke="stroke" stroke-width="1.5" stroke-dasharray="6,3"/>
<text x="cx" y="cy-6" text-anchor="middle" fill="text" font-size="10" opacity="0.7">MEMORY</text>
<text x="cx" y="cy+8" text-anchor="middle" fill="text" font-size="13">Short-term</text>
```

### Tool / Function Call (rect with gear symbol)
```xml
<rect x="x" y="y" width="w" height="h" rx="6" fill="fill" stroke="stroke" stroke-width="1.5"/>
<!-- Gear: simplified as ⚙ unicode or small circle with lines -->
<text x="cx" y="cy-4" text-anchor="middle" font-size="16">⚙</text>
<text x="cx" y="cy+12" text-anchor="middle" fill="text" font-size="12">Tool Name</text>
```

### Queue / Stream (horizontal pipe)
```xml
<!-- Pipe tube: left cap ellipse + body + right cap ellipse -->
<ellipse cx="x1" cy="cy" rx="ry*0.6" ry="ry" fill="fill-dark" stroke="stroke" stroke-width="1.5"/>
<rect x="x1" y="cy-ry" width="x2-x1" height="ry*2" fill="fill" stroke="none"/>
<line x1="x1" y1="cy-ry" x2="x2" y2="cy-ry" stroke="stroke" stroke-width="1.5"/>
<line x1="x1" y1="cy+ry" x2="x2" y2="cy+ry" stroke="stroke" stroke-width="1.5"/>
<ellipse cx="x2" cy="cy" rx="ry*0.6" ry="ry" fill="fill-light" stroke="stroke" stroke-width="1.5"/>
```

### User / Human Actor
```xml
<!-- Head -->
<circle cx="cx" cy="cy-18" r="10" fill="fill" stroke="stroke" stroke-width="1.2"/>
<!-- Body / shoulders -->
<path d="M cx-14,cy+16 Q cx-14,cy-4 cx,cy-4 Q cx+14,cy-4 cx+14,cy+16"
      fill="fill" stroke="stroke" stroke-width="1.2"/>
<text x="cx" y="cy+30" text-anchor="middle" fill="text" font-size="12">User</text>
```

### API Gateway (hexagon, single border, smaller)
```xml
<polygon points="cx,cy-28  cx+24,cy-14  cx+24,cy+14  cx,cy+28  cx-24,cy+14  cx-24,cy-14"
         fill="fill" stroke="stroke" stroke-width="1.5"/>
<text x="cx" y="cy+5" text-anchor="middle" fill="text" font-size="11">API</text>
```

### Browser / Web Client
```xml
<rect x="x" y="y" width="w" height="h" rx="6" fill="fill" stroke="stroke" stroke-width="1.5"/>
<!-- Title bar -->
<rect x="x" y="y" width="w" height="20" rx="6" fill="fill-dark" stroke="none"/>
<rect x="x" y="y+14" width="w" height="6" fill="fill-dark"/>
<!-- Traffic light dots -->
<circle cx="x+12" cy="y+10" r="4" fill="#ef4444" opacity="0.8"/>
<circle cx="x+24" cy="y+10" r="4" fill="#f59e0b" opacity="0.8"/>
<circle cx="x+36" cy="y+10" r="4" fill="#10b981" opacity="0.8"/>
```

### Document / File
```xml
<!-- Folded corner rectangle -->
<path d="M x,y L x+w-12,y L x+w,y+12 L x+w,y+h L x,y+h Z"
      fill="fill" stroke="stroke" stroke-width="1.5"/>
<!-- Fold -->
<path d="M x+w-12,y L x+w-12,y+12 L x+w,y+12" fill="fill-dark" stroke="stroke" stroke-width="1"/>
<!-- Lines inside -->
<line x1="x+8" y1="y+h*0.45" x2="x+w-8" y2="y+h*0.45" stroke="stroke" stroke-width="1" opacity="0.5"/>
<line x1="x+8" y1="y+h*0.6"  x2="x+w-8" y2="y+h*0.6"  stroke="stroke" stroke-width="1" opacity="0.5"/>
<line x1="x+8" y1="y+h*0.75" x2="x+w-16" y2="y+h*0.75" stroke="stroke" stroke-width="1" opacity="0.5"/>
```

### Decision Diamond (flowcharts)
```xml
<!-- cx/cy = center, hw = half-width, hh = half-height -->
<polygon points="cx,cy-hh  cx+hw,cy  cx,cy+hh  cx-hw,cy"
         fill="fill" stroke="stroke" stroke-width="1.5"/>
<text x="cx" y="cy+5" text-anchor="middle" fill="text" font-size="12">Condition?</text>
```

### Swim Lane Container
```xml
<!-- Background band for a layer/group -->
<rect x="x" y="y" width="w" height="h" rx="6"
      fill="fill" fill-opacity="0.04" stroke="stroke" stroke-width="1" stroke-dasharray="6,4"/>
<!-- Layer label top-left -->
<text x="x+12" y="y+16" fill="label-color" font-size="10" font-weight="600" letter-spacing="0.06em">LAYER NAME</text>
```

---

## Product Icons (Brand Colors + Inline SVG)

All use circle badge + text abbreviation pattern. Replace `cx`, `cy` with actual coordinates.

### AI / ML Products

| Product | Color | Badge Text |
|---------|-------|-----------|
| OpenAI / ChatGPT | `#10A37F` | `OAI` |
| Anthropic / Claude | `#D97757` | `Claude` |
| Google Gemini | `#4285F4` | `Gemini` |
| Meta LLaMA | `#0467DF` | `LLaMA` |
| Mistral | `#FF7000` | `Mistral` |
| Cohere | `#39594D` | `Cohere` |
| Groq | `#F55036` | `Groq` |
| Together AI | `#6366F1` | `Together` |
| Replicate | `#191919` | `Rep` |
| Hugging Face | `#FFD21E` (text dark) | `HF` |

**Template:**
```xml
<circle cx="cx" cy="cy" r="22" fill="BRAND_COLOR"/>
<text x="cx" y="cy+5" text-anchor="middle" fill="white"
      font-size="10" font-weight="700" font-family="Helvetica">BADGE_TEXT</text>
<!-- Optional: outer ring for "AI" products -->
<circle cx="cx" cy="cy" r="24" fill="none" stroke="BRAND_COLOR" stroke-width="1" opacity="0.4"/>
```

### AI Memory & RAG Products

| Product | Color | Badge |
|---------|-------|-------|
| Mem0 | `#6366F1` | `mem0` |
| LangChain | `#1C3C3C` | `🦜` or `LC` |
| LlamaIndex | `#8B5CF6` | `LI` |
| LangGraph | `#1C3C3C` | `LG` |
| CrewAI | `#EF4444` | `Crew` |
| AutoGen | `#0078D4` | `AG` |
| Haystack | `#FF6D00` | `🌾` or `HS` |
| DSPy | `#7C3AED` | `DSPy` |

### Vector Databases

| Product | Color | Badge |
|---------|-------|-------|
| Pinecone | `#1C1C2E` + green | `Pine` |
| Weaviate | `#FA0050` | `Wea` |
| Qdrant | `#DC244C` | `Qdrant` |
| Chroma | `#FF6B35` | `Chr` |
| Milvus | `#00A1EA` | `Milvus` |
| pgvector | `#336791` | `pgv` |
| Faiss | `#0467DF` | `FAISS` |

**Vector DB template (cylinder + badge):**
```xml
<!-- Cylinder shape -->
<ellipse cx="cx" cy="top" rx="40" ry="12" fill="FILL" stroke="STROKE" stroke-width="1.5"/>
<rect x="cx-40" y="top" width="80" height="50" fill="FILL" stroke="none"/>
<line x1="cx-40" y1="top" x2="cx-40" y2="top+50" stroke="STROKE" stroke-width="1.5"/>
<line x1="cx+40" y1="top" x2="cx+40" y2="top+50" stroke="STROKE" stroke-width="1.5"/>
<ellipse cx="cx" cy="top+50" rx="40" ry="12" fill="FILL_DARK" stroke="STROKE" stroke-width="1.5"/>
<!-- Product name -->
<text x="cx" y="top+30" text-anchor="middle" fill="white"
      font-size="11" font-weight="700">Pinecone</text>
```

### Classic Databases & Storage

| Product | Color |
|---------|-------|
| PostgreSQL | `#336791` |
| MySQL | `#4479A1` |
| MongoDB | `#47A248` |
| Redis | `#DC382D` |
| Elasticsearch | `#005571` |
| Cassandra | `#1287B1` |
| Neo4j | `#008CC1` |
| SQLite | `#003B57` |

### Message Queues & Streaming

| Product | Color |
|---------|-------|
| Apache Kafka | `#231F20` |
| RabbitMQ | `#FF6600` |
| AWS SQS | `#FF9900` |
| NATS | `#27AAE1` |
| Pulsar | `#188FFF` |

### Cloud & Infra

| Product | Color |
|---------|-------|
| AWS | `#FF9900` |
| GCP | `#4285F4` |
| Azure | `#0089D6` |
| Cloudflare | `#F48120` |
| Vercel | `#000000` |
| Docker | `#2496ED` |
| Kubernetes | `#326CE5` |
| Terraform | `#7B42BC` |
| Nginx | `#009639` |
| FastAPI | `#009688` |

### Observability

| Product | Color |
|---------|-------|
| Grafana | `#F46800` |
| Prometheus | `#E6522C` |
| Datadog | `#632CA6` |
| LangSmith | `#1C3C3C` |
| Langfuse | `#6366F1` |
| Arize | `#6B48FF` |

---

## Azure Service Icons

Azure brand colour: `#0089D6` (top tile / outer ring).
Service-specific accents come from Microsoft's Azure icon set; use them
as the inner badge fill so a glance still tells you "this is Azure".

**Template (Azure tile):**
```xml
<!-- Azure tile: outer rounded square in Azure blue, inner badge for the service. -->
<rect x="cx-22" y="cy-22" width="44" height="44" rx="6"
      fill="#0089D6" stroke="none"/>
<rect x="cx-19" y="cy-19" width="38" height="38" rx="4"
      fill="SERVICE_COLOR" stroke="none"/>
<text x="cx" y="cy+5" text-anchor="middle" fill="white"
      font-size="9" font-weight="700" font-family="Helvetica">BADGE</text>
```

### Azure Compute

| Product | Service Color | Badge |
|---------|---------------|-------|
| Azure Functions | `#0062AD` | `Func` |
| Azure App Service | `#0072C6` | `App` |
| Azure Container Apps | `#3F8624` | `ACA` |
| Azure Container Instances | `#0078D4` | `ACI` |
| Azure Kubernetes Service (AKS) | `#326CE5` | `AKS` |
| Azure Virtual Machines | `#0078D4` | `VM` |
| Azure Batch | `#0072C6` | `Batch` |
| Azure Spring Apps | `#6DB33F` | `Spring` |

### Azure Data & Analytics

| Product | Service Color | Badge |
|---------|---------------|-------|
| Azure SQL Database | `#0066A1` | `SQL` |
| Azure Cosmos DB | `#3D7AB3` | `Cosmos` |
| Azure Database for PostgreSQL | `#336791` | `pg` |
| Azure Database for MySQL | `#4479A1` | `MySQL` |
| Azure Synapse Analytics | `#0078D4` | `Syn` |
| Azure Data Factory | `#0078D4` | `ADF` |
| Azure Databricks | `#FF3621` | `Bricks` |
| Azure Stream Analytics | `#0072C6` | `Stream` |
| Azure Data Explorer (Kusto) | `#1E5180` | `Kusto` |
| Azure Cache for Redis | `#DC382D` | `Redis` |

### Azure Storage

| Product | Service Color | Badge |
|---------|---------------|-------|
| Azure Blob Storage | `#0078D4` | `Blob` |
| Azure Queue Storage | `#0078D4` | `Queue` |
| Azure Table Storage | `#0078D4` | `Table` |
| Azure Files | `#0078D4` | `Files` |
| Azure Data Lake Storage Gen2 | `#0078D4` | `Lake` |

### Azure AI

| Product | Service Color | Badge |
|---------|---------------|-------|
| Azure OpenAI Service | `#10A37F` | `AOAI` |
| Azure AI Search (Cognitive Search) | `#0078D4` | `AISrch` |
| Azure AI Foundry | `#742774` | `Foundry` |
| Azure Machine Learning | `#0078D4` | `AML` |
| Azure AI Content Safety | `#107C10` | `Safety` |
| Azure Speech / Translator | `#0078D4` | `Speech` |

### Azure Messaging & Eventing

| Product | Service Color | Badge |
|---------|---------------|-------|
| Azure Service Bus | `#0078D4` | `SB` |
| Azure Event Grid | `#0078D4` | `Grid` |
| Azure Event Hubs | `#0078D4` | `Hubs` |
| Azure Notification Hubs | `#0078D4` | `Notif` |
| Azure SignalR Service | `#0078D4` | `SignalR` |

### Azure Networking & Edge

| Product | Service Color | Badge |
|---------|---------------|-------|
| Azure Front Door | `#0078D4` | `AFD` |
| Azure Application Gateway | `#0078D4` | `AppGW` |
| Azure Load Balancer | `#0078D4` | `LB` |
| Azure API Management | `#1FBA9F` | `APIM` |
| Azure Virtual Network | `#0078D4` | `VNet` |
| Azure Private Link | `#0078D4` | `PL` |
| Azure CDN | `#0078D4` | `CDN` |
| Azure DNS | `#0078D4` | `DNS` |

### Azure Identity & Security

| Product | Service Color | Badge |
|---------|---------------|-------|
| Microsoft Entra ID (Azure AD) | `#0072C6` | `Entra` |
| Azure Key Vault | `#FFB900` | `KV` |
| Azure Sentinel | `#0072C6` | `Sentinel` |
| Microsoft Defender for Cloud | `#0078D4` | `Defender` |

### Azure DevOps & Operations

| Product | Service Color | Badge |
|---------|---------------|-------|
| Azure DevOps Pipelines | `#0078D4` | `Pipelines` |
| GitHub Actions (Azure target) | `#181717` | `GHA` |
| Azure Monitor | `#0078D4` | `Monitor` |
| Application Insights | `#0072C6` | `AppI` |
| Azure Log Analytics | `#0078D4` | `Logs` |

### Azure-specific shapes

For diagrams that need a recognisable "Azure" visual without a service
badge — e.g. a region container or a subscription boundary — use a
dashed Azure-blue outline:

```xml
<!-- Azure region/subscription container -->
<rect x="x" y="y" width="w" height="h" rx="8"
      fill="#0089D6" fill-opacity="0.04"
      stroke="#0089D6" stroke-width="1.2" stroke-dasharray="6,4"/>
<text x="x+12" y="y+16" fill="#0089D6" font-size="10"
      font-weight="700" letter-spacing="0.06em">AZURE • REGION NAME</text>
```

---

## Icon Sizing Guide

| Context | Recommended Size | Padding |
|---------|-----------------|---------|
| Node badge (inside box) | 28×28px circle | 10px |
| Standalone icon node | 40×40px | 16px |
| Hero / central node | 56×56px | 20px |
| Small inline indicator | 16×16px | 6px |

## Arrow Marker Templates

```xml
<defs>
  <!-- Standard filled arrow -->
  <marker id="arrow-COLORNAME" markerWidth="10" markerHeight="7"
          refX="9" refY="3.5" orient="auto">
    <polygon points="0 0, 10 3.5, 0 7" fill="COLOR"/>
  </marker>

  <!-- Open arrow (outline only) -->
  <marker id="arrow-open" markerWidth="10" markerHeight="8"
          refX="9" refY="4" orient="auto">
    <path d="M 0 0 L 10 4 L 0 8" fill="none" stroke="COLOR" stroke-width="1.5"/>
  </marker>

  <!-- Circle dot (for association lines) -->
  <marker id="dot" markerWidth="8" markerHeight="8"
          refX="4" refY="4" orient="auto">
    <circle cx="4" cy="4" r="3" fill="COLOR"/>
  </marker>
</defs>
```
references/composition-quality-contract.md
# Composition Quality Contract

This contract applies to every visual style. Style references control color,
typography, material, corner radius, and decorative treatment. They never
weaken geometry or composition quality.

## Official showcase profile

Use `"quality_profile": "showcase"` for official samples and polished delivery
artifacts. The renderer and validator enforce all of these budgets:

| Metric | Showcase limit |
|---|---:|
| Edge crossings | 0 |
| Bridge jumps | 0 |
| Bends on one edge | 2 maximum |
| Bends in the six-node reference topology | 8 maximum |
| Route length / direct Manhattan length | 1.35 maximum |
| Shortest route segment | 16px minimum |
| Node-to-node whitespace | 40px minimum |
| Node-to-container gutter | 20px minimum |
| Edge-label clearance from unrelated geometry | 4px minimum |

The six-node reference topology currently scores 100 with four total bends,
zero crossings, zero bridges, a route-stretch maximum of 1.0, 50px minimum
node spacing, and 20px minimum container gutter.

## Layout grammar

1. Establish containers, their header reservations, and node rows before any
   edge is routed.
2. Keep nodes in a row aligned to the same y coordinate and use consistent
   heights for equivalent semantic roles.
3. Reserve one empty inter-container corridor for cross-layer routes. A route
   may cross a container boundary only through an open gap and must not run on
   top of the border.
4. Route the primary horizontal flow first. Route cross-layer context and
   feedback paths through separate, non-overlapping corridor segments.
5. Use distinct node ports when several edges share a side. Never stack
   multiple arrowheads on one coordinate.
6. Prefer a monotone orthogonal route. If a connection needs more than two
   bends, change the node placement before adding waypoints.
7. Treat every external title, subtitle, tag, side label, edge label, legend,
   footer, and title block as an obstacle with measurable bounds.
8. Keep legends outside business-flow corridors. Use a single horizontal row
   when the canvas has enough width.
9. Fit long single-line node titles to the card width. Do not let text touch or
   cross the node border.
10. If the topology cannot meet the showcase limits, simplify the composition,
    split it into focused diagrams, or explicitly use the standard profile for
    a non-showcase engineering stress artifact.

## Style identity boundary

Styles may vary these properties freely within their own reference:

- background, palette, semantic accent colors;
- font family, title alignment, and typographic hierarchy;
- card fill, border treatment, shadow, glow, and corner radius;
- blueprint title blocks, terminal chrome, or restrained brand details.

Styles must share these structural properties for a direct comparison:

- topology and edge direction;
- row/column alignment;
- port assignment and corridor positions;
- crossing, bridge, bend, stretch, spacing, and gutter budgets;
- semantic SVG roles required by the validator.

Visual effects never create a second business connector. Pencil echoes, rail
casings, critical-path glow, and similar layers use
`data-graph-role="decoration"` with `data-owner`; exactly one ordinary
`data-graph-role="edge"` carries the source, target, route, and arrowhead.

## Validation

Run both gates before delivery:

```bash
python3 scripts/validate_svg.py diagram.svg --check geometry
python3 scripts/validate_svg.py diagram.svg --check composition
```

`scripts/validate-svg.sh` runs both automatically. A successful render without
these gates is still a draft.

Dense legacy diagrams live under `fixtures/stress/`. They preserve routing
pressure cases and are not visual quality references. The public 12-style
showcase keeps a distinct engineering scene for every style. The internal
`fixtures/quality-baseline/` set applies one shared Agent Runtime Architecture
topology to all 11 generator-backed styles; Style 8 remains the static,
AI-authored exception.
package.json
{
  "name": "@yizhiyanhua-ai/fireworks-tech-graph",
  "version": "1.2.0",
  "description": "Agent Skill and CLI for geometry-safe technical diagrams, focused SVG-to-GIF motion, and offline HTML.",
  "keywords": [
    "claude-code",
    "codex",
    "openai-codex",
    "agent-skills",
    "skill",
    "diagram",
    "svg",
    "architecture",
    "flowchart",
    "uml",
    "sequence-diagram",
    "er-diagram",
    "visualization",
    "animation",
    "gif"
  ],
  "author": "yizhiyanhua-ai",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/yizhiyanhua-ai/fireworks-tech-graph.git"
  },
  "bugs": {
    "url": "https://github.com/yizhiyanhua-ai/fireworks-tech-graph/issues"
  },
  "homepage": "https://yizhiyanhua-ai.github.io/fireworks-tech-graph/",
  "bin": {
    "fireworks-tech-graph": "scripts/fireworks.py"
  },
  "scripts": {
    "test": "python3 -m unittest discover -s tests -v",
    "check": "python3 tools/check_project_consistency.py && python3 tools/distribution.py --check",
    "examples": "python3 scripts/fireworks.py examples"
  },
  "files": [
    "SKILL.md",
    "README.md",
    "README.zh.md",
    "LICENSE",
    "CHANGELOG.md",
    "CONTRIBUTING.md",
    "agents/",
    "docs/",
    "examples/",
    "references/",
    "schemas/",
    "scripts/",
    "tests/",
    "fixtures/",
    "templates/",
    "assets/",
    "!**/__pycache__/**",
    "!**/*.pyc"
  ],
  "engines": {
    "node": ">=18.0.0"
  }
}
fixtures/quality-baseline/agent-runtime-style3.json
{
  "schema_version": 1,
  "mode": "architecture",
  "template_type": "architecture",
  "style": 3,
  "quality_profile": "showcase",
  "width": 960,
  "height": 600,
  "title": "Agent Runtime Architecture",
  "subtitle": "orchestration, memory, tools, and evaluation",
  "containers": [
    { "id": "control-plane", "x": 40, "y": 120, "width": 880, "height": 116, "label": "Control Plane", "header_prefix": "01" },
    { "id": "execution-state", "x": 40, "y": 304, "width": 880, "height": 136, "label": "Execution and State", "header_prefix": "02" }
  ],
  "nodes": [
    { "id": "client", "kind": "rect", "x": 60, "y": 156, "width": 160, "height": 60, "label": "Client", "sublabel": "request + policy", "fill": "#0b3b5e", "stroke": "#94a3b8", "flat": true },
    { "id": "gateway", "kind": "double_rect", "x": 300, "y": 156, "width": 180, "height": 60, "label": "Gateway", "sublabel": "auth + routing", "fill": "#0b3b5e", "stroke": "#fde047" },
    { "id": "agent", "kind": "double_rect", "x": 560, "y": 156, "width": 180, "height": 60, "label": "Agent Runtime", "sublabel": "plan + inference", "fill": "#0b3b5e", "stroke": "#c084fc" },
    { "id": "memory", "kind": "rect", "x": 300, "y": 344, "width": 180, "height": 60, "label": "Vector Memory", "sublabel": "retrieve + persist", "fill": "#0b3b5e", "stroke": "#38bdf8", "flat": true },
    { "id": "tools", "kind": "rect", "x": 560, "y": 344, "width": 180, "height": 60, "label": "Tool Runtime", "sublabel": "MCP + functions", "fill": "#0b3b5e", "stroke": "#34d399", "flat": true },
    { "id": "eval", "kind": "rect", "x": 790, "y": 344, "width": 110, "height": 60, "label": "Trace + Eval", "sublabel": "quality gate", "fill": "#0b3b5e", "stroke": "#fb7185", "flat": true }
  ],
  "arrows": [
    { "id": "request", "source": "client", "target": "gateway", "source_port": "right", "target_port": "left", "flow": "control", "label": "request" },
    { "id": "dispatch", "source": "gateway", "target": "agent", "source_port": "right", "target_port": "left", "flow": "control", "label": "dispatch" },
    { "id": "context", "source": "agent", "target": "memory", "source_port": "bottom", "target_port": "top", "flow": "read", "label": "context", "route_points": [[632, 270], [390, 270]] },
    { "id": "tool-call", "source": "agent", "target": "tools", "source_port": "bottom", "target_port": "top", "flow": "async", "label": "tool call", "dashed": true },
    { "id": "augment", "source": "memory", "target": "tools", "source_port": "right", "target_port": "left", "flow": "write", "label": "augment" },
    { "id": "trace", "source": "tools", "target": "eval", "source_port": "right", "target_port": "left", "flow": "data", "label": "trace" },
    { "id": "z-feedback", "source": "eval", "target": "agent", "source_port": "top", "target_port": "bottom", "flow": "feedback", "label": "feedback", "dashed": true, "route_points": [[845, 270], [668, 270]] }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 510,
  "legend_locked": true,
  "legend": [
    { "flow": "control", "label": "primary" },
    { "flow": "write", "label": "data" },
    { "flow": "async", "label": "dependency" }
  ],
  "blueprint_title_block": {
    "title": "AGENT RUNTIME",
    "subtitle": "SYSTEM ARCHITECTURE",
    "center_caption": "STYLE 3",
    "left_caption": "REV: 1.1",
    "right_caption": "DWG: AGT-001",
    "width": 220,
    "height": 76,
    "x": 700,
    "y": 500
  },
  "footer": "Style 3 · Blueprint · showcase composition contract",
  "footer_x": 48,
  "footer_y": 570
}
fixtures/system-architecture-style6.json
{
  "schema_version": 1,
  "mode": "architecture",
  "template_type": "architecture",
  "style": 6,
  "motion_scene": "governed-runtime",
  "quality_profile": "showcase",
  "width": 960,
  "height": 700,
  "title": "System Architecture",
  "subtitle": "warm hierarchy for interface, orchestration, inference, safety, and operations",
  "containers": [
    { "id": "interface", "x": 40, "y": 120, "width": 880, "height": 110, "label": "Interface", "stroke": "#ded8cf", "fill": "none" },
    { "id": "core", "x": 40, "y": 280, "width": 880, "height": 130, "label": "Core Runtime", "stroke": "#ded8cf", "fill": "none" },
    { "id": "foundation", "x": 40, "y": 450, "width": 880, "height": 140, "label": "Foundation", "stroke": "#ded8cf", "fill": "none" }
  ],
  "nodes": [
    { "id": "client", "kind": "rect", "x": 80, "y": 156, "width": 180, "height": 54, "label": "Client Surface", "sublabel": "web + product UI", "fill": "#e9f1fb", "stroke": "#8c6f5a", "flat": true },
    { "id": "gateway", "kind": "double_rect", "x": 340, "y": 156, "width": 200, "height": 54, "label": "Gateway", "sublabel": "auth + routing", "fill": "#f7ecda", "stroke": "#d97757" },
    { "id": "planner", "kind": "rect", "x": 80, "y": 340, "width": 180, "height": 50, "label": "Task Planner", "sublabel": "decompose + schedule", "fill": "#f7ecda", "stroke": "#d0b893", "flat": true },
    { "id": "runtime", "kind": "double_rect", "x": 340, "y": 340, "width": 200, "height": 50, "label": "Model Runtime", "sublabel": "inference core", "fill": "#c8e4db", "stroke": "#8c6f5a" },
    { "id": "guardrails", "kind": "rect", "x": 740, "y": 340, "width": 160, "height": 50, "label": "Guardrails", "sublabel": "policy + safety", "fill": "#efe8de", "stroke": "#d0c3b3", "flat": true },
    { "id": "memory", "kind": "cylinder", "x": 90, "y": 510, "width": 160, "height": 60, "label": "Memory", "sublabel": "context + recall", "fill": "#e5e8df", "stroke": "#7b8b5c" },
    { "id": "tools", "kind": "rect", "x": 360, "y": 510, "width": 160, "height": 60, "label": "Tool Runtime", "sublabel": "actions", "fill": "#f7ecda", "stroke": "#d0b893", "flat": true },
    { "id": "observability", "kind": "rect", "x": 580, "y": 510, "width": 120, "height": 60, "label": "Observability", "sublabel": "traces", "fill": "#efe8de", "stroke": "#d0c3b3", "flat": true },
    { "id": "registry", "kind": "rect", "x": 760, "y": 510, "width": 120, "height": 60, "label": "Registry", "sublabel": "rollouts", "fill": "#efe8de", "stroke": "#d0c3b3", "flat": true }
  ],
  "arrows": [
    { "id": "request", "source": "client", "target": "gateway", "source_port": "right", "target_port": "left", "flow": "read", "motion_role": "ingress", "motion_stage": 1, "motion_order": 0, "label": "request" },
    { "id": "dispatch", "source": "gateway", "target": "runtime", "source_port": "bottom", "target_port": "top", "flow": "control", "motion_role": "dispatch", "motion_stage": 2, "motion_order": 0, "label": "dispatch" },
    { "id": "plan", "source": "runtime", "target": "planner", "source_port": "left", "target_port": "right", "flow": "control", "motion_role": "runtime-branch", "motion_stage": 3, "motion_order": 0, "label": "plan" },
    { "id": "enforce", "source": "runtime", "target": "guardrails", "source_port": "right", "target_port": "left", "flow": "control", "motion_role": "runtime-branch", "motion_stage": 3, "motion_order": 1, "label": "enforce" },
    { "id": "remember", "source": "planner", "target": "memory", "source_port": "bottom", "target_port": "top", "flow": "write", "motion_role": "foundation", "motion_stage": 4, "motion_order": 0, "label": "remember" },
    { "id": "act", "source": "runtime", "target": "tools", "source_port": "bottom", "target_port": "top", "flow": "read", "motion_role": "runtime-branch", "motion_stage": 3, "motion_order": 2, "label": "act" },
    { "id": "register", "source": "guardrails", "target": "registry", "source_port": "bottom", "target_port": "top", "flow": "feedback", "motion_role": "foundation", "motion_stage": 4, "motion_order": 1, "label": "release" },
    { "id": "trace", "source": "tools", "target": "observability", "source_port": "right", "target_port": "left", "flow": "feedback", "motion_role": "foundation", "motion_stage": 4, "motion_order": 2, "label": "trace" },
    { "id": "promote", "source": "observability", "target": "registry", "source_port": "right", "target_port": "left", "flow": "feedback", "motion_role": "promote", "motion_stage": 5, "motion_order": 0, "label": "promote" }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 625,
  "legend_locked": true,
  "legend": [
    { "flow": "read", "label": "request / action" },
    { "flow": "control", "label": "orchestration" },
    { "flow": "write", "label": "memory write" },
    { "flow": "feedback", "label": "operations" }
  ],
  "footer": "Style 6 · Claude Official · restrained system hierarchy",
  "footer_x": 48,
  "footer_y": 680
}
references/motion-effects.md
# Focused SVG-to-GIF Motion

Motion has one public contract:

```text
generated semantic SVG -> validated animated GIF
```

PNG and JPEG are not accepted as animation inputs. Animated WebP, MP4, WebM,
poster generation, and the old offline motion-review player are not part of this
surface. Static SVG/PNG rendering and the single-file interactive HTML exporter
remain unchanged.

## Simplest prompts

Use the most recently generated SVG when the user says:

- `让这张图动起来`
- `生成 GIF`
- `制作 GIF`
- `把刚才的 SVG 转成 GIF`
- `Generate a GIF`
- `Animate this diagram`
- `Animate this SVG as a GIF`

No format, preset, duration, or frame-rate wording is needed. The default is a
960px-wide, 5.75-second, 20fps, 115-frame, infinitely looping GIF. `auto` reads the
style id and explicit `data-motion-*` edge metadata from the generated SVG.

The input must satisfy one of the twelve approved motion-scene contracts. Exact
source bytes are not pinned, so validated title and content variants of a
supported topology are accepted. The role/stage/order schedule, route direction,
required source colors, and geometry remain fail-closed; `auto` does not infer
missing metadata or apply reviewed motion to an arbitrary same-style topology.
The only motion media artifact is GIF. Unless `--report` supplies another path,
the CLI also writes `<output>.motion.json` with the validation evidence.

## ByteByteGo-derived rules

Six measured ByteByteGo newsletter GIFs use short 2.10–2.75 second loops at a
uniform 20 or 33.33fps. Only 1.20–3.56% of their canvases change over a loop and
every measured frame is unique. The default loop extends to 5.75 seconds so the
accepted 1.8-second construction leaves frames 38–109 for a clearly readable,
continuously moving operating state. This project applies those lessons as a reusable technical-diagram contract:

1. Frame 0 keeps nodes and regions readable while every connector is absent.
2. Existing routes draw on in semantic order, then settle with their direction markers.
3. Nodes, labels, containers, marker geometry, and the camera remain fixed.
4. Connector overlap is scene-pinned: Styles 4 and 12 use one active build, plans that call for two enforce two, and Style 7's exact parallel governance schedule reaches three. The completed topology gets a noticeable operating hold.
5. Samples use a uniform GIF-native cadence; no 6/7-centisecond alternation.
6. The final slot is a real animation sample, never a copied opening frame.
7. Every settled route carries marker-free operating motion toward the target. Styles 1–4 retain their approved packet-head, terminal-evidence, Blueprint-bead, and Notion-card identities. Styles 5–12 use a separate rail/signature family per scene and remain legible at 50% review size.

## Approved default gate

Styles 1–12 are enabled and their signature, speed, path, geometry, and
construction contracts are user-approved. The shared `+2s-settled-flow`
timing revision was approved on 2026-07-17, so new default 5.75-second packages
record `review_status: user-approved`. The explicit 3.75-second and 2.75-second
compatibility timelines remain available when requested.

### Style 1 — Memory Weave

Style 1 follows eight explicit route roles:

| Frames | Role | Edge meaning |
|---:|---|---|
| `1–8` | `ingress` | user request |
| `5–12` | `reason` | application to model |
| `9–16` | `extract` | fact extraction |
| `13–20` | `transform` | extracted facts |
| `17–24` | `resolve` | conflict resolution |
| `21–28` | `memory-write` | durable write |
| `25–32` | `memory-read` | memory relation/read |
| `29–36` | `response-context` | context to response |

The `connector-draw-on-with-persistent-data-flow` primitive transiently hides all immutable
source edges and their route-owned label groups. Each route gets one marker-free
linear draw clone and one settled clone that restores the original marker at
arrival. Its label plate and text appear only with that settled route; label
geometry never moves. The topology is complete by frame 36. From frames `36–114`,
every settled route gets an adjacent marker-free, filter-free pair that preserves
the source path direction. The `persistent-data-flow-stream` body uses memory-cyan
`#06b6d4`, rounded caps and joins, opacity `.90`, width
`min(4.0px, max(3.0px, source stroke × 1.60))`, and dash pattern `16 25`. Style 1's
current 2.4px source routes therefore resolve to exactly 3.84px. Its immediately
following `persistent-data-flow-head` clone stays inside that body with color
`#e0f2fe`, width `2.20px`, opacity `.98`, rounded caps and joins, and pattern
`6 35`. Both patterns have a 41-unit period; the head dash offset is always the
body offset minus 10 units, placing the bright six-unit segment over the leading
six units of the cyan body. Neither clone carries a marker, filter, glow, or blur.

Both layers advance together by `-6.0` SVG user units per rendered frame toward
the target: 6px/frame or 120px/s at 960px, and 3px/frame or 60px/s at 50% review
scale. Their shared phase is `(motionStage × 7 + motionOrder × 3) mod 41`; the
request/reason/extract/facts/resolved/write/relate/context phase order is
`[7, 14, 21, 28, 31, 35, 1, 8]`. Period 41 and step 6 are coprime, so the 39
stream samples do not repeat a phase. Frames 36, 37, and 38 use pair-layer factors
`.30`, `.65`, and `1.00`; frames `38–109` hold full body/head opacities `.90`/`.98`.
Frames `110–114` keep both layers advancing while multiplying settled topology,
labels, body, and head opacity by `1.00`, `.7575`, `.515`, `.2725`, and `.03`.
Sampling uses `time × fps − 0.5`, so frames `0–35` match the accepted raw Chromium
baseline exactly.
This keeps the reset in motion and leaves frame 74 distinct from the connector-free
frame 0. Runtime geometry sentinels also fail closed unless `ingress` travels right,
`resolve` travels left, and `memory-write` follows down → left → down while the
dash offset remains negative; declaring source/target metadata alone is not enough.

### Style 2 — Dark Terminal evidence trace

Style 2 selects the `tool-grounding` scene by SVG style metadata. Its schedule is
keyed by the exact `(data-motion-role, data-motion-order)` pair, so two grounding
routes remain independently addressable and every expected key must appear exactly
once. The source edge stage and route color are validated before rendering.

| Frames | Stage | Role / order | Evidence transition | Body color |
|---:|---:|---|---|---|
| `1–8` | `1` | `ingress / 0` | query enters the application | `#a855f7` |
| `5–12` | `2` | `delegate / 0` | application delegates to the model | `#a855f7` |
| `9–16` | `3` | `tool-call / 0` | model issues the terminal command | `#38bdf8` |
| `13–20` | `4` | `inspect / 0` | terminal output is inspected | `#38bdf8` |
| `17–24` | `5` | `index / 0` | inspected evidence enters the index | `#22c55e` |
| `21–28` | `6` | `grounding / 0` | indexed evidence returns to grounding | `#fb7185` |
| `25–32` | `6` | `grounding / 1` | grounding reaches the answer context | `#fb7185` |
| `29–36` | `7` | `answer / 0` | grounded answer returns to the user | `#f97316` |

After each route settles, it receives one adjacent, marker-free, filter-free pair.
The `terminal-evidence-stream` body inherits that source route's stroke, uses width
`min(3.8px, max(3.0px, source stroke × 1.50))`, opacity `.94`, and dash pattern
`15 26`. Style 2's 2.3px routes resolve to exactly 3.45px. The immediately following
`terminal-command-head` is white `#f8fafc`, 2.0px wide, opacity `1.00`, and uses
pattern `5 36`; its offset is the body offset minus 10 units. Both advance by
`-6.0` SVG user units per rendered frame, equal to 6px/frame at 960px and 3px/frame
at 50% review size. The phase formula is
`(motionStage × 6 + motionOrder × 3) mod 41`, producing
`[6, 12, 18, 24, 30, 36, 39, 1]` in schedule order. Period 41 and step 6 are
coprime, so all 39 stream samples remain phase-unique.

All eight directions are runtime sentinels: `ingress / 0` right;
`delegate / 0` down → left → down; `tool-call / 0` right; `inspect / 0` down;
`index / 0` right → up; `grounding / 0` up → left → up; `grounding / 1` right;
and `answer / 0` right. The body/head pair fades in on frames `36–38`, holds full
opacity on frames `38–109`, then keeps moving while sharing the five-frame reset.

One scene signature appears over the terminal prompt: `terminal-prompt-cursor`.
The renderer requires exactly one `data-node-id="terminal"` node and exactly one
unchanged `_` source text within it. A 2.2px-high `#a7f3d0` rectangle is derived
from that underscore's runtime `getBBox()` and placed in a separate signature
layer. It changes opacity only: from frames `16–69`, each 16-frame period has eight
bright frames at `.95` followed by eight absent frames. Its cadence runs through
`reset_start - 1`; frames `110–114` multiply a
bright cursor by the shared reset opacity. The source underscore remains visible
and unmutated. Terminal text typing, scan lines, glows, camera motion, and animated
backgrounds are outside this scene contract.

### Style 3 — Blueprint distribution wave

Style 3 selects the `service-blueprint` scene. SHA-256
`b8f55d9ea0c6111176d8ff50d2e844b2001ee5087a3940621e635e1b875d470d`
remains the reviewed reference source, not an exact-byte input lock. Its schedule
is keyed by the exact `(data-motion-role, data-motion-order)` pair.
Every key resolves to one immutable source edge, and every source stage and route
color is validated before rendering.

| Frames | Stage | Role / order | Blueprint transition | Body color |
|---:|---:|---|---|---|
| `1–6` | `1` | `ingress / 0` | client request enters the gateway | `#38bdf8` |
| `4–9` | `2` | `policy / 0` | gateway checks identity and policy | `#67e8f9` |
| `8–13` | `3` | `fanout / 0` | gateway opens the Order branch | `#38bdf8` |
| `11–16` | `3` | `fanout / 1` | gateway opens the Catalog branch | `#38bdf8` |
| `14–19` | `3` | `fanout / 2` | gateway opens the Billing branch | `#38bdf8` |
| `18–23` | `4` | `data-write / 0` | Order writes Postgres | `#fde047` |
| `21–26` | `4` | `data-write / 1` | Catalog writes Redis | `#fde047` |
| `24–29` | `4` | `data-write / 2` | Billing writes Warehouse | `#fde047` |
| `28–33` | `5` | `event / 0` | Billing publishes an event | `#fb7185` |
| `31–36` | `6` | `telemetry / 0` | Event Router emits metrics | `#fb7185` |

From frame 36, each settled route receives one marker-free, filter-free
`blueprint-distribution-wave` body. It inherits the source stroke, uses width
`min(3.4px, max(2.8px, source stroke × 1.40))`, opacity `.92`, rounded caps and
joins, and dash pattern `12 31`. The reviewed 2.1px routes resolve to exactly
2.94px, or 1.47px at 50% review scale. Dash offset advances by `-6.0` SVG units
per rendered frame.

Each body is paired with one `blueprint-registration-bead`: a circle with radius
3.0px, fill `#e0f2fe`, source-colored 1.2px stroke, and opacity `.98`. Its initial
path distance is the route phase. Every rendered frame advances `cx` and `cy` by
`+6.0` user units through `getPointAtLength`; the bead wraps from the target end
to the source start and never reverses. Only `cx`, `cy`, and `opacity` change.

The stage-only phase formula is
`(motionStage × 7 + motionOrder × 0) mod 43`, producing
`[7, 14, 21, 21, 21, 28, 28, 28, 35, 42]`. All three fan-out routes therefore
share phase 21, and all three equal-length data-write routes share phase 28 and
the same bead Y coordinate throughout the live interval. Period 43 and step 6
are coprime, so the 39 live samples do not repeat a body phase.

The ten runtime direction sentinels require: ingress right; policy right;
fan-out/0 down → left → down; fan-out/1 down; fan-out/2 down → right → down;
all three data writes down; event right; and telemetry down. Body dash offset
must stay negative while bead travel stays positive. Bodies and beads are absent
through frame 35, fade in with `.30`, `.65`, and `1.00` factors on frames
`36–38`, hold full opacity on frames `38–109`, then keep advancing through the
five-frame reset. Glow, blur, shadow, scan lines, node/text/camera/background
motion, halo, ripple, and terminal cursors remain outside the Style 3 contract.

### Style 4 — Notion memory-card handoff

Style 4 selects the `memory-lifecycle` scene. SHA-256
`04cf833659e82c3e1743db4042cacf839a6d784a99c32d076e36fd4776e70c1b`
remains the reviewed reference source, not an exact-byte input lock. Its exact
`(data-motion-role, data-motion-order)` schedule allows only one active
connector build:

| Frames | Stage | Role / order | Memory transition | Rail/card color |
|---:|---:|---|---|---|
| `1–4` | `1` | `sample / 0` | sensory input enters working memory | `#3b82f6` |
| `5–8` | `2` | `attend / 0` | active context reaches the agent | `#3b82f6` |
| `9–12` | `3` | `invoke / 0` | the agent invokes procedural memory | `#7c3aed` |
| `13–22` | `4` | `remember / 0` | working context enters episodic memory | `#059669` |
| `23–26` | `5` | `consolidate / 0` | episodes become semantic knowledge | `#ea580c` |
| `27–36` | `6` | `recall / 0` | semantic knowledge returns to the agent | `#ea580c` |

From frame 36, all six settled paths receive a marker-free, filter-free
`notion-memory-rail`. Each rail uses destination-memory color, width
`min(3.0px, max(2.4px, source stroke × 1.50))`, opacity `.88`, round caps and
joins, and dash pattern `12 35`. The reviewed 1.8px paths resolve to exactly
2.70px, or 1.35px at 50% review size. The phase formula
`(motionStage × 7 + motionOrder × 0) mod 47` produces
`[7, 14, 21, 28, 35, 42]`; dash offset advances by `-6.0` user units per frame.
Period 47 and step 6 are coprime, so none of the 39 live rail samples repeats.

Each rail is paired with one `notion-memory-card` group. Its white outer rect is
`x=-7`, `y=-5`, `14×10`, `rx=2`, with a 1.4px semantic-color stroke. Two 2px
butt-cap ink lines use `shape-rendering="crispEdges"` and run from `(-4.5,-2)`
to `(4,-2)` and from `(-4.5,2)` to `(0.5,2)`. The exact normalized starting-progress vector is
`[0.08, 0.22, 0.36, 0.50, 0.64, 0.78]`; distance is
`8 + progress × (pathLength − 16)`. Cards keep 8px clearance from both endpoints,
advance `+6.0` path units per rendered frame, wrap from target clearance to
source clearance, and animate only group `transform` and `opacity`.

Direction sentinels require sample, attend, and invoke to point right at `0°`;
remember to point down at `90°`; consolidate to point right at `0°`; and recall
to point up at `-90°`. Rails and cards are absent through frame 35, fade in with
`.30`, `.65`, and `1.00` factors on frames `36–38`, hold full opacity on frames
`38–109`, and keep advancing during the shared five-frame reset. Cards and rails
remain below route labels and nodes. Generic packet heads, circular beads,
terminal cursors, glow, blur, shadow, scan lines, node/text/camera/background
motion, halo, and ripple remain outside the Style 4 contract.

### Styles 5–12 — approved signatures and shared timing

All eight approved style contracts preserve immutable source paths, labels, nodes, containers,
text, and markers. They reuse the connector-free opening, exact semantic draw-on,
frames `36–38` live fade, frames `38–109` operating hold, and moving `110–114`
reset. Live rails never exceed their source stroke width or their scene ceiling.

| Style | Preset | Rail | Signature / auxiliary | Travel | Style / timing status |
|---:|---|---|---|---:|---|
| 5 | `agent-orchestration` | `glass-handoff-rail` | 14×9 `glass-task-capsule`; coordinator halo | 6px/frame | approved / `+2s` approved |
| 6 | `governed-runtime` | `governance-thread` | 12×12 `policy-seal` | 6px/frame | approved / `+2s` approved |
| 7 | `token-stream` | `api-token-rail` | three-cell `token-train` | 6px/frame | approved / `+2s` approved |
| 8 | `golden-circuit` | `luxury-circuit-rail` | `gem-tracer`; only its gem halo is filtered | 6px/frame | approved / `+2s` approved |
| 9 | `review-trace` | `review-trace-rail` | `review-cursor` | 5px/frame | approved / `+2s` approved |
| 10 | `cloud-flow` | `cloud-flow-rail` | region chevrons, replication capsule, availability pulses | 6px/frame | approved / `+2s` approved |
| 11 | `event-transit` | `event-transit-rail` | event train, exception/projection cars, dwell rings | 5px/frame | approved / `+2s` approved |
| 12 | `ops-pulse` | incident / telemetry rails | ECG/export heads, trace reveals, `waterfall-scanner` | 5px/frame | approved / `+2s` approved |

Styles 8, 11, and 12 use `(data-motion-role, data-motion-stage,
data-motion-order)` because repeated roles must stay independently addressable.
Styles 10–12 add only explicit motion metadata to their source fixtures; stripping
`data-motion-role`, `data-motion-stage`, and `data-motion-order` reproduces the
pre-animation static SVG geometry exactly.

## CLI

```bash
SKILL_ROOT="${CLAUDE_SKILL_DIR:-/absolute/path/from-codex-skill-metadata}"
python3 "$SKILL_ROOT/scripts/fireworks.py" animate diagram.svg diagram.gif
```

Optional controls remain deliberately small:

```bash
python3 "$SKILL_ROOT/scripts/fireworks.py" animate diagram.svg diagram.gif \
  --preset auto --duration 5.75 --fps 20 --width 960 \
  --report diagram.motion.json
```

Use `--dry-run` to validate identity, semantics, geometry, timeline, output path,
and render budget without launching Chrome or writing artifacts. `duration × fps`
must be a whole number of at least 55 frames. Width × height × frame count may
not exceed 600 million rendered pixels.

## Runtime

SVG-to-GIF export needs Node.js 18+, FFmpeg/FFprobe, Chrome/Chromium, and either
`puppeteer` or `puppeteer-core`:

```bash
brew install ffmpeg
for SKILL_ROOT in \
  "$HOME/.agents/skills/fireworks-tech-graph" \
  "$HOME/.claude/skills/fireworks-tech-graph"
do
  [ -d "$SKILL_ROOT" ] || continue
  npm install --prefix "$SKILL_ROOT" --ignore-scripts --no-save --package-lock=false puppeteer-core@25.3.0
  python3 "$SKILL_ROOT/scripts/fireworks.py" doctor
done
```

Set `FIREWORKS_CHROME_PATH` for a nonstandard Chrome install. Set
`FIREWORKS_PUPPETEER_PATH` to an explicit trusted module directory when keeping
the Node runtime elsewhere. Renderer resolution never searches the caller's
working directory, so installing `puppeteer-core` in the diagram project does
not make it available to a copied Skill. A `skills --copy` install creates two
independent roots, so each existing root needs its own runtime installation.
Chrome sandboxing is enabled by default; the no-sandbox override is reserved for
isolated CI runners.

## Quality contract

- Require a fireworks-tech-graph SVG with `viewBox`, valid semantic metadata, and
  explicit motion role, stage, and order on every reviewed edge.
- Sanitize SVG and block scripts, event handlers, external references, active CSS,
  and browser network requests.
- Pass XML, marker, geometry, and composition checks before rendering.
- Preserve the source SHA-256 and assert the static DOM before every frame capture.
- Hide source edges and route labels with a transient stylesheet; insert draw,
  settled-marker, per-style stream-body, packet-head, registration-bead, or
  memory-card decorations below nodes. Keep each Style 1/2 head immediately
  after its body, keep Style 3 beads and Style 4 cards below route labels and
  nodes, and keep the arrival-label layer above every motion path. Keep Style 2's
  opacity-only cursor above the terminal node without mutating its source underscore.
- Render uniform frame-center samples on a manual timeline.
- Compare Style 1 raw Chromium frames `0–35` from 55-frame and 75-frame timelines;
  their hashes must match before GIF palette quantization. Keep a separate 75-frame
  raw baseline for the approved Style 1 worker and require every post-change frame
  to remain byte-identical.
- Raster-test Style 2's body/head layering, 6px/frame full-scale travel,
  3px/frame half-scale travel, semantic route colors, all eight direction
  sentinels, cursor cadence, and five-frame reset before GIF encoding.
- Raster-test Style 3's connector-free opening, ten bodies and ten registration
  beads, 6px/frame full-scale and 3px/frame half-scale bead travel, stage-locked
  fan-out/data-write synchrony, all ten directions, and source-DOM integrity.
- Preserve separate 75-frame raw Chromium baselines for approved Styles 1–3 and
  require every post-change frame to remain byte-identical.
- Render every style at 75 and 115 frames and accept all 852 raw comparisons for
  frames `0–70` through a three-tier compatibility gate: binary SHA-256 exact,
  decoded RGBA exact, then guarded compositor-antialias equivalence. The guarded
  tier requires AE ≤ 128, normalized RMSE ≤ 0.001, every connected difference
  component to be at most 2px wide or high, and every component to remain on an
  edge or node border. Report each tier separately. DOM and per-style signature
  geometry remain strict-exact before GIF palette quantization.
- Raster-test Style 4's connector-free opening, sequential six-route arrivals,
  six semantic rails and six outlined cards, 8px endpoint clearance, 6px/frame
  full-scale and 3px/frame half-scale travel, exact progress/phase/rotation
  vectors, all six directions, and source-DOM integrity. After real Chromium
  capture and 50% point downsampling, horizontal, downward, and upward cards
  must each retain two non-touching unequal-length ink strokes with a complete
  background row or column between them.
- Require exact codec, dimensions, duration, and frame count from FFprobe.
- Require exact raw and encoded frame counts and zero adjacent duplicates. Keep
  every frame unique through 75 frames. For longer timelines, require at least 75
  unique rasters and allow repeated hashes inside full-opacity frames `38–109`.
  Frame `110` is the sole permitted boundary repeat because its unchanged reset
  opacity is exactly `1.00`; classify it as `intentional_reset_boundary_repeat`.
  Frames `111–114` remain globally distinct. Record all repeated frame indices
  and embed infinite GIF looping.
- Install the GIF and optional JSON report atomically.
- Treat 500KB as the focused artifact target and report deterministic size advice.
- Deliver one live GIF plus a phase contact sheet for each style review.
fixtures/quality-baseline/agent-runtime-style9.json
{
  "schema_version": 1,
  "mode": "architecture",
  "template_type": "architecture",
  "style": 9,
  "semantic_profile": "generic",
  "quality_profile": "showcase",
  "width": 960,
  "height": 600,
  "title": "Agent Runtime Architecture",
  "subtitle": "orchestration, memory, tools, and evaluation",
  "containers": [
    { "id": "control-plane", "x": 40, "y": 120, "width": 880, "height": 116, "label": "Control Plane" },
    { "id": "execution-state", "x": 40, "y": 304, "width": 880, "height": 136, "label": "Execution and State" }
  ],
  "nodes": [
    { "id": "client", "kind": "rect", "x": 60, "y": 156, "width": 160, "height": 60, "label": "Client", "sublabel": "request + policy", "fill": "#f8fafc", "stroke": "#94a3b8", "flat": true },
    { "id": "gateway", "kind": "double_rect", "x": 300, "y": 156, "width": 180, "height": 60, "label": "Gateway", "sublabel": "auth + routing", "fill": "#fffbeb", "stroke": "#f59e0b", "flat": true },
    { "id": "agent", "kind": "double_rect", "x": 560, "y": 156, "width": 180, "height": 60, "label": "Agent Runtime", "sublabel": "plan + inference", "fill": "#faf5ff", "stroke": "#8b5cf6", "flat": true },
    { "id": "memory", "kind": "rect", "x": 300, "y": 344, "width": 180, "height": 60, "label": "Vector Memory", "sublabel": "retrieve + persist", "fill": "#f0f9ff", "stroke": "#38bdf8", "flat": true },
    { "id": "tools", "kind": "rect", "x": 560, "y": 344, "width": 180, "height": 60, "label": "Tool Runtime", "sublabel": "MCP + functions", "fill": "#f0fdf4", "stroke": "#22c55e", "flat": true },
    { "id": "eval", "kind": "rect", "x": 790, "y": 344, "width": 110, "height": 60, "label": "Trace + Eval", "sublabel": "quality gate", "fill": "#fff1f2", "stroke": "#fb7185", "flat": true }
  ],
  "arrows": [
    { "id": "request", "source": "client", "target": "gateway", "source_port": "right", "target_port": "left", "flow": "control", "label": "request" },
    { "id": "dispatch", "source": "gateway", "target": "agent", "source_port": "right", "target_port": "left", "flow": "control", "label": "dispatch" },
    { "id": "context", "source": "agent", "target": "memory", "source_port": "bottom", "target_port": "top", "flow": "read", "label": "context", "route_points": [[632, 270], [390, 270]] },
    { "id": "tool-call", "source": "agent", "target": "tools", "source_port": "bottom", "target_port": "top", "flow": "async", "label": "tool call", "dashed": true },
    { "id": "augment", "source": "memory", "target": "tools", "source_port": "right", "target_port": "left", "flow": "write", "label": "augment" },
    { "id": "trace", "source": "tools", "target": "eval", "source_port": "right", "target_port": "left", "flow": "data", "label": "trace" },
    { "id": "z-feedback", "source": "eval", "target": "agent", "source_port": "top", "target_port": "bottom", "flow": "feedback", "label": "feedback", "dashed": true, "route_points": [[845, 270], [668, 270]] }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 510,
  "legend_locked": true,
  "legend": [
    { "flow": "control", "label": "primary" },
    { "flow": "write", "label": "data" },
    { "flow": "async", "label": "dependency" }
  ],
  "footer": "Style 9 · C4 Review Canvas · showcase composition contract",
  "footer_x": 48,
  "footer_y": 570
}
references/style-2-dark-terminal.md
# Style 2: Dark Terminal

Neon-on-dark hacker aesthetic. Matches CLAUDE.md standard tech diagram style.

## Colors

```
Background:     #0f0f1a  (near-black)
Panel fill:     #0f172a  (slate-950)
Panel stroke:   #334155  (slate-700)
Box radius:     6px

Text primary:   #e2e8f0  (slate-200)
Text secondary: #94a3b8  (slate-400)
Text muted:     #475569  (slate-600)

Accent palette (use per theme/layer):
  Purple:   #7c3aed / #a855f7
  Orange:   #ea580c / #f97316
  Blue:     #1d4ed8 / #3b82f6
  Green:    #059669 / #10b981
  Gold:     #eab308
  Red:      #dc2626 / #ef4444

Arrow colors: match accent of the source node's theme
```

## Background Gradient

```xml
<defs>
  <linearGradient id="bg-grad" x1="0%" y1="0%" x2="100%" y2="100%">
    <stop offset="0%" stop-color="#0f0f1a"/>
    <stop offset="100%" stop-color="#1a1a2e"/>
  </linearGradient>
</defs>
<rect width="960" height="600" fill="url(#bg-grad)"/>
```

## Typography

```
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', 'Courier New', 'Microsoft YaHei', 'SimHei', monospace
font-size:   13px labels, 11px sub-labels, 15px titles
font-weight: 400 normal, 700 bold for section headers
letter-spacing: 0.02em for labels
```

## Box Styles

```xml
<!-- Standard panel -->
<rect rx="6" ry="6" fill="#0f172a" stroke="#334155" stroke-width="1"/>

<!-- Colored accent box (themed by function) -->
<rect rx="6" ry="6" fill="#1e1b4b" stroke="#7c3aed" stroke-width="1.5"/>
<!-- Purple for AI/ML nodes -->
<!-- #1c1917 / #ea580c for compute/API nodes -->
<!-- #052e16 / #059669 for storage/DB nodes -->
<!-- #1e3a5f / #3b82f6 for network/gateway nodes -->
```

## Glow Effect (optional, for key nodes)

```xml
<defs>
  <filter id="glow-purple">
    <feGaussianBlur stdDeviation="3" result="blur"/>
    <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
  </filter>
</defs>
<rect filter="url(#glow-purple)" stroke="#a855f7" .../>
```

## Arrows

```xml
<defs>
  <marker id="arrow-purple" markerWidth="8" markerHeight="6"
          refX="7" refY="3" orient="auto">
    <polygon points="0 0, 8 3, 0 6" fill="#a855f7"/>
  </marker>
</defs>
<path stroke="#a855f7" stroke-width="1.5" stroke-dasharray="none"
      fill="none" marker-end="url(#arrow-purple)"/>
```

## SVG Template

```xml
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 600"
     width="960" height="600">
  <style>
    text { font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', 'Courier New', 'Microsoft YaHei', 'SimHei', monospace; fill: #e2e8f0; }
  </style>
  <defs>
    <linearGradient id="bg-grad" x1="0%" y1="0%" x2="100%" y2="100%">
      <stop offset="0%" stop-color="#0f0f1a"/>
      <stop offset="100%" stop-color="#1a1a2e"/>
    </linearGradient>
    <!-- arrow markers -->
    <!-- glow filters -->
  </defs>
  <rect width="960" height="600" fill="url(#bg-grad)"/>
  <!-- nodes, edges, legend -->
</svg>
```
references/png-export.md
# PNG Export

Load this reference only when the bundled `generate-diagram.sh` fallback is insufficient or a specific renderer must be selected.

## Renderer Choice

| Tool | Install | Use when |
| --- | --- | --- |
| CairoSVG | `python3 -m pip install cairosvg` | Default; good CSS support |
| `rsvg-convert` | `brew install librsvg` or `apt install librsvg2-bin` | Simple SVGs without complex CSS |
| Puppeteer | Node.js plus Chromium | Browser-generated SVG, CJK/emoji fallback, or pixel fidelity |

Prefer the bundled validation/export entry point:

```bash
SKILL_ROOT="${CLAUDE_SKILL_DIR:-/absolute/path/from-codex-skill-metadata}"
"$SKILL_ROOT/scripts/generate-diagram.sh" -t architecture -s 1 -o ./diagram.svg -w 1920
```

## Manual CairoSVG

```bash
python3 -c "import cairosvg; cairosvg.svg2png(url='input.svg', write_to='output.png', output_width=1920)"
```

## Manual librsvg

```bash
rsvg-convert -w 1920 input.svg -o output.png
```

## Puppeteer

Install Puppeteer outside the user's project and run the bundled converter:

```bash
SKILL_ROOT="${CLAUDE_SKILL_DIR:-/absolute/path/from-codex-skill-metadata}"
npm install --prefix /tmp/fireworks-tech-graph-puppeteer puppeteer
NODE_PATH=/tmp/fireworks-tech-graph-puppeteer/node_modules node "$SKILL_ROOT/scripts/svg2png.js" ./output
```

## Known Limits

- `rsvg-convert` can omit complex CSS, filters, and `<foreignObject>` content.
- CairoSVG can miss CJK or emoji glyphs when the selected system font lacks them.
- Use Puppeteer when browser rendering is required; its viewport path preserves the SVG dimensions more reliably than a raw Chrome `--window-size` screenshot.
fixtures/quality-baseline/agent-runtime-style6.json
{
  "schema_version": 1,
  "mode": "architecture",
  "template_type": "architecture",
  "style": 6,
  "quality_profile": "showcase",
  "width": 960,
  "height": 600,
  "title": "Agent Runtime Architecture",
  "subtitle": "orchestration, memory, tools, and evaluation",
  "containers": [
    { "id": "control-plane", "x": 40, "y": 120, "width": 880, "height": 116, "label": "Control Plane" },
    { "id": "execution-state", "x": 40, "y": 304, "width": 880, "height": 136, "label": "Execution and State" }
  ],
  "nodes": [
    { "id": "client", "kind": "rect", "x": 60, "y": 156, "width": 160, "height": 60, "label": "Client", "sublabel": "request + policy", "fill": "#efe8de", "stroke": "#a29a8f", "flat": true },
    { "id": "gateway", "kind": "double_rect", "x": 300, "y": 156, "width": 180, "height": 60, "label": "Gateway", "sublabel": "auth + routing", "fill": "#f7ecda", "stroke": "#d97757" },
    { "id": "agent", "kind": "double_rect", "x": 560, "y": 156, "width": 180, "height": 60, "label": "Agent Runtime", "sublabel": "plan + inference", "fill": "#c8e4db", "stroke": "#8c6f5a" },
    { "id": "memory", "kind": "rect", "x": 300, "y": 344, "width": 180, "height": 60, "label": "Vector Memory", "sublabel": "retrieve + persist", "fill": "#e5e8df", "stroke": "#7b8b5c", "flat": true },
    { "id": "tools", "kind": "rect", "x": 560, "y": 344, "width": 180, "height": 60, "label": "Tool Runtime", "sublabel": "MCP + functions", "fill": "#f7ecda", "stroke": "#d0b893", "flat": true },
    { "id": "eval", "kind": "rect", "x": 790, "y": 344, "width": 110, "height": 60, "label": "Trace + Eval", "sublabel": "quality gate", "fill": "#f3e3dc", "stroke": "#d97757", "flat": true }
  ],
  "arrows": [
    { "id": "request", "source": "client", "target": "gateway", "source_port": "right", "target_port": "left", "flow": "control", "label": "request" },
    { "id": "dispatch", "source": "gateway", "target": "agent", "source_port": "right", "target_port": "left", "flow": "control", "label": "dispatch" },
    { "id": "context", "source": "agent", "target": "memory", "source_port": "bottom", "target_port": "top", "flow": "read", "label": "context", "route_points": [[632, 270], [390, 270]] },
    { "id": "tool-call", "source": "agent", "target": "tools", "source_port": "bottom", "target_port": "top", "flow": "async", "label": "tool call", "dashed": true },
    { "id": "augment", "source": "memory", "target": "tools", "source_port": "right", "target_port": "left", "flow": "write", "label": "augment" },
    { "id": "trace", "source": "tools", "target": "eval", "source_port": "right", "target_port": "left", "flow": "data", "label": "trace" },
    { "id": "z-feedback", "source": "eval", "target": "agent", "source_port": "top", "target_port": "bottom", "flow": "feedback", "label": "feedback", "dashed": true, "route_points": [[845, 270], [668, 270]] }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 510,
  "legend_locked": true,
  "legend": [
    { "flow": "control", "label": "primary" },
    { "flow": "write", "label": "data" },
    { "flow": "async", "label": "dependency" }
  ],
  "footer": "Style 6 · Claude Official · showcase composition contract",
  "footer_x": 48,
  "footer_y": 570
}
references/style-3-blueprint.md
# Style 3: Blueprint

Engineering blueprint aesthetic with grid background and technical annotation style.

## Colors

```
Background:     #0a1628
Grid color:     #112240  (subtle grid lines)
Panel fill:     #0d1f3c
Panel stroke:   #00b4d8  (cyan/teal)
Box radius:     2px  (sharp corners for technical feel)

Text primary:   #caf0f8  (light cyan)
Text secondary: #90e0ef
Text label:     #00b4d8
Text muted:     #48cae4 at 60% opacity

Accent colors:
  Cyan:    #00b4d8 / #48cae4
  White:   #ffffff (key labels)
  Orange:  #f77f00 (warnings/alerts)
  Green:   #06d6a0 (success/active)
```

## Background with Grid

```xml
<defs>
  <pattern id="grid" width="30" height="30" patternUnits="userSpaceOnUse">
    <path d="M 30 0 L 0 0 0 30" fill="none"
          stroke="#112240" stroke-width="0.5"/>
  </pattern>
</defs>
<rect width="960" height="600" fill="#0a1628"/>
<rect width="960" height="600" fill="url(#grid)" opacity="0.6"/>
```

## Typography

```
font-family: 'Courier New', 'Lucida Console', 'Microsoft YaHei', 'SimHei', monospace
font-size:   13px labels, 10px annotations, 16px title
font-weight: 400; titles use 700
text-transform: uppercase for section headers
letter-spacing: 0.05em
```

## Box Styles

```xml
<!-- Technical node box -->
<rect rx="2" ry="2" fill="#0d1f3c" stroke="#00b4d8" stroke-width="1"/>

<!-- Corner brackets instead of full border (engineering style) -->
<!-- Draw 4 L-shapes at corners instead of full rect -->

<!-- Dashed box (external/optional component) -->
<rect rx="2" fill="none" stroke="#00b4d8" stroke-width="1"
      stroke-dasharray="6,3"/>
```

## Arrows & Annotations

```xml
<defs>
  <marker id="arrow-cyan" markerWidth="8" markerHeight="6"
          refX="7" refY="3" orient="auto">
    <polygon points="0 0, 8 3, 0 6" fill="#00b4d8"/>
  </marker>
</defs>
<!-- Lines are sharp, orthogonal routing preferred -->
<polyline points="x1,y1 x2,y1 x2,y2"
          stroke="#00b4d8" stroke-width="1" fill="none"
          marker-end="url(#arrow-cyan)"/>

<!-- Annotation label on line -->
<text fill="#48cae4" font-size="10" text-anchor="middle">HTTP/REST</text>
```

## Title Block (bottom-right)

```xml
<!-- Blueprint title block -->
<rect x="700" y="530" width="240" height="60"
      fill="#0d1f3c" stroke="#00b4d8" stroke-width="1"/>
<line x1="700" y1="545" x2="940" y2="545"
      stroke="#00b4d8" stroke-width="0.5"/>
<text x="820" y="542" text-anchor="middle"
      fill="#caf0f8" font-size="10">SYSTEM ARCHITECTURE</text>
<text x="820" y="578" text-anchor="middle"
      fill="#00b4d8" font-size="13" font-weight="700">DIAGRAM TITLE</text>
```

## SVG Template

```xml
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 600"
     width="960" height="600">
  <style>
    text { font-family: 'Courier New', 'Lucida Console', 'Microsoft YaHei', 'SimHei', monospace; fill: #caf0f8; }
  </style>
  <defs>
    <pattern id="grid" width="30" height="30" patternUnits="userSpaceOnUse">
      <path d="M 30 0 L 0 0 0 30" fill="none" stroke="#112240" stroke-width="0.5"/>
    </pattern>
    <!-- arrow markers -->
  </defs>
  <rect width="960" height="600" fill="#0a1628"/>
  <rect width="960" height="600" fill="url(#grid)" opacity="0.6"/>
  <!-- nodes, edges, title block -->
</svg>
```
references/style-10-cloud-fabric.md
# Style 10: Cloud Fabric

A deployment-topology view for regions, VPCs/networks, compute, data stores,
and cross-boundary traffic. It answers “where does this run?” without turning
the diagram into a vendor icon poster.

## Best fit

- Multi-region and disaster-recovery reviews
- Cloud deployment and network ownership maps
- Kubernetes cluster/namespace placement
- Migration plans and platform boundary discussions

## Visual tokens

| Token | Value |
|---|---|
| Canvas | `#edf5fb` with a restrained blue grid |
| Card | `#ffffff` |
| Primary text | `#102a43` |
| Boundary | `#7fa3c2` |
| Traffic | `#2563eb` |
| Write | `#ea580c` |
| Data | `#059669` |
| Cross-region | `#7c3aed`, dashed |

Nested boundaries carry a small `GLOBAL`, `REGION`, or `NETWORK` badge. Nodes
use a manifest-backed neutral glyph plus provider metadata. A colored ownership
spine makes each nested deployment boundary readable even in grayscale.

## Required semantic contract

Select `semantic_profile: "cloud-fabric"` and provide:

- `diagram_type: "deployment"`
- `platform_profile`: `provider-neutral`, `aws`, `azure`, `gcp`, or `kubernetes`
- `icon_manifest_version: "2026.07-neutral.1"`
- at least one `deployment_kind: "region"` boundary
- every node: `deployment_id` and a manifest-backed `icon_id`
- every cross-deployment edge: a non-empty `via`

Boundary parents must form an acyclic tree, nesting depth is capped at four,
child boundaries stay at least `16px` inside their parent, and nodes stay at
least `20px` inside their assigned deployment.

## Icon policy

The bundled manifest contains provider-neutral geometric glyphs. It records
official AWS, Azure, and Google Cloud asset URLs as adapter metadata; it does
not redistribute vendor trademarks. If a user supplies an official icon pack,
preserve its license and version outside this repository and map it through the
manifest boundary.

Never invent a vendor service logo. Unknown `icon_id` values fail validation.

## Composition rules

- Show deployment ownership through nesting, not background color alone
- Use a shallow `global → region → network` hierarchy where possible
- Keep ingress paths vertical and regional data paths aligned
- Name cross-boundary mechanisms (`peering`, `transit gateway`, `service mesh`)
- Zero crossings in official samples; use no more than two bends per edge
- Do not encode both logical service architecture and every subnet in one view
- Reserve the icon/provider area before fitting text; no title or subtitle may cross a card edge
- Place ingress mechanism labels in inter-boundary whitespace, clear of Region and Network headers

## Signature checklist

- Pale cloud grid and explicit `global → region → network` nesting
- Ownership spines plus `GLOBAL`, `REGION`, and `NETWORK` boundary badges
- Manifest-backed globe, compute, database, gateway, stream, or observe glyphs
- Top-right platform/region/deployment-mode stamp and named cross-boundary mechanisms

## Prompt cues

- English: `multi-region deployment map`, `cloud landing zone map`, `region/VPC ownership map`
- 中文:`多区域部署图`、`云部署拓扑`、`Region/VPC 归属图`
- Copyable cue: `Use Style 10 Cloud Fabric; show global ingress, Region and VPC ownership, neutral cloud glyphs, deployment mode, and every cross-boundary mechanism.`

## Do not blend with

Do not use C4 abstraction labels, transit-map station numbering, or live SRE
metric cards. When deployment evidence is absent, fall back to a generic Style
1–7 architecture view instead of inventing cloud ownership.

## Fixture

`fixtures/cloud-fabric-style10.json` demonstrates active-active checkout
deployment with two regions, explicit VPC ownership, and neutral glyphs.
references/style-1-flat-icon.md
# Style 1: Flat Icon (Default)

Inspired by draw.io defaults and Apple documentation style.

## Colors

```
Background:     #ffffff
Box fill:       #ffffff
Box stroke:     #d1d5db  (gray-300)
Box radius:     8px
Text primary:   #111827  (gray-900)
Text secondary: #6b7280  (gray-500)

Semantic arrow colors (pick by flow type):
  Flow A (main):   #2563eb  (blue-600)
  Flow B (alt):    #dc2626  (red-600)
  Flow C (data):   #16a34a  (green-600)
  Flow D (async):  #9333ea  (purple-600)

Icon accent backgrounds:
  Blue tint:   #eff6ff / #dbeafe
  Red tint:    #fef2f2 / #fee2e2
  Green tint:  #f0fdf4 / #dcfce7
  Purple tint: #faf5ff / #ede9fe
  Orange tint: #fff7ed / #fed7aa
  Teal tint:   #f0fdfa / #ccfbf1
```

## Typography

```
font-family: 'Helvetica Neue', Helvetica, Arial, 'PingFang SC',
             'Microsoft YaHei', 'Microsoft JhengHei', 'SimHei', sans-serif
font-size:   14px labels, 12px sub-labels, 16px titles
font-weight: 400 normal, 600 semi-bold for titles
```

## Box Shapes

```xml
<!-- Standard node box -->
<rect rx="8" ry="8" fill="#ffffff" stroke="#d1d5db" stroke-width="1.5"/>

<!-- Icon accent box (colored background) -->
<rect rx="8" ry="8" fill="#eff6ff" stroke="#bfdbfe" stroke-width="1.5"/>

<!-- Database cylinder (use SVG path) -->
<!-- Terminal box: rx=4, fill=#111827, stroke=#374151 -->
<!-- User/actor: circle or rounded rect with icon -->
```

## Arrows

```xml
<defs>
  <marker id="arrow-blue" markerWidth="10" markerHeight="7"
          refX="9" refY="3.5" orient="auto">
    <polygon points="0 0, 10 3.5, 0 7" fill="#2563eb"/>
  </marker>
  <marker id="arrow-red" markerWidth="10" markerHeight="7"
          refX="9" refY="3.5" orient="auto">
    <polygon points="0 0, 10 3.5, 0 7" fill="#dc2626"/>
  </marker>
</defs>

<!-- Line -->
<line stroke="#2563eb" stroke-width="1.5" marker-end="url(#arrow-blue)"/>
<!-- Or path for curved/orthogonal routing -->
<path stroke="#2563eb" stroke-width="1.5" fill="none" marker-end="url(#arrow-blue)"/>
```

## Legend

Always include a legend in the bottom-left if multiple arrow colors are used:

```xml
<g transform="translate(20, 560)">
  <line x1="0" y1="8" x2="30" y2="8" stroke="#2563eb" stroke-width="1.5"
        marker-end="url(#arrow-blue)"/>
  <text x="36" y="12" fill="#6b7280" font-size="12">Agent flow</text>
  <line x1="0" y1="24" x2="30" y2="24" stroke="#dc2626" stroke-width="1.5"
        marker-end="url(#arrow-red)"/>
  <text x="36" y="28" fill="#6b7280" font-size="12">RAG flow</text>
</g>
```

## SVG Template

```xml
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 600"
     width="960" height="600">
  <style>
    /* NO @import — cairosvg / rsvg-convert cannot fetch external URLs */
    text { font-family: 'Helvetica Neue', Helvetica, Arial, 'PingFang SC', 'Microsoft YaHei', 'Microsoft JhengHei', 'SimHei', sans-serif; }
  </style>
  <defs>
    <!-- arrow markers here -->
    <!-- gradients/filters if needed -->
  </defs>
  <!-- white background -->
  <rect width="960" height="600" fill="#ffffff"/>
  <!-- diagram title (optional) -->
  <!-- nodes -->
  <!-- edges -->
  <!-- legend -->
</svg>
```
references/style-5-glassmorphism.md
# Style 5: Glassmorphism

Frosted glass cards on dark gradient. Designed for product sites, keynotes, and hero sections.

## Colors

```
Background gradient: #0d1117 → #161b22 → #0d1117 (diagonal)

Glass card:
  fill:           rgba(255,255,255,0.05)
  stroke:         rgba(255,255,255,0.15)
  backdrop-filter: blur(12px)  [SVG: use feGaussianBlur]
  box-radius:     12px

Text primary:   #f0f6fc  (near-white)
Text secondary: #8b949e  (muted)
Text gradient:  use linearGradient on text fill for hero labels

Accent glows (one per layer):
  Blue glow:    #58a6ff  / rgba(88,166,255,0.3)
  Purple glow:  #bc8cff  / rgba(188,140,255,0.3)
  Green glow:   #3fb950  / rgba(63,185,80,0.3)
  Orange glow:  #f78166  / rgba(247,129,102,0.3)
```

## Background

```xml
<defs>
  <linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
    <stop offset="0%"   stop-color="#0d1117"/>
    <stop offset="50%"  stop-color="#161b22"/>
    <stop offset="100%" stop-color="#0d1117"/>
  </linearGradient>
  <!-- Ambient glow spots -->
  <radialGradient id="glow-blue" cx="30%" cy="40%" r="40%">
    <stop offset="0%" stop-color="rgba(88,166,255,0.15)"/>
    <stop offset="100%" stop-color="rgba(88,166,255,0)"/>
  </radialGradient>
  <radialGradient id="glow-purple" cx="70%" cy="60%" r="35%">
    <stop offset="0%" stop-color="rgba(188,140,255,0.12)"/>
    <stop offset="100%" stop-color="rgba(188,140,255,0)"/>
  </radialGradient>
</defs>
<rect width="960" height="600" fill="url(#bg)"/>
<rect width="960" height="600" fill="url(#glow-blue)"/>
<rect width="960" height="600" fill="url(#glow-purple)"/>
```

## Glass Card Effect

SVG cannot do real backdrop-filter, so simulate with:

```xml
<defs>
  <filter id="glass-blur">
    <feGaussianBlur in="SourceGraphic" stdDeviation="0.5"/>
  </filter>
</defs>

<!-- Glass card: layered rects -->
<!-- 1. Subtle inner shadow -->
<rect rx="12" fill="rgba(255,255,255,0.03)" stroke="none"/>
<!-- 2. Glass body -->
<rect rx="12" fill="rgba(255,255,255,0.06)"
      stroke="rgba(255,255,255,0.15)" stroke-width="1"/>
<!-- 3. Top highlight line -->
<line stroke="rgba(255,255,255,0.25)" stroke-width="1"/>
```

## Typography

```
font-family: 'Inter', -apple-system, 'SF Pro Display', 'PingFang SC', 'Microsoft YaHei', 'Microsoft JhengHei', 'SimHei', sans-serif
font-size:   14px labels, 12px sublabels, 20px hero title
font-weight: 400 normal, 600 semi-bold, 700 bold titles
```

## Gradient Text (for hero labels)

```xml
<defs>
  <linearGradient id="text-grad-blue" x1="0%" y1="0%" x2="100%" y2="0%">
    <stop offset="0%"   stop-color="#58a6ff"/>
    <stop offset="100%" stop-color="#bc8cff"/>
  </linearGradient>
</defs>
<text fill="url(#text-grad-blue)" font-weight="700" font-size="20">
  AI Pipeline
</text>
```

## Arrows

```xml
<defs>
  <marker id="arrow-blue" markerWidth="8" markerHeight="6"
          refX="7" refY="3" orient="auto">
    <polygon points="0 0, 8 3, 0 6" fill="#58a6ff"/>
  </marker>
</defs>
<!-- Slightly glowing line -->
<path stroke="#58a6ff" stroke-width="1.5" fill="none"
      opacity="0.8" marker-end="url(#arrow-blue)"/>
```

## SVG Template

```xml
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 600"
     width="960" height="600">
  <style>
    @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
    text { font-family: 'Inter', -apple-system, 'SF Pro Display', 'PingFang SC', 'Microsoft YaHei', 'Microsoft JhengHei', 'SimHei', sans-serif; fill: #f0f6fc; }
  </style>
  <defs>
    <!-- bg gradients, glow gradients, glass filter, arrow markers -->
  </defs>
  <!-- background layers -->
  <!-- glass cards (nodes) -->
  <!-- glowing edges -->
  <!-- labels with gradient text for heroes -->
</svg>
```
references/style-4-notion-clean.md
# Style 4: Notion Clean

Minimal, documentation-friendly. Designed to embed in Notion, Confluence, or GitHub wikis.

## Colors

```
Background:     #ffffff
Box fill:       #f9fafb  (gray-50) or #ffffff
Box stroke:     #e5e7eb  (gray-200)
Box radius:     4px

Text primary:   #111827  (gray-900)
Text secondary: #374151  (gray-700)
Text muted:     #9ca3af  (gray-400)
Text label:     #6b7280  (gray-500), uppercase, 11px

Accent (subtle, used sparingly):
  Blue:   #3b82f6 (arrows only)
  Gray:   #d1d5db (dividers)
```

## Design Principles

- **No decorative icons** — use geometric shapes only (rect, circle, diamond)
- **Generous whitespace** — 24px+ padding between elements
- **Single arrow color** — blue (#3b82f6) for all connections
- **Labels in ALL CAPS** — section headers and node type labels
- **No drop shadows** — flat only

## Typography

```
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI',
             'Helvetica Neue', Arial, 'PingFang SC', 'Microsoft YaHei',
             'Microsoft JhengHei', 'SimHei', sans-serif
font-size:   14px labels, 11px uppercase type labels, 18px title
font-weight: 400 normal, 500 medium for node labels
```

## Box Styles

```xml
<!-- Standard node -->
<rect rx="4" fill="#f9fafb" stroke="#e5e7eb" stroke-width="1"/>
<text fill="#111827" font-size="14" font-weight="500"/>

<!-- Type label (inside or above box) -->
<text fill="#9ca3af" font-size="11"
      font-weight="500" letter-spacing="0.08em">DATABASE</text>

<!-- Section grouping (dashed container) -->
<rect rx="4" fill="none" stroke="#e5e7eb" stroke-width="1"
      stroke-dasharray="4,3"/>
```

## Arrows

```xml
<defs>
  <marker id="arrow-blue" markerWidth="8" markerHeight="6"
          refX="7" refY="3" orient="auto">
    <polygon points="0 0, 8 3, 0 6" fill="#3b82f6"/>
  </marker>
</defs>
<line stroke="#3b82f6" stroke-width="1.5"
      marker-end="url(#arrow-blue)"/>
<!-- Optional: gray arrow for secondary flows -->
<line stroke="#d1d5db" stroke-width="1"
      stroke-dasharray="4,3" marker-end="url(#arrow-gray)"/>
```

## SVG Template

```xml
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 560"
     width="960" height="560">
  <style>
    text { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Arial, 'PingFang SC', 'Microsoft YaHei', 'Microsoft JhengHei', 'SimHei', sans-serif; }
  </style>
  <defs>
    <!-- arrow markers (blue only) -->
  </defs>
  <rect width="960" height="560" fill="#ffffff"/>
  <!-- nodes (no icons, geometry only) -->
  <!-- edges (single color) -->
  <!-- legend (minimal, only if 2+ flows) -->
</svg>
```

## Sizing Guide

- Node box: min 120×40px, prefer 160×48px for readability
- Title: top-left, 18px, gray-900, margin 32px from edges
- Spacing: 80px minimum between nodes horizontally, 60px vertically
references/style-6-claude-official.md
# Style 6: Claude Official

Inspired by Anthropic's Claude blog technical diagrams — warm, approachable, professional.

```
Background:     #f8f6f3  (warm cream)
Box fill:
  - Blue tint:   #a8c5e6  (alert/input nodes)
  - Green tint:  #9dd4c7  (agent nodes)
  - Beige:       #f4e4c1  (infrastructure/bus)
  - Gray tint:   #e8e6e3  (storage/state)
Box stroke:     #4a4a4a  (dark gray)
Box radius:     12px
Text primary:   #1a1a1a  (near black)
Text secondary: #6a6a6a  (medium gray)
Text labels:    #5a5a5a  (arrow labels)

Semantic colors:
  Input/Source:    #a8c5e6  (soft blue)
  Agent/Process:   #9dd4c7  (soft teal-green)
  Infrastructure:  #f4e4c1  (warm beige)
  Storage/State:   #e8e6e3  (light gray)

Arrow color:     #5a5a5a  (consistent dark gray)
```

## Typography

```
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue',
             Arial, 'PingFang SC', 'Microsoft YaHei', 'Microsoft JhengHei', 'SimHei', sans-serif
font-size:   16px node labels, 14px descriptions, 13px arrow labels
font-weight: 600 for node labels, 400 for descriptions, 700 for titles
```

## Box Shapes

```xml
<!-- Agent node (teal-green) -->
<rect rx="12" ry="12" fill="#9dd4c7" stroke="#4a4a4a" stroke-width="2.5"/>

<!-- Input/Source node (soft blue) -->
<rect rx="12" ry="12" fill="#a8c5e6" stroke="#4a4a4a" stroke-width="2.5"/>

<!-- Infrastructure node (warm beige) -->
<rect rx="12" ry="12" fill="#f4e4c1" stroke="#4a4a4a" stroke-width="2.5"/>

<!-- Storage/State node (light gray) -->
<rect rx="12" ry="12" fill="#e8e6e3" stroke="#4a4a4a" stroke-width="2.5"/>
```

## Arrows

```xml
<defs>
  <marker id="arrow-claude" markerWidth="8" markerHeight="8"
          refX="7" refY="4" orient="auto">
    <polygon points="0 0, 8 4, 0 8" fill="#5a5a5a"/>
  </marker>
</defs>

<!-- Arrow line -->
<line stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>

<!-- Or use simple line without arrowhead for cleaner look -->
<line stroke="#5a5a5a" stroke-width="2"/>
```

## Arrow Semantics

Use different arrow styles to convey meaning:

| Flow Type | Color | Stroke | Dash | Usage |
|-----------|-------|--------|------|-------|
| Primary data flow | #5a5a5a | 2px solid | none | Main request/response path |
| Memory write | #5a5a5a | 2px | `5,3` | Write/store operations |
| Memory read | #5a5a5a | 2px solid | none | Retrieval from store |
| Control/trigger | #5a5a5a | 1.5px | `3,2` | Event triggers |

```xml
<!-- Solid arrow for reads -->
<line stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>

<!-- Dashed arrow for writes -->
<line stroke="#5a5a5a" stroke-width="2" stroke-dasharray="5,3" marker-end="url(#arrow-claude)"/>
```

## Arrow Labels

Arrow labels should be **technical and specific**, positioned mid-arrow:

```xml
<text x="..." y="..." fill="#5a5a5a" font-size="13" text-anchor="middle">
  store(embedding)
</text>
```

Good labels: `query(text)`, `retrieve(top_k=5)`, `embed(768d)`, `POST /api/search`
Avoid vague labels: "Process", "Send", "Get"

## Node Content Guidelines

Node content should include **technical details**, not just concepts:

**Good examples:**
- "Vector Store" → "Vector Store" + "• 768-dim embeddings" + "• Cosine similarity"
- "LLM" → "GPT-4" + "• 8K context" + "• Temperature: 0.7"
- "Memory" → "Redis Cache" + "• TTL: 5min" + "• Max: 4K tokens"

**Avoid vague descriptions:**
- "Process data" → specify what processing
- "Store information" → specify storage type and format
- "Handle requests" → specify request type and method

Use 2-3 lines per node:
1. Component name (bold, 16px)
2. Technical detail or implementation (14px)
3. Key parameter or constraint (14px, optional)

## Layer Labels

For multi-layer architectures, add layer labels on the left side:

```xml
<text x="30" y="130" fill="#6a6a6a" font-size="14" font-weight="600">Input</text>
<text x="30" y="290" fill="#6a6a6a" font-size="14" font-weight="600">Processing</text>
<text x="30" y="490" fill="#6a6a6a" font-size="14" font-weight="600">Storage</text>
```

Position at the vertical center of each layer.

## Legend Requirement

When using 2+ arrow types or colors, include a legend in the bottom-right corner:

```xml
<rect x="720" y="500" width="220" height="80" rx="8" ry="8"
      fill="#ffffff" stroke="#4a4a4a" stroke-width="1.5"/>
<text x="735" y="522" fill="#1a1a1a" font-size="13" font-weight="600">Legend</text>

<!-- Legend items -->
<line x1="735" y1="540" x2="765" y2="540" stroke="#5a5a5a" stroke-width="2"/>
<text x="775" y="545" fill="#6a6a6a" font-size="12">Read operation</text>

<line x1="735" y1="560" x2="765" y2="560" stroke="#5a5a5a" stroke-width="2" stroke-dasharray="5,3"/>
<text x="775" y="565" fill="#6a6a6a" font-size="12">Write operation</text>
```

Position: bottom-right, 20px margin from edges.

## Layout Principles

- **Generous spacing**: Minimum 80px between node edges
- **Horizontal alignment**: Same-layer nodes align perfectly
- **Vertical flow**: Top-to-bottom preferred
- **Symmetry**: Balanced composition
- **Clean lines**: Orthogonal routing — vertical then horizontal, or vice versa

## SVG Template

```xml
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 600"
     width="960" height="600">
  <style>
    text { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI',
                   'Helvetica Neue', Arial, 'PingFang SC', 'Microsoft YaHei', 'Microsoft JhengHei', 'SimHei', sans-serif;
    }
  </style>
  <defs>
    <marker id="arrow-claude" markerWidth="8" markerHeight="8"
            refX="7" refY="4" orient="auto">
      <polygon points="0 0, 8 4, 0 8" fill="#5a5a5a"/>
    </marker>
    <filter id="shadow-soft">
      <feDropShadow dx="0" dy="2" stdDeviation="6" flood-color="#00000008"/>
    </filter>
  </defs>

  <!-- Warm cream background -->
  <rect width="960" height="600" fill="#f8f6f3"/>

  <!-- Title (optional) -->
  <text x="480" y="40" text-anchor="middle" fill="#1a1a1a"
        font-size="20" font-weight="700">Diagram Title</text>

  <!-- Nodes -->
  <!-- Agent node example -->
  <rect x="100" y="100" width="180" height="80" rx="12" ry="12"
        fill="#9dd4c7" stroke="#4a4a4a" stroke-width="2.5"
        filter="url(#shadow-soft)"/>
  <text x="190" y="145" text-anchor="middle" fill="#1a1a1a"
        font-size="16" font-weight="600">Agent name</text>

  <!-- Edges -->
  <line x1="190" y1="180" x2="190" y2="240"
        stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>
  <text x="210" y="215" fill="#5a5a5a" font-size="13">Publish</text>
</svg>
```

## Design Philosophy

Claude's official style emphasizes:
- **Warmth**: Cream background, soft fills
- **Clarity**: High contrast text, generous spacing
- **Professionalism**: Consistent stroke weights, aligned elements
- **Approachability**: Rounded corners, friendly colors

Avoid:
- Harsh shadows or gradients
- Overly saturated colors
- Thin stroke weights (< 2px)
- Cluttered layouts
references/style-12-ops-pulse.md
# Style 12: Ops Pulse

An operational review surface that combines service topology, the four golden
signals, one critical request path, and a correlated trace waterfall. It is
for incident and reliability work, not a screenshot of a live dashboard.

## Best fit

- Incident reviews and production-readiness reviews
- SLO/error-budget discussions
- Latency and dependency investigations
- Runbooks that connect topology to one representative trace

## Visual tokens

| Token | Value |
|---|---|
| Canvas | `#07111f` with a restrained ops grid |
| Service card | `#0d1b2a` |
| Border | `#29435d` |
| Healthy | `#22c55e` |
| Warning | `#f59e0b` |
| Critical | `#f43f5e` |
| Telemetry | `#22d3ee`, dashed |
| Trace | `#38bdf8` |

## Required semantic contract

Select `semantic_profile: "ops-pulse"` and provide:

- `diagram_type: "observability"`
- one to twelve nodes with `ops_role: "service"`
- each service: exactly `latency`, `traffic`, `errors`, and `saturation`
- every signal: `value`, `unit`, `window`, and `status`
- service cards at least `180×108` and a visible `status_label`
- a non-empty ordered `critical_path` of business edge ids
- business and telemetry edges using different `flow` tokens
- trace spans with positive duration and valid parent coverage

The critical path must be contiguous and may not include telemetry edges. A
trace waterfall has exactly one root when spans are present.

## Composition rules

- Keep service health cards in one aligned band
- Show only one highlighted critical path per view
- Telemetry export is dashed and visually subordinate
- Put the trace waterfall in its own boundary below topology
- Do not duplicate a critical edge to create glow; glow is decoration owned by
  the single semantic path
- Use a fixed observation window and display units on every signal
- Zero bridge crossings and no more than two bends per edge

## Signature checklist

- Top-right `LIVE` investigation stamp with observation window and worst status
- Status rail on every service card and four metric chips carrying explicit windows
- Numbered critical-hop markers on the single highlighted business path
- Dedicated trace boundary with a 0–100% ruler and one correlated waterfall

## Prompt cues

- English: `reliability pulse`, `incident investigation view`, `golden signals trace`, `SRE trace review`
- 中文:`可靠性脉冲`、`事故排查视图`、`黄金信号追踪图`、`SRE Trace 评审`
- Copyable cue: `Use Style 12 Ops Pulse; show a fixed observation window, four golden signals per service, one numbered critical path, telemetry export, and one correlated trace waterfall.`

## Do not blend with

Do not turn the service band into a Region/VPC deployment map, an event-metro
rail, or a C4 responsibility review. If the prompt lacks measured signals,
status, time window, and trace evidence, use a generic architecture style.

## Fixture

`fixtures/ops-pulse-style12.json` demonstrates a degraded checkout path with
four service cards, an OTel collector, and a correlated four-span trace.
references/style-11-event-transit.md
# Style 11: Event Transit

A transit-map metaphor for event-driven systems. Topics are rails, processing
steps are stations, declared junctions are branch points, and dead letters get
a visibly separate route.

## Best fit

- Kafka/Pulsar/NATS topology reviews
- Event choreography and consumer-group discussions
- Stream processing, retry, and DLQ runbooks
- Schema evolution and materialized-view pipelines

## Visual tokens

| Token | Value |
|---|---|
| Canvas | `#fbf7ee` with transit dots |
| Card | `#fffdf8` |
| Primary rail | `#e4475b` |
| State projection | `#00897b` |
| Read/consumer | `#2563eb` |
| DLQ | `#c62828`, dashed |
| Text | `#17213c` |

The dark casing beneath a rail is decoration. The colored path with
`data-graph-role="edge"` remains the ordinary semantic connector. Semantic
rails use a maximum `2.8px` stroke and fixed user-space arrowheads so markers do
not grow with line width.

## Required semantic contract

Select `semantic_profile: "event-transit"` and provide:

- `diagram_type: "event_stream"`
- one to four topics, each with unique `id` and `color`
- every node: `transit_role` and optional/required topic metadata
- stations/junctions: `operation`
- consumers: `consumer_group`
- rail stations: unique integer `station_order`
- rail edges: `transit_type: "rail"`, topic id, `right → left` ports

A rail edge must connect consecutive station orders on one horizontal
centerline with at least `64px` clearance. A dead-letter edge must target a
real `dlq` node. Multiple rail departures are only valid from a `junction`.

## Composition rules

- One horizontal centerline per topic rail
- Use branches only where the node is declared as a junction
- Put DLQ and state-store terminals below their owning station
- Reserve dashed red for dead-letter/retry semantics
- Show partition, lag, or schema facts as short badges
- Zero bridge crossings and no duplicated rail overlay in official samples
- Keep at least `64px` clear rail between adjacent station cards
- Use compact user-space arrowheads and a subtle casing; the semantic rail stays at or below `2.8px`

## Signature checklist

- Dark top-right `EVENT METRO` line stamp and a signed topic-line count
- Numbered station medallions, compact midpoint chevrons, and one rail centerline per topic
- Distinct junction dot, dashed DLQ terminal with `×`, and state-store terminal with a square glyph
- Partition, schema, latency, fan-out, lag, replay, and state badges attached to stations

## Prompt cues

- English: `event metro map`, `topic rail map`, `Kafka topology`, `stream choreography map`
- 中文:`事件地铁图`、`事件轨道图`、`Topic 线路图`、`Kafka 拓扑图`
- Copyable cue: `Use Style 11 Event Transit; render topics as thin metro rails, processors as numbered stations, declared junctions, consumer groups, DLQ, and state projections.`

## Do not blend with

Do not nest Region/VPC deployment boundaries, label cards as C4 containers, or
attach golden-signal dashboards to every station. A request/response flow with
no topic, consumer-group, or stream evidence should use a generic flow style.

## Fixture

`fixtures/event-transit-style11.json` shows a checkout event line with schema
validation, fraud enrichment, a declared junction, DLQ, and materialized state.
references/style-7-openai.md
# Style 7: OpenAI Official

Clean, modern aesthetic matching OpenAI's documentation and research diagrams — minimal but precise.

## Color Palette

```
Background:     #ffffff  (pure white)
Primary text:   #0d0d0d  (near black)
Secondary text: #6e6e80  (muted gray)
Border:         #e5e5e5  (light gray)

Accent colors (reserved):
Green accent:   #10a37f  (OpenAI brand green)
Blue accent:    #1d4ed8  (links, actions)
Orange accent:  #f97316  (highlights, warnings)
Gray accent:    #71717a  (secondary elements)
```

## Typography

```
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica Neue, 'PingFang SC', 'Microsoft YaHei', 'Microsoft JhengHei', 'SimHei', sans-serif
font-size:   16px node labels, 13px descriptions, 12px arrow labels
font-weight: 600 for titles, 500 for labels, 400 for descriptions
letter-spacing: 0
```

No custom fonts. System font stack only for maximum compatibility.

## Node Boxes

Clean, minimal boxes with subtle borders:

```xml
<!-- Standard node -->
<rect x="100" y="100" width="180" height="80" rx="8" ry="8"
      fill="#ffffff" stroke="#e5e5e5" stroke-width="1.5"/>

<!-- Accent node (with green left border) -->
<rect x="100" y="100" width="180" height="80" rx="8" ry="8"
      fill="#ffffff" stroke="#e5e5e5" stroke-width="1.5"/>
<rect x="100" y="100" width="4" height="80" rx="2" ry="2"
      fill="#10a37f"/>
```

**Key techniques:**
1. White fill with light gray border (no shadows)
2. Optional colored left-border accent strip (4px wide)
3. `rx="8"` for subtle rounding
4. `stroke-width: 1.5` — thin, precise borders
5. No gradients, no shadows, no decorative elements

## Arrows

Thin, precise arrows with subtle colors:

```xml
<defs>
  <!-- Default arrow (gray) -->
  <marker id="arrow-oai" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
    <polygon points="0 0, 10 3.5, 0 7" fill="#71717a"/>
  </marker>

  <!-- Accent arrow (green) -->
  <marker id="arrow-oai-green" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
    <polygon points="0 0, 10 3.5, 0 7" fill="#10a37f"/>
  </marker>
</defs>

<!-- Default connection -->
<line x1="280" y1="140" x2="400" y2="140"
      stroke="#71717a" stroke-width="1.5" marker-end="url(#arrow-oai)"/>

<!-- Accent connection -->
<line x1="280" y1="140" x2="400" y2="140"
      stroke="#10a37f" stroke-width="1.5" marker-end="url(#arrow-oai-green)"/>
```

**Arrow guidelines:**
- Prefer straight lines (orthogonal routing with right angles)
- `stroke-width: 1.5` — thin and precise
- Filled polygon arrowheads (not stroke-based)
- Gray for default, green for primary/accent flows
- Dashed (`stroke-dasharray="4,3"`) for optional/async flows

## Arrow Labels

Minimal, small labels:

```xml
<text x="340" y="133" text-anchor="middle" fill="#6e6e80" font-size="12">
  label
</text>
```

Labels should be:
- 12px, gray color (`#6e6e80`)
- No background rect by default (white background is usually sufficient); add a white fallback rect only when offsetting cannot prevent a collision
- Short, technical language
- Midpoint of arrow

## Database Shapes

Clean cylinders with thin borders:

```xml
<ellipse cx="200" cy="100" rx="50" ry="12" fill="#ffffff" stroke="#e5e5e5" stroke-width="1.5"/>
<path d="M 150,100 L 150,140 Q 200,155 250,140 L 250,100"
      fill="#ffffff" stroke="#e5e5e5" stroke-width="1.5"/>
<ellipse cx="200" cy="140" rx="50" ry="12" fill="none" stroke="#e5e5e5" stroke-width="1.5"/>
```

## Grouping Containers

Dashed rect containers for logical grouping:

```xml
<rect x="80" y="80" width="400" height="200" rx="8" ry="8"
      fill="none" stroke="#e5e5e5" stroke-width="1" stroke-dasharray="4,3"/>
<text x="90" y="97" fill="#6e6e80" font-size="12" font-weight="500">
  Group Label
</text>
```

## Node Content

Clean, minimal text layout:

```xml
<rect x="100" y="100" width="180" height="80" rx="8" ry="8"
      fill="#ffffff" stroke="#e5e5e5" stroke-width="1.5"/>
<text x="190" y="130" text-anchor="middle" fill="#0d0d0d"
      font-size="16" font-weight="600">
  Component Name
</text>
<text x="190" y="150" text-anchor="middle" fill="#6e6e80"
      font-size="13">
  Brief description
</text>
```

**Content guidelines:**
- 1-2 lines per box
- Main label: 16px, font-weight: 600, near-black
- Description: 13px, font-weight: 400, gray
- Center-aligned text within boxes

## Layout Principles

**Precise, grid-aligned layout:**
- Snap all coordinates to 8px grid
- Consistent 100px horizontal spacing
- Consistent 120px vertical spacing
- Generous whitespace (40px+ margins)
- No decorative elements

**OpenAI minimalism:**
- Only use color when semantically meaningful (brand green for primary flow)
- White boxes on white background — differentiation through border and label only
- Avoid: shadows, gradients, patterns, icons, decorative elements
- Prefer: straight lines, orthogonal routing, thin strokes

## SVG Template

```xml
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 600" width="960" height="600">
  <style>
    text { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica Neue, 'PingFang SC', 'Microsoft YaHei', 'Microsoft JhengHei', 'SimHei', sans-serif; }
  </style>
  <defs>
    <marker id="arrow-oai" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
      <polygon points="0 0, 10 3.5, 0 7" fill="#71717a"/>
    </marker>
    <marker id="arrow-oai-green" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
      <polygon points="0 0, 10 3.5, 0 7" fill="#10a37f"/>
    </marker>
  </defs>

  <!-- White background -->
  <rect width="960" height="600" fill="#ffffff"/>

  <!-- Title -->
  <text x="480" y="30" text-anchor="middle" fill="#0d0d0d"
        font-size="20" font-weight="600">Diagram Title</text>

  <!-- Standard node -->
  <rect x="100" y="70" width="180" height="80" rx="8" ry="8"
        fill="#ffffff" stroke="#e5e5e5" stroke-width="1.5"/>
  <text x="190" y="100" text-anchor="middle" fill="#0d0d0d"
        font-size="16" font-weight="600">Component</text>
  <text x="190" y="120" text-anchor="middle" fill="#6e6e80"
        font-size="13">Description</text>

  <!-- Connection -->
  <line x1="280" y1="110" x2="400" y2="110"
        stroke="#71717a" stroke-width="1.5" marker-end="url(#arrow-oai)"/>
  <text x="340" y="103" text-anchor="middle" fill="#6e6e80" font-size="12">label</text>
</svg>
```

## Design Philosophy

OpenAI Official style emphasizes:
- **Minimalism**: White on white, only essential visual elements
- **Precision**: Thin strokes, sharp corners (rx=8), grid-aligned
- **Clarity**: Content-first, no visual noise
- **Brand consistency**: `#10a37f` green used sparingly for primary flows

Avoid:
- Shadows and gradients
- Colorful fills (white fill only)
- Thick borders (>2px)
- Decorative elements (icons, patterns, textures)
- Custom fonts (system stack only)
references/style-8-dark-luxury.md
# Style 8 — Dark Luxury

Inspired by the `dark-gold` theme from [Understand-Anything](https://github.com/nicholasgasior/understand-anything).
Deep black canvas with warm champagne-gold accents and hybrid serif/sans typography.
Uniquely warm among all dark styles — closest peer is style-2 (Dark Terminal) but with a premium editorial feel.

## Color Tokens

| Token | Value | Usage |
|-------|-------|-------|
| Background | `#0a0a0a` | Canvas fill (deepest black, no blue tint) |
| Surface | `#111111` | Node / panel fill |
| Elevated | `#1a1a1a` | Secondary panels, sub-headers |
| Accent gold | `#d4a574` | Primary arrows, titles, borders, cluster containers |
| Accent dim | `#c9a96e` | Muted gold borders, secondary accents, cluster labels |
| Accent bright | `#e8c49a` | Highlights, selected state stroke |
| Text primary | `#f5f0eb` | Main labels (warm near-white) |
| Text secondary | `#a39787` | Sub-labels, descriptions |
| Text muted | `#6b5f53` | Annotations, fine print |

## Typography

```
Title / Section label:  Georgia, 'Times New Roman', serif
                        font-size: 21px (diagram title), 14px (section labels)
                        font-weight: 700
                        fill: #f5f0eb / #c9a96e (gold for section headers)

Node name:              -apple-system, 'Helvetica Neue', Arial, 'PingFang SC', sans-serif
                        font-size: 13-14px, font-weight: 600, fill: <bucket color>

Sub-label / detail:     sans-serif, font-size: 10-11px, fill: #a39787 or #6b5f53

Arrow label / legend:   sans-serif, font-size: 10-11px, fill: #a39787

Code / path text:       'Cascadia Code', 'SF Mono', 'Courier New', monospace
                        font-size: 10-11px, fill: #a39787
```

**Rule**: Georgia serif is used ONLY for diagram titles and cluster/section labels (≥14px).
All node names, arrow labels, and fine-print use sans-serif. This prevents CJK readability issues
at small sizes while preserving the luxury editorial hook where it's most visible.

## Node Semantic Color Buckets

Six buckets cover the full color wheel — no two adjacent bucket colors are similar:

| Bucket | Border Color | Use For |
|--------|-------------|---------|
| Code / Logic | `#5a9e6f` (sage green) | functions, classes, modules, algorithms |
| Service / API | `#a78bfa` (soft violet) | services, endpoints, APIs, gateways |
| Data / Storage | `#38bdf8` (sky blue) | databases, tables, files, caches |
| Concept / Domain | `#f87171` (soft rose) | concepts, domains, entities, topics |
| Infra / Config | `#fbbf24` (amber yellow) | infrastructure, config, scripts, pipelines |
| Meta / Doc | `#94a3b8` (cool gray) | documents, schemas, resources, sources |

All nodes share: `rx="6"` rounded rect, `fill="#111111"`, `stroke-width="1.5"`, stroke color matches bucket.

### SVG node example (Code / Logic):

```xml
<rect x="60" y="100" width="260" height="52" rx="6"
      fill="#111111" stroke="#5a9e6f" stroke-width="1.5"/>
<text x="72" y="122" font-family="-apple-system,'Helvetica Neue',Arial,sans-serif"
      font-size="13" font-weight="600" fill="#5a9e6f">MyComponent</text>
<text x="72" y="138" font-family="-apple-system,sans-serif"
      font-size="10" fill="#a39787">React component · state management</text>
```

## Arrow System

| Flow Type | Color | Stroke | Dash | Marker |
|-----------|-------|--------|------|--------|
| Primary / structural | `#d4a574` (gold) | 2px solid | none | gold arrowhead |
| Data flow | `#6ee7b7` (mint) | 1.5px solid | none | mint arrowhead |
| Control / trigger | `#fdba74` (amber-orange) | 1.5px solid | none | orange arrowhead |
| Reference / semantic | `#a39787` (warm muted) | 1px dashed | `4,3` | muted arrowhead |
| Dependency | `#a78bfa` (violet) | 1px dashed | `6,3` | violet arrowhead |
| Feedback / loop | `#d4a574` (gold) | 1.5px curved | — | gold arrowhead |

### SVG arrow markers (add to `<defs>`):

```xml
<defs>
  <!-- Gold — primary structural arrows -->
  <marker id="arr-gold" markerWidth="10" markerHeight="7"
          refX="9" refY="3.5" orient="auto">
    <polygon points="0 0,10 3.5,0 7" fill="#d4a574"/>
  </marker>

  <!-- Amber-orange — control/trigger -->
  <marker id="arr-orange" markerWidth="8" markerHeight="6"
          refX="7" refY="3" orient="auto">
    <polygon points="0 0,8 3,0 6" fill="#fdba74"/>
  </marker>

  <!-- Sky blue — data/storage references -->
  <marker id="arr-blue" markerWidth="10" markerHeight="7"
          refX="9" refY="3.5" orient="auto">
    <polygon points="0 0,10 3.5,0 7" fill="#38bdf8"/>
  </marker>

  <!-- Gray — muted/optional paths -->
  <marker id="arr-gray" markerWidth="8" markerHeight="6"
          refX="7" refY="3" orient="auto">
    <polygon points="0 0,8 3,0 6" fill="#94a3b8"/>
  </marker>
</defs>
```

### Arrow label placement

Use **offset-first**: place label 6–8px above horizontal arrows (not centered on the line).
Add background rect (`fill="#0a0a0a" opacity="0.9"`) only when the label still collides with another element.

## Container / Cluster Style

```xml
<!-- Layer cluster container -->
<rect x="40" y="80" width="880" height="140" rx="8"
      fill="none"
      stroke="#d4a574" stroke-width="0.5"
      stroke-dasharray="6,4" opacity="0.4"/>

<!-- Cluster label: top-left, Georgia serif, gold muted -->
<text x="52" y="98"
      font-family="Georgia,'Times New Roman',serif"
      font-size="11" font-weight="700"
      fill="#c9a96e" opacity="0.7">LAYER NAME</text>
```

## Background Treatment

No gradient needed — depth comes from contrast levels:

1. Canvas `#0a0a0a` — absolute black floor
2. Node surface `#111111` — first elevation (+1 stop)
3. Panel/sub-header `#1a1a1a` — second elevation (+2 stops)
4. Gold `#d4a574` — the only warmth; draws the eye to structure

Optional: subtle ambient radial glow around the central cluster
```xml
<radialGradient id="glow" cx="50%" cy="50%" r="30%">
  <stop offset="0%" stop-color="#d4a574" stop-opacity="0.04"/>
  <stop offset="100%" stop-color="#d4a574" stop-opacity="0"/>
</radialGradient>
<rect width="960" height="600" fill="url(#glow)"/>
```

## Full `<style>` Block

```xml
<style>
  text { font-family: -apple-system,"Helvetica Neue",Arial,"PingFang SC",sans-serif; }
  .ttl { font-family: Georgia,"Times New Roman",serif;
         font-size: 21px; font-weight: 700; fill: #f5f0eb; }
  .lbl { font-family: Georgia,"Times New Roman",serif;
         font-size: 11px; font-weight: 700; fill: #c9a96e; opacity: 0.7; }
  .nm  { font-size: 13px; font-weight: 600; }    /* node name — fill set per bucket */
  .sm  { font-size: 10px; fill: #a39787; }        /* sub-label */
  .xs  { font-size:  9px; fill: #6b5f53; }        /* fine print */
  .al  { font-size: 10px; fill: #8c7e72; }        /* arrow label */
  .fn  { font-family: "Cascadia Code","SF Mono","Courier New",monospace;
         font-size: 10px; fill: #a39787; }        /* code / path text */
</style>
```

## ViewBox Recommendations

| Diagram | ViewBox |
|---------|---------|
| Standard architecture | `0 0 960 600` |
| Tall pipeline / flow | `0 0 960 820` |
| Wide multi-layer | `0 0 1200 600` |

## Comparable Styles

| Style | Background | Accent | Typography |
|-------|-----------|--------|-----------|
| Style 2 Dark Terminal | `#0f0f1a` (blue-black) | `#00ff88` (neon green) | monospace throughout |
| **Style 8 Dark Luxury** | `#0a0a0a` (pure black) | `#d4a574` (champagne gold) | serif titles + sans body |
| Style 3 Blueprint | `#0a1628` (navy) | `#4fc3f7` (cyan) | sans-serif |

**When to choose Style 8**: architecture/pipeline docs where you want editorial gravitas —
README hero images, conference slides, knowledge-base diagrams, premium product docs.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#0f0f1a">
<meta name="color-scheme" content="dark">
<title>fireworks-tech-graph — Describe your system. Get the diagram.</title>
<meta name="description" content="An open-source Codex and Claude Code Agent Skill: turn plain English or Chinese into geometry-safe SVG, PNG, validated semantic GIF, and offline interactive technical diagrams. 12 visual styles, 14 UML types, and executable engineering semantics.">
<meta name="keywords" content="fireworks-tech-graph, technical diagram generator, AI diagram, SVG to GIF, animated architecture diagram, SVG diagram, PNG diagram, interactive diagram, Mermaid alternative, draw.io alternative, architecture diagram, RAG diagram, agent diagram, Codex skill, Claude Code skill, UML, AI/agent patterns">
<meta name="author" content="yizhiyanhua-ai">
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">
<link rel="canonical" href="https://yizhiyanhua-ai.github.io/fireworks-tech-graph/">
<link rel="alternate" hreflang="en" href="https://yizhiyanhua-ai.github.io/fireworks-tech-graph/">
<link rel="alternate" hreflang="x-default" href="https://yizhiyanhua-ai.github.io/fireworks-tech-graph/">
<meta property="og:type" content="website">
<meta property="og:site_name" content="fireworks-tech-graph">
<meta property="og:title" content="fireworks-tech-graph — Describe your system. Get the diagram.">
<meta property="og:description" content="Describe your system in plain English; get publication-ready SVG, PNG, and semantic motion GIFs. 12 styles, 14 UML types, and engineering semantic contracts.">
<meta property="og:url" content="https://yizhiyanhua-ai.github.io/fireworks-tech-graph/">
<meta property="og:image" content="https://yizhiyanhua-ai.github.io/fireworks-tech-graph/assets/samples/sample-style3-blueprint.png">
<meta property="og:locale" content="en_US">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="fireworks-tech-graph — Describe your system. Get the diagram.">
<meta name="twitter:description" content="Plain-English to publication-ready SVG, PNG, and semantic motion GIFs. 12 styles, 14 UML types, engineering semantics.">
<meta name="twitter:image" content="https://yizhiyanhua-ai.github.io/fireworks-tech-graph/assets/samples/sample-style3-blueprint.png">
<link rel="icon" href="agentloop-core.svg" type="image/svg+xml">
<link rel="manifest" href="site.webmanifest">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Sora:wght@600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"SoftwareApplication","name":"fireworks-tech-graph","softwareVersion":"1.2.0","applicationCategory":"DeveloperApplication","operatingSystem":"Cross-platform (Codex and Claude Code)","description":"An open-source Agent Skill and CLI that turns plain-English system descriptions into geometry-safe SVG, PNG, validated semantic GIF, and offline interactive technical diagrams. 12 visual styles, 14 UML diagram types, and executable engineering semantic contracts.","url":"https://yizhiyanhua-ai.github.io/fireworks-tech-graph/","downloadUrl":"https://github.com/yizhiyanhua-ai/fireworks-tech-graph/releases/tag/v1.2.0","license":"https://github.com/yizhiyanhua-ai/fireworks-tech-graph/blob/main/LICENSE","offers":{"@type":"Offer","price":"0","priceCurrency":"USD"},"author":{"@type":"Organization","name":"yizhiyanhua-ai","url":"https://github.com/yizhiyanhua-ai"}}
</script>
<style>
  :root{--bg:#08080f;--panel:#0f172a;--panel2:#11182b;--border:#26314a;--border2:#334155;--txt:#eef2ff;--dim:#94a3b8;--faint:#5b6478;--purple:#a855f7;--orange:#f97316;--blue:#3b82f6;--green:#10b981;--gold:#eab308;--grad:linear-gradient(110deg,#3b82f6,#a855f7 50%,#f97316)}
  *{box-sizing:border-box;margin:0;padding:0}
  html{scroll-behavior:smooth}
  body{background:radial-gradient(1100px 600px at 82% -8%,rgba(59,130,246,.18),transparent 60%),radial-gradient(900px 520px at -8% 12%,rgba(168,85,247,.12),transparent 58%),linear-gradient(180deg,#08080f,#0d0d1a 40%,#08080f);background-attachment:fixed;color:var(--txt);font-family:'Inter',system-ui,sans-serif;line-height:1.6;-webkit-font-smoothing:antialiased;overflow-x:hidden}
  ::selection{background:rgba(168,85,247,.35);color:#fff}
  a{color:inherit}
  .wrap{max-width:1140px;margin:0 auto;padding:0 22px}
  .grad{background:var(--grad);-webkit-background-clip:text;background-clip:text;color:transparent}
  .mono{font-family:'JetBrains Mono',ui-monospace,monospace}
  .topbar{display:block;text-align:center;text-decoration:none;font-size:13px;color:var(--txt);font-weight:500;background:linear-gradient(90deg,rgba(59,130,246,.18),rgba(168,85,247,.14),rgba(249,115,22,.18));border-bottom:1px solid var(--border);padding:9px 16px;transition:background .2s}
  .topbar:hover{background:linear-gradient(90deg,rgba(59,130,246,.3),rgba(168,85,247,.24),rgba(249,115,22,.3))}
  .topbar b{color:#fff}
  .topbar .arr{opacity:.6;margin-left:4px}
  nav.bar{position:sticky;top:0;z-index:50;backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);background:rgba(8,8,15,.72);border-bottom:1px solid transparent;transition:.25s}
  nav.bar.scrolled{border-color:var(--border);background:rgba(8,8,15,.9)}
  .nav-in{display:flex;align-items:center;gap:18px;height:64px}
  .brand{display:flex;align-items:center;gap:11px;text-decoration:none;font-weight:700;font-size:16px;letter-spacing:-.01em}
  .brand .dot{width:26px;height:26px;border-radius:7px;background:var(--grad);display:grid;place-items:center;color:#0a0a12;font-weight:800;font-size:14px}
  .navlinks{display:none;gap:6px;margin-left:14px}
  .navlinks a{text-decoration:none;color:var(--dim);font-size:14px;font-weight:500;padding:8px 12px;border-radius:8px;transition:.18s}
  .navlinks a:hover{color:var(--txt);background:rgba(255,255,255,.05)}
  .nav-cta{margin-left:auto;display:flex;gap:10px;align-items:center}
  .btn{display:inline-flex;align-items:center;gap:8px;text-decoration:none;font-weight:600;font-size:14px;padding:9px 16px;border-radius:10px;transition:.18s;border:1px solid transparent;cursor:pointer;white-space:nowrap}
  .btn-ghost{color:var(--dim);border-color:var(--border2)}
  .btn-ghost:hover{color:var(--txt);border-color:var(--blue)}
  .btn-gh{color:#fff;background:rgba(255,255,255,.06);border-color:var(--border2)}
  .btn-gh:hover{background:rgba(255,255,255,.12);border-color:#fff}
  .btn-primary{color:#0a0a12;background:var(--grad);font-weight:700;box-shadow:0 8px 30px -10px rgba(59,130,246,.6)}
  .btn-primary:hover{transform:translateY(-1px);box-shadow:0 12px 36px -10px rgba(59,130,246,.8)}
  header.hero{position:relative;padding:80px 0 52px;text-align:center;overflow:hidden}
  .eyebrow{display:inline-flex;align-items:center;gap:8px;font-family:'JetBrains Mono',monospace;font-size:12px;letter-spacing:.16em;text-transform:uppercase;color:var(--blue);background:rgba(59,130,246,.08);border:1px solid rgba(59,130,246,.28);padding:6px 14px;border-radius:999px;margin-bottom:24px}
  .eyebrow .d{width:7px;height:7px;border-radius:50%;background:var(--green);box-shadow:0 0 10px var(--green)}
  h1{font-family:'Sora',sans-serif;font-weight:800;font-size:clamp(34px,6vw,60px);line-height:1.05;letter-spacing:-.025em;margin:0 auto 20px;max-width:18ch}
  .hero .lede{font-size:clamp(16px,2.2vw,20px);color:var(--dim);max-width:62ch;margin:0 auto 32px}
  .hero .lede b{color:var(--txt);font-weight:600}
  .cta-row{display:flex;gap:13px;justify-content:center;flex-wrap:wrap;margin-bottom:24px}
  .cta-row .btn{padding:13px 22px;font-size:15px}
  .install{max-width:620px;margin:0 auto 28px;text-align:left}
  .install .label{display:flex;align-items:center;justify-content:space-between;background:#0a0a14;border:1px solid var(--border);border-bottom:none;border-radius:12px 12px 0 0;padding:9px 14px}
  .install .label span{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--faint);letter-spacing:.06em}
  .install pre{font-family:'JetBrains Mono',monospace;font-size:13px;line-height:1.7;color:#cbd5e1;background:#07070e;border:1px solid var(--border);border-radius:0 12px 12px 0;padding:14px 16px;overflow:auto;margin:0;border-top:none}
  .install pre .s{color:var(--green)}
  .copy{background:rgba(59,130,246,.12);border:1px solid rgba(59,130,246,.3);color:var(--blue);font-family:'JetBrains Mono',monospace;font-size:11px;padding:4px 9px;border-radius:6px;cursor:pointer;transition:.18s}
  .copy:hover{background:rgba(59,130,246,.22)}
  .stats{display:flex;gap:14px;justify-content:center;flex-wrap:wrap}
  .stat{background:rgba(255,255,255,.03);border:1px solid var(--border);border-radius:12px;padding:14px 20px;min-width:120px}
  .stat b{display:block;font-family:'Sora',sans-serif;font-size:24px;font-weight:800}
  .stat span{font-size:11.5px;color:var(--faint);font-family:'JetBrains Mono',monospace}
  section{padding:72px 0;position:relative}
  .sec-head{text-align:center;max-width:680px;margin:0 auto 44px}
  .sec-head .k{font-family:'JetBrains Mono',monospace;font-size:12px;letter-spacing:.18em;text-transform:uppercase;color:var(--blue);margin-bottom:12px}
  .sec-head h2{font-family:'Sora',sans-serif;font-weight:700;font-size:clamp(26px,4vw,38px);letter-spacing:-.02em;margin-bottom:14px}
  .sec-head p{color:var(--dim);font-size:16.5px}
  .benefits{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:16px}
  .benefit{background:linear-gradient(180deg,var(--panel),var(--panel2));border:1px solid var(--border);border-radius:16px;padding:26px 24px;transition:.2s}
  .benefit:hover{transform:translateY(-3px);border-color:var(--blue)}
  .benefit .ic{font-size:24px;margin-bottom:12px;display:block}
  .benefit h3{font-family:'Sora',sans-serif;font-size:18px;margin-bottom:8px;font-weight:700}
  .benefit p{color:var(--dim);font-size:14px;line-height:1.55}
  .steps{max-width:760px;margin:0 auto;counter-reset:s}
  .step{display:flex;gap:18px;padding:18px 0;border-bottom:1px solid var(--border)}
  .step:last-child{border-bottom:none}
  .step .n{flex:none;width:36px;height:36px;border-radius:10px;background:var(--grad);color:#0a0a12;font-family:'JetBrains Mono',monospace;font-weight:700;display:grid;place-items:center}
  .step h3{font-size:17px;margin-bottom:4px}
  .step p{color:var(--dim);font-size:14.5px}
  .step code{font-family:'JetBrains Mono',monospace;font-size:13px;color:var(--green);background:rgba(16,185,129,.08);padding:2px 7px;border-radius:5px}
  .gallery{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px}
  .gcard{display:block;text-decoration:none;color:inherit;background:linear-gradient(180deg,var(--panel),var(--panel2));border:1px solid var(--border);border-radius:14px;overflow:hidden;transition:.2s}
  .gcard:hover{transform:translateY(-3px);border-color:var(--blue);box-shadow:0 18px 40px -22px rgba(59,130,246,.6)}
  .gcard img{width:100%;display:block;background:#0a0a14;border-bottom:1px solid var(--border);max-height:300px;object-fit:contain}
  .gcard .cap{padding:12px 14px}
  .gcard .cap .k{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--faint);letter-spacing:.08em}
  .gcard .cap h4{font-size:14.5px;margin-top:3px;font-weight:600}
  .motion-showcase{display:block;text-decoration:none;color:inherit;margin:28px 0 18px;background:linear-gradient(180deg,var(--panel),var(--panel2));border:1px solid var(--border);border-radius:18px;overflow:hidden;box-shadow:0 28px 70px -46px rgba(59,130,246,.85)}
  .motion-showcase img{display:block;width:100%;max-height:760px;object-fit:contain;background:#08080f}
  .motion-showcase .cap{display:flex;justify-content:space-between;gap:18px;align-items:center;padding:15px 18px;border-top:1px solid var(--border);color:var(--dim);font-family:'JetBrains Mono',monospace;font-size:12px}
  .motion-showcase .cap b{color:var(--txt);font-family:'Inter',system-ui,sans-serif;font-size:14px}
  .patterns{display:flex;flex-wrap:wrap;gap:10px;justify-content:center;max-width:760px;margin:0 auto}
  .patterns span{font-family:'JetBrains Mono',monospace;font-size:13px;color:var(--dim);background:rgba(255,255,255,.04);border:1px solid var(--border);padding:7px 13px;border-radius:999px}
  .faq{max-width:780px;margin:0 auto}
  .faq details{background:linear-gradient(180deg,var(--panel),var(--panel2));border:1px solid var(--border);border-radius:12px;margin-bottom:10px;overflow:hidden}
  .faq summary{padding:18px 22px;cursor:pointer;font-weight:600;font-size:15.5px;list-style:none;display:flex;justify-content:space-between;align-items:center;gap:14px}
  .faq summary::-webkit-details-marker{display:none}
  .faq summary::after{content:"+";color:var(--blue);font-size:20px;font-weight:700}
  .faq details[open] summary::after{transform:rotate(45deg)}
  .faq .a{padding:0 22px 18px;color:var(--dim);font-size:14.5px;line-height:1.6}
  .sponsor-band{position:relative;border:1px solid var(--border);border-radius:24px;overflow:hidden;background:radial-gradient(700px 320px at 50% -30%,rgba(234,179,8,.18),transparent 70%),linear-gradient(180deg,#0c0c18,#0a0a14);padding:56px 24px}
  .sponsor-band h2{font-family:'Sora',sans-serif;font-weight:800;font-size:clamp(26px,4vw,40px);letter-spacing:-.02em;margin-bottom:14px;text-align:center}
  .sponsor-band .pitch{color:var(--dim);max-width:56ch;margin:0 auto 30px;text-align:center;font-size:16px}
  .tiers{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;max-width:880px;margin:0 auto 28px}
  .tier{display:block;text-decoration:none;color:inherit;background:rgba(255,255,255,.03);border:1px solid var(--border);border-radius:14px;padding:22px 20px;text-align:left;transition:.2s}
  .tier:hover{transform:translateY(-3px);border-color:var(--gold)}
  .tier.featured{border-color:var(--gold);background:rgba(234,179,8,.06)}
  .tier .p{font-family:'JetBrains Mono',monospace;font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--faint)}
  .tier .amt{font-family:'Sora',sans-serif;font-size:26px;font-weight:800;margin:6px 0 2px}
  .tier .per{font-size:12px;color:var(--faint);font-family:'JetBrains Mono',monospace}
  .tier ul{list-style:none;margin:14px 0 0;padding:0}
  .tier li{font-size:13px;color:var(--dim);padding:4px 0 4px 20px;position:relative}
  .tier li::before{content:"✓";position:absolute;left:0;color:var(--gold);font-weight:700}
  .sponsor-cta{text-align:center;display:flex;gap:12px;justify-content:center;flex-wrap:wrap}
  footer{border-top:1px solid var(--border);padding:40px 0 60px;margin-top:40px}
  .foot-in{display:flex;justify-content:space-between;align-items:center;gap:20px;flex-wrap:wrap}
  .foot-in small{color:var(--faint);font-family:'JetBrains Mono',monospace;font-size:12px}
  .foot-links{display:flex;gap:18px;flex-wrap:wrap}
  .foot-links a{color:var(--dim);font-size:13px;text-decoration:none;font-family:'JetBrains Mono',monospace}
  .foot-links a:hover{color:var(--txt)}
  .reveal{opacity:0;transform:translateY(18px);transition:opacity .6s cubic-bezier(.2,.7,.2,1),transform .6s cubic-bezier(.2,.7,.2,1)}
  .reveal.in{opacity:1;transform:none}
  @media(min-width:860px){.navlinks{display:flex}}
  @media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important;scroll-behavior:auto!important}.reveal{opacity:1;transform:none}}
</style>
</head>
<body>
<a class="topbar" href="https://github.com/yizhiyanhua-ai/fireworks-tech-graph/releases/tag/v1.2.0" target="_blank" rel="noopener">v1.2.0 · Semantic SVG→GIF motion is live · <b>12 styles, one verified runtime</b><span class="arr">→</span></a>

<nav class="bar" id="nav">
  <div class="wrap nav-in">
    <a class="brand" href="#top"><span class="dot">f</span> fireworks-tech-graph</a>
    <div class="navlinks">
      <a href="#how">How it works</a>
      <a href="#styles">Styles</a>
      <a href="#patterns">Patterns</a>
      <a href="#faq">FAQ</a>
    </div>
    <div class="nav-cta">
      <a class="btn btn-gh" href="https://github.com/yizhiyanhua-ai/fireworks-tech-graph" target="_blank" rel="noopener">GitHub</a>
      <a class="btn btn-primary" href="#styles">See the diagrams</a>
    </div>
  </div>
</nav>

<header class="hero" id="top">
  <div class="wrap">
    <span class="eyebrow"><span class="d"></span> v1.2.0 · Open-source · Codex + Claude Code Skill</span>
    <h1>Stop drawing diagrams <span class="grad">by hand.</span></h1>
    <p class="lede">Describe your system in plain English or Chinese — get <b>publication-ready SVG + PNG + semantic GIF</b> technical diagrams. Connectors draw on from an empty frame, then stay alive with persistent data flow.</p>
    <div class="cta-row">
      <a class="btn btn-primary" href="https://github.com/yizhiyanhua-ai/fireworks-tech-graph" target="_blank" rel="noopener">★ Star on GitHub</a>
      <a class="btn btn-ghost" href="#styles">See 12 styles →</a>
      <a class="btn btn-ghost" href="examples/interactive-architecture.html" target="_blank" rel="noopener">Open interactive demo ↗</a>
    </div>
    <div class="install">
      <div class="label"><span>~ install complete skill in Codex + Claude Code</span><button class="copy" id="copy">copy</button></div>
      <pre id="code">npx -y skills@1.5.17 add <span class="s">yizhiyanhua-ai/fireworks-tech-graph/skills/fireworks-tech-graph</span> --agent codex claude-code -g -y --copy</pre>
    </div>
    <div class="stats">
      <div class="stat"><b>12</b><span>VISUAL STYLES</span></div>
      <div class="stat"><b>14</b><span>UML TYPES</span></div>
      <div class="stat"><b>40+</b><span>BRAND ICONS</span></div>
      <div class="stat"><b>5.75s</b><span>DRAW-ON + SETTLED FLOW</span></div>
    </div>
  </div>
</header>

<section id="benefits" style="padding:64px 0 16px">
  <div class="wrap">
    <div class="sec-head reveal">
      <div class="k">Why teams use it</div>
      <h2>Mermaid is too limited. draw.io is too manual.</h2>
      <p>fireworks-tech-graph understands AI/Agent domain patterns, enforces a consistent visual language, and exports high-res static or semantic motion output — all from a sentence.</p>
    </div>
    <div class="benefits">
      <div class="benefit reveal"><span class="ic">📝</span><h3>Natural language in</h3><p>"Draw a Mem0 memory architecture, dark style." The skill classifies the diagram type and style, then generates the SVG.</p></div>
      <div class="benefit reveal"><span class="ic">🎨</span><h3>12 production styles</h3><p>Styles 1–7 and 9–12 are deterministic generator profiles. Dark Luxury keeps its AI-authored editorial composition and static regression fixture.</p></div>
      <div class="benefit reveal"><span class="ic">🤖</span><h3>AI/Agent patterns built-in</h3><p>RAG, Agentic RAG, Mem0, Multi-Agent, Tool Call Flow — with the right shape vocabulary (hexagons for Agents, ringed cylinders for Vector Stores).</p></div>
      <div class="benefit reveal"><span class="ic">🧩</span><h3>Geometry-safe semantic graph</h3><p>Typed nodes and edges, orthogonal routing, exact waypoints, port fan-out, label and legend avoidance, plus verified bridge jumps keep wires out of boxes.</p></div>
      <div class="benefit reveal"><span class="ic">🏷️</span><h3>40+ brand icons</h3><p>OpenAI, Anthropic, Pinecone, Weaviate, Kafka, PostgreSQL, LangChain, LangGraph, CrewAI, Kubernetes, Grafana — correct brand colors, inline SVG, no CDN fetch.</p></div>
      <div class="benefit reveal"><span class="ic">⚡</span><h3>Validated SVG → GIF motion</h3><p>Every style shares one fail-closed motion runtime: empty-frame draw-on, semantic connector timing, and an extended settled phase with persistent data flow.</p></div>
    </div>
  </div>
</section>

<section id="how" style="background:linear-gradient(180deg,transparent,rgba(59,130,246,.04),transparent)">
  <div class="wrap">
    <div class="sec-head reveal">
      <div class="k">How it works</div>
      <h2>From sentence to diagram in four steps.</h2>
    </div>
    <div class="steps">
      <div class="step reveal"><div class="n">1</div><div><h3>Install the complete skill</h3><p><code>npx -y skills@1.5.17 add yizhiyanhua-ai/fireworks-tech-graph/skills/fireworks-tech-graph --agent codex claude-code -g -y --copy</code> installs every bundled file for both runtimes.</p></div></div>
      <div class="step reveal"><div class="n">2</div><div><h3>Choose static or motion output</h3><p>Use CairoSVG for PNG, or ask for a GIF to activate the focused SVG-to-GIF runtime with Chromium, FFmpeg, and ImageMagick.</p></div></div>
      <div class="step reveal"><div class="n">3</div><div><h3>Describe your system</h3><p>"Draw a RAG pipeline flowchart." Or: "Generate a Mem0 architecture diagram, dark style." Triggers automatically on phrases like <em>draw / visualize / architecture diagram / 画图</em>.</p></div></div>
      <div class="step reveal"><div class="n">4</div><div><h3>Ship verified output</h3><p>The skill checks geometry and engineering semantics, then probes GIF dimensions, frames, duration, cadence, and infinite looping before installing the final artifact atomically.</p></div></div>
    </div>
  </div>
</section>

<section id="styles">
  <div class="wrap">
    <div class="sec-head reveal">
      <div class="k">Style gallery</div>
      <h2>Twelve distinct engineering views.</h2>
      <p>Each style keeps its own engineering scenario and visual grammar. v1.2.0 adds the same approved motion contract to all twelve: draw the structure, hand off to the scene, then keep the final system alive with visible data flow.</p>
    </div>
    <a class="motion-showcase reveal" href="assets/samples/showcase-12-styles.gif" target="_blank" rel="noopener">
      <img src="assets/samples/showcase-12-styles.gif" data-static-src="assets/samples/showcase-12-styles.png" alt="All 12 fireworks-tech-graph styles animated with draw-on and persistent settled data flow">
      <div class="cap"><b>v1.2.0 · 12-style motion overview</b><span>5.75 seconds · infinite loop · verified GIF</span></div>
    </a>
    <div class="gallery">
      <a class="gcard reveal" href="assets/samples/sample-style1-flat.gif" target="_blank"><img src="assets/samples/sample-style1-flat.gif" data-static-src="assets/samples/sample-style1-flat.png" alt="Animated Mem0 Memory Architecture in Flat Icon style" loading="lazy"><div class="cap"><div class="k">STYLE 01 · FLAT ICON</div><h4>Mem0 Memory Architecture</h4></div></a>
      <a class="gcard reveal" href="assets/samples/sample-style2-dark.gif" target="_blank"><img src="assets/samples/sample-style2-dark.gif" data-static-src="assets/samples/sample-style2-dark.png" alt="Animated Tool Call Flow in Dark Terminal style" loading="lazy"><div class="cap"><div class="k">STYLE 02 · DARK TERMINAL</div><h4>Tool Call Flow</h4></div></a>
      <a class="gcard reveal" href="assets/samples/sample-style3-blueprint.gif" target="_blank"><img src="assets/samples/sample-style3-blueprint.gif" data-static-src="assets/samples/sample-style3-blueprint.png" alt="Animated Microservices Architecture in Blueprint style" loading="lazy"><div class="cap"><div class="k">STYLE 03 · BLUEPRINT</div><h4>Microservices Architecture</h4></div></a>
      <a class="gcard reveal" href="assets/samples/sample-style4-notion.gif" target="_blank"><img src="assets/samples/sample-style4-notion.gif" data-static-src="assets/samples/sample-style4-notion.png" alt="Animated Agent Memory Types in Notion Clean style" loading="lazy"><div class="cap"><div class="k">STYLE 04 · NOTION CLEAN</div><h4>Agent Memory Types</h4></div></a>
      <a class="gcard reveal" href="assets/samples/sample-style5-glass.gif" target="_blank"><img src="assets/samples/sample-style5-glass.gif" data-static-src="assets/samples/sample-style5-glass.png" alt="Animated Multi-Agent Collaboration in Glassmorphism style" loading="lazy"><div class="cap"><div class="k">STYLE 05 · GLASSMORPHISM</div><h4>Multi-Agent Collaboration</h4></div></a>
      <a class="gcard reveal" href="assets/samples/sample-style6-claude.gif" target="_blank"><img src="assets/samples/sample-style6-claude.gif" data-static-src="assets/samples/sample-style6-claude.png" alt="Animated System Architecture in Claude Official style" loading="lazy"><div class="cap"><div class="k">STYLE 06 · CLAUDE OFFICIAL</div><h4>System Architecture</h4></div></a>
      <a class="gcard reveal" href="assets/samples/sample-style7-openai.gif" target="_blank"><img src="assets/samples/sample-style7-openai.gif" data-static-src="assets/samples/sample-style7-openai.png" alt="Animated API Integration Flow in OpenAI Official style" loading="lazy"><div class="cap"><div class="k">STYLE 07 · OPENAI OFFICIAL</div><h4>API Integration Flow</h4></div></a>
      <a class="gcard reveal" href="assets/samples/sample-style8-dark-luxury.gif" target="_blank"><img src="assets/samples/sample-style8-dark-luxury.gif" data-static-src="assets/samples/sample-style8-dark-luxury.png" alt="Animated Agent Runtime Architecture in Dark Luxury style" loading="lazy"><div class="cap"><div class="k">STYLE 08 · DARK LUXURY</div><h4>Agent Runtime Architecture</h4></div></a>
      <a class="gcard reveal" href="assets/samples/sample-style9-c4-review-canvas.gif" target="_blank"><img src="assets/samples/sample-style9-c4-review-canvas.gif" data-static-src="assets/samples/sample-style9-c4-review-canvas.png" alt="Animated Checkout Container Review in C4 Review Canvas style" loading="lazy"><div class="cap"><div class="k">STYLE 09 · C4 REVIEW CANVAS</div><h4>Checkout Container Review</h4></div></a>
      <a class="gcard reveal" href="assets/samples/sample-style10-cloud-fabric.gif" target="_blank"><img src="assets/samples/sample-style10-cloud-fabric.gif" data-static-src="assets/samples/sample-style10-cloud-fabric.png" alt="Animated Active-Active Checkout Deployment in Cloud Fabric style" loading="lazy"><div class="cap"><div class="k">STYLE 10 · CLOUD FABRIC</div><h4>Active–Active Deployment</h4></div></a>
      <a class="gcard reveal" href="assets/samples/sample-style11-event-transit.gif" target="_blank"><img src="assets/samples/sample-style11-event-transit.gif" data-static-src="assets/samples/sample-style11-event-transit.png" alt="Animated Checkout Event Line in Event Transit style" loading="lazy"><div class="cap"><div class="k">STYLE 11 · EVENT TRANSIT</div><h4>Checkout Event Line</h4></div></a>
      <a class="gcard reveal" href="assets/samples/sample-style12-ops-pulse.gif" target="_blank"><img src="assets/samples/sample-style12-ops-pulse.gif" data-static-src="assets/samples/sample-style12-ops-pulse.png" alt="Animated Checkout Reliability Pulse in Ops Pulse style" loading="lazy"><div class="cap"><div class="k">STYLE 12 · OPS PULSE</div><h4>Checkout Reliability Pulse</h4></div></a>
    </div>
  </div>
</section>

<section id="patterns" style="background:linear-gradient(180deg,transparent,rgba(168,85,247,.04),transparent)">
  <div class="wrap">
    <div class="sec-head reveal">
      <div class="k">AI / Agent domain patterns</div>
      <h2>Speaks the vocabulary of modern AI systems.</h2>
      <p>Not just generic flowcharts — built-in patterns for the architectures you actually document.</p>
    </div>
    <div class="patterns reveal">
      <span>RAG Pipeline</span><span>Agentic RAG</span><span>Agentic Search</span><span>Mem0 Memory Layer</span><span>Agent Memory Types</span><span>Multi-Agent</span><span>Tool Call Flow</span><span>Class · Component · Deployment</span><span>Sequence · State Machine</span><span>Activity · Use Case</span><span>ER Diagram</span><span>Mind Map · Comparison Matrix</span>
    </div>
  </div>
</section>

<section id="faq">
  <div class="wrap">
    <div class="sec-head reveal">
      <div class="k">FAQ</div>
      <h2>Questions, answered.</h2>
    </div>
    <div class="faq reveal">
      <details><summary>How is this different from Mermaid?</summary><div class="a">Mermaid's DSL is limited and every diagram looks the same. fireworks-tech-graph takes natural language, understands AI/Agent domain patterns, enforces a consistent semantic shape vocabulary, and exports high-res PNG — across 12 distinct visual styles.</div></details>
      <details><summary>Do I need a specific renderer?</summary><div class="a">You need one PNG renderer. <code>pip install cairosvg</code> is recommended (best CSS support). <code>brew install librsvg</code> (rsvg-convert) and <code>npm install puppeteer</code> also work. SVG is always produced regardless.</div></details>
      <details><summary>What can I say to trigger it?</summary><div class="a">Natural phrases like "draw a diagram", "visualize", "architecture diagram", "flowchart", "sequence diagram" — in English or Chinese (画图 / 帮我画 / 生成图 / 出图). The skill auto-classifies the diagram type and style.</div></details>
      <details><summary>How do I generate the animated GIF?</summary><div class="a">Ask naturally: “把这张 SVG 做成 GIF” or “Generate a GIF for this diagram.” v1.2.0 keeps the path focused on semantic SVG input, then validates the draw-on sequence, settled data flow, 5.75-second timeline, frame cadence, and infinite loop.</div></details>
      <details><summary>Does it really understand engineering semantics?</summary><div class="a">Yes — in addition to AI/Agent patterns, Styles 9–12 validate C4 abstraction levels, deployment ownership, event-rail topology, and golden-signal/trace contracts before rendering. The visual treatment cannot hide missing engineering facts.</div></details>
      <details><summary>Can I use my own brand icons?</summary><div class="a">40+ brand icons ship built-in (OpenAI, Anthropic, Pinecone, Weaviate, Kafka, LangChain, LangGraph, CrewAI, Kubernetes, Grafana…) as inline SVG with correct brand colors — no CDN fetch needed.</div></details>
      <details><summary>Is it free?</summary><div class="a">MIT, open source, runs locally in Codex and Claude Code. If it saves you time, consider <a href="#sponsor">sponsoring</a>.</div></details>
    </div>
  </div>
</section>

<section id="sponsor" style="padding:48px 0 24px">
  <div class="wrap">
    <div class="sponsor-band reveal">
      <h2>Support fireworks-tech-graph</h2>
      <p class="pitch">Open source, MIT, built in the open. Sponsorship funds new styles, more domain patterns, and keeps the diagrams sharp.</p>
      <div class="tiers">
        <a class="tier" href="https://paypal.me/yizhiyanhua/50" target="_blank" rel="noopener"><div class="p">☕ Backer</div><div class="amt">$50</div><div class="per">suggested donation</div><ul><li>Name in BACKERS</li><li>Early access to new styles</li></ul></a>
        <a class="tier featured" href="https://paypal.me/yizhiyanhua/99" target="_blank" rel="noopener"><div class="p">🎯 Studio</div><div class="amt">$99</div><div class="per">most popular</div><ul><li>Everything in Backer</li><li>Priority issue triage</li><li>Vote on new styles</li></ul></a>
        <a class="tier" href="https://paypal.me/yizhiyanhua/199" target="_blank" rel="noopener"><div class="p">🏛️ Patron</div><div class="amt">$199</div><div class="per">for teams</div><ul><li>Everything in Studio</li><li>1 custom style / quarter</li><li>Logo in the README</li></ul></a>
      </div>
      <div class="sponsor-cta">
        <a class="btn btn-primary" href="https://paypal.me/yizhiyanhua" target="_blank" rel="noopener">💙 Donate via PayPal</a>
        <a class="btn btn-ghost" href="https://paypal.me/yizhiyanhua" target="_blank" rel="noopener">Choose your own amount →</a>
      </div>
    </div>
  </div>
</section>

<footer>
  <div class="wrap foot-in">
    <a class="brand" href="#top"><span class="dot">f</span> fireworks-tech-graph</a>
    <small>MIT · describe it, ship the diagram</small>
    <div class="foot-links">
      <a href="https://github.com/yizhiyanhua-ai/fireworks-tech-graph" target="_blank" rel="noopener">GitHub</a>
      <a href="https://github.com/yizhiyanhua-ai/fireworks-tech-graph#readme" target="_blank" rel="noopener">README</a>
      <a href="https://paypal.me/yizhiyanhua" target="_blank" rel="noopener">Donate ❤️</a>
      <a href="https://www.npmjs.com/package/@yizhiyanhua-ai/fireworks-tech-graph" target="_blank" rel="noopener">npm</a>
    </div>
  </div>
</footer>

<script>
  const nav=document.getElementById('nav');const onScroll=()=>nav.classList.toggle('scrolled',scrollY>8);onScroll();addEventListener('scroll',onScroll,{passive:true});
  const reduce=matchMedia('(prefers-reduced-motion:reduce)').matches;if(reduce){document.querySelectorAll('img[data-static-src]').forEach(img=>img.src=img.dataset.staticSrc)}const io=new IntersectionObserver(es=>es.forEach(e=>{if(e.isIntersecting){e.target.classList.add('in');io.unobserve(e.target)}}),{threshold:.12});document.querySelectorAll('.reveal').forEach(el=>reduce?el.classList.add('in'):io.observe(el));
  const code=document.getElementById('code'),btn=document.getElementById('copy');btn?.addEventListener('click',async()=>{try{await navigator.clipboard.writeText(code.innerText.trim());const t=btn.textContent;btn.textContent='copied ✓';setTimeout(()=>btn.textContent=t,1400);}catch(e){}});
</script>
</body>
</html>
schemas/architecture-v1.schema.json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://yizhiyanhua-ai.github.io/fireworks-tech-graph/schemas/architecture-v1.schema.json",
  "allOf": [
    { "$ref": "diagram-v1.schema.json" },
    { "properties": { "mode": { "const": "architecture" } } }
  ]
}
references/style-9-c4-review-canvas.md
# Style 9: C4 Review Canvas

A review-room canvas for C4 context, container, and component diagrams. The
warm paper surface and deterministic pencil echoes make design discussion feel
human while the underlying geometry remains exact.

## Best fit

- Architecture Decision Records and design reviews
- C4 context, container, or component views
- Responsibility and dependency reviews
- Diagrams that need annotations without looking like a slide template

## Visual tokens

| Token | Value |
|---|---|
| Canvas | `#f7f2e8` |
| Card | `#fffdf7` |
| Primary text | `#24312f` |
| Boundary | `#8c7d68`, dashed |
| Primary relationship | `#365f56` |
| State change | `#a44a3f` |
| Data/read | `#356a8a` |
| Asynchronous | `#7a5c99` |

Use Avenir/system sans-serif. Keep type labels uppercase and small; keep
descriptions sentence-like and technologies terse.

## Required semantic contract

Select `semantic_profile: "c4-review"` and provide:

- `diagram_type: "c4"`
- one `c4_level`: `context`, `container`, or `component`
- `scope`, `title`, integer `rough_seed`, and a legend
- every node: `c4_type`, `label`, and `description`
- container/component nodes: `technology` and a card at least `170×96`
- every relationship: short `label` and explicit `protocol`

Do not mix a deeper C4 element into a higher-level view. The runtime rejects a
component in a container view and rejects unknown boundary parents.

## Roughness without geometry drift

The `rough_seed` creates stable decorative second strokes. Decorations use
`data-graph-role="decoration"`; the ordinary orthogonal path remains the only
semantic edge. A seed must reproduce byte-identical SVG across runs.

Never jitter:

- node bounds or port anchors
- the semantic route or arrowhead
- label bounds
- container gutters

## Composition rules

- One abstraction level per canvas
- At least `40px` between nodes and `20px` inside boundaries
- Zero undeclared crossings and zero bridge crossings for official samples
- At most two bends per relationship; prefer aligned straight paths
- Put external people/systems outside the system boundary with a clear gutter
- Keep labels short enough to sit in whitespace, not on top of cards
- Wrap responsibility copy to at most two lines; keep technology in the lower-right safe area

## Signature checklist

- Top-right dashed review stamp showing the C4 level and review state
- Uppercase C4 type, responsibility copy, and a separate technology footer on each card
- Dashed external-system cards and deterministic pencil echoes that never alter geometry
- Two-line relationship badges: action first, protocol in brackets below

## Prompt cues

- English: `C4 review board`, `ADR review canvas`, `container responsibility review`
- 中文:`C4 评审画布`、`ADR 评审图`、`职责边界评审图`
- Copyable cue: `Use Style 9 C4 Review Canvas; show one C4 level, responsibilities, technologies, review state, and relationship protocols.`

## Do not blend with

Do not add Region/VPC ownership bands, event-metro stations, or golden-signal
metric chips. If those facts dominate the request, route to Style 10, 11, or
12 instead of turning the C4 review into a mixed abstraction canvas.

## Fixture

`fixtures/c4-review-canvas-style9.json` demonstrates a checkout container
review with a deterministic seed and a 100-point showcase composition score.
references/svg-layout-best-practices.md
# SVG Technical Diagram Layout Best Practices

## Universal Layout Rules (Apply to All Styles)

### 1. Component Spacing
- **Minimum clearance between components**: 80px (edge to edge)
- **Minimum clearance for arrow paths**: 60px from component edges
- **Layer vertical spacing**: 120px between horizontal layers
- **Same-layer horizontal spacing**: 100-120px between components

### 2. Arrow Routing & Connection Points

#### Connection Point Rules
- **Never connect arrows to component corners** - use midpoints of edges
- **Entry/exit points**:
  - Top edge: `cx ± offset` where offset = 0 for single arrow, ±30px for multiple
  - Bottom edge: same rule
  - Left/right edges: `cy ± offset`
- **Clearance from corners**: minimum 20px

#### Arrow Path Routing
- **Avoid diagonal lines crossing components** - use orthogonal routing (L-shaped paths)
- **For curved arrows**:
  - Control point should be at least 40px away from any component edge
  - Use intermediate waypoints for complex routing: `M x1,y1 L x2,y2 Q cx,cy x3,y3`
- **Multiple arrows between same layers**: stagger Y-coordinates by 15-20px to avoid overlap

#### Arrow Overlap Prevention
```svg
<!-- Bad: diagonal arrow crosses component -->
<path d="M 200,100 L 600,400"/>

<!-- Good: orthogonal routing around component -->
<path d="M 200,100 L 200,250 L 600,250 L 600,400"/>

<!-- Good: curved with safe control point -->
<path d="M 200,100 Q 400,200 600,400"/>
<!-- Control point (400,200) is 50px+ away from any component -->
```

### 3. Arrow Label Placement
- **Position**: midpoint of arrow path, offset by 5-10px perpendicular to arrow direction
- **Offset first**: move the label 5-10px perpendicular to the arrow so it does not sit on the stroke
- **Background rect fallback**: include only when offsetting cannot avoid another visual element, with:
  - Padding: 4px horizontal, 2px vertical
  - Fill: match background color
  - Opacity: 0.9-0.95
- **Safety distance**: 15px minimum from any component edge
- **Multiple converging arrows**: stagger label positions vertically by 20px

### 4. Component Overlap Detection
Before finalizing SVG, check:
- No component bounding boxes overlap (8px safety margin)
- No arrow paths pass through component interiors (except intentional tunneling with dashed style)
- No text labels overlap with components or other labels

### 5. Z-Index Layering (SVG render order)
```svg
<!-- Render order (top to bottom = back to front): -->
1. Background rect
2. Grouping containers (dashed rects)
3. Arrow paths
4. Arrow label background rects when collision fallback is needed
5. Components (boxes, cylinders, etc.)
6. Component text
7. Arrow label text
8. Legend
```

## Style-Specific Enhancements

### Style-1: Flat Icon Clean
- **Perfect alignment**: snap all coordinates to 8px grid
- **Sharp corners**: rx="8" ry="8" for rounded rects (consistent)
- **Arrows**: thin (1.5-2px), filled polygon markers
- **No shadows**: flat design principle

### Style-6: Claude Official Warm
- **Soft shadows**: `<feDropShadow dx="0" dy="2" stdDeviation="6" flood-color="#00000008"/>`
- **Rounded corners**: rx="12" ry="12" (more rounded than Style-1)
- **Arrows**: medium weight (2px), subtle markers

## Validation Checklist

Before exporting PNG, verify:
- [ ] No arrow-component overlaps (visual inspection)
- [ ] Arrow labels are offset from lines; fallback background rects are used only where needed
- [ ] Minimum 60px clearance for all arrow paths
- [ ] Component spacing ≥ 80px
- [ ] Arrow connection points avoid corners (≥20px from corner)
- [ ] Multiple arrows between layers are staggered
- [ ] Legend is readable and doesn't overlap content
- [ ] SVG renders cleanly via `cairosvg` (or `rsvg-convert` as fallback)

## Common Anti-Patterns to Avoid

| Anti-Pattern | Fix |
|--------------|-----|
| Arrow crosses component | Use orthogonal routing, increase control point distance |
| Label overlaps component | Increase offset; add a matching-background rect if the collision remains |
| Components too close | Increase spacing to 80px minimum |
| Arrow connects to corner | Move connection point to edge midpoint offset |
| No z-index planning | Follow render order: arrows -> components -> text |
references/style-diagram-matrix.md
# Style-to-Diagram-Type Adaptation Guide

Not all styles work equally well for every diagram type. Use this guide to pick the best style.

## Engineering-first styles (9–12)

Styles 9–12 pair a visual language with an executable semantic contract. They
are deliberately specialized rather than universal skins.

| Style | Required evidence | Visual fingerprint | Prompt cues | Fallback | Never blend with |
|---|---|---|---|---|---|
| 9 C4 Review Canvas | One declared C4 level, responsibilities, technology, labeled protocols | Warm review board, C4 type headers, dashed review stamp, deterministic pencil echo | `C4 review board`, `C4 评审画布`, `ADR 评审图` | Styles 1–7 generic architecture | Region/VPC bands, event stations, live metric chips |
| 10 Cloud Fabric | Region/network/workload ownership and named cross-boundary mechanisms | Cloud grid, nested ownership spines, neutral manifest glyphs, deployment-mode stamp | `deployment topology`, `多区域部署图`, `Region/VPC 归属图` | Styles 1–7 when deployment facts are absent | C4 abstraction labels, station numbering, golden-signal cards |
| 11 Event Transit | Topics, ordered processors, consumer groups, junctions, DLQ/state | Thin metro rails, numbered stations, fixed arrowheads, role-specific terminals | `event metro map`, `事件地铁图`, `Kafka 拓扑图` | Generic flow style when stream evidence is absent | Cloud nesting, C4 cards, SRE dashboards |
| 12 Ops Pulse | Fixed window, four golden signals, statuses, one critical path and trace | Live stamp, status rails, metric windows, numbered hops, trace ruler | `reliability pulse`, `事故排查视图`, `黄金信号追踪图` | Generic architecture when measured evidence is absent | Deployment ownership, event rail metaphor, C4 review marks |

All four official fixtures use the `showcase` composition contract: at least
40px node spacing, 20px container gutter, zero bridge crossings, at most two
bends per edge, and no semantic edge duplicated for visual effects.

## Architecture Diagram
| Style | Suitability | Notes |
|-------|----------|
| 1 Flat Icon | Excellent | Default choice; colorful node fills, clear layering |
| 2 Dark Terminal | Excellent | Popular for dev blogs; use colored borders on dark bg |
| 3 Blueprint | Excellent | Perfect for formal architecture docs |
| 4 Notion Clean | Good | Minimal, works for inline docs |
| 5 Glassmorphism | Good | Striking for presentations and product pages |
| 6 Claude Official | Good | Warm aesthetic, Anthropic-style presentations |
| 7 OpenAI Official | Good | Clean, precise; minimal borders, brand green accents |
| 8 Dark Luxury *(AI-authored)* | Excellent | Premium editorial; gold-on-black layers stand out for architecture docs |

## Class Diagram / ER Diagram
| Style | Suitability | Notes |
|-------|------------|-------|
| 1 Flat Icon | Good | Colored headers per class category |
| 2 Dark Terminal | Good | High contrast for code-like diagrams |
| 3 Blueprint | Excellent | Best for formal UML documentation |
| 4 Notion Clean | Excellent | Clean, minimal; ideal for Notion-embedded diagrams |
| 5 Glassmorphism | Poor | Glass effects distract from structural content |
| 6 Claude Official | Excellent | Warm, readable; good for documentation |
| 7 OpenAI Official | Excellent | Minimal aesthetic matches UML precision |
| 8 Dark Luxury *(AI-authored)* | Fair | Non-standard dark bg for UML; use only for premium editorial contexts |

## Sequence Diagram
| Style | Suitability | Notes |
|-------|------------|-------|
| 1 Flat Icon | Good | Clear lifelines; activation boxes visible |
| 2 Dark Terminal | Good | Good for dev articles; dashed lifelines visible |
| 3 Blueprint | Excellent | Formal, technical documentation |
| 4 Notion Clean | Excellent | Best for Notion-embedded sequence diagrams |
| 5 Glassmorphism | Poor | Glass effects make lifelines hard to read |
| 6 Claude Official | Excellent | Ward contrast |
| 7 OpenAI Official | Excellent | Minimal, precise; ideal for API docs |
| 8 Dark Luxury *(AI-authored)* | Good | Dramatic contrast; dark lifelines suit developer blogs and premium tech docs |

## Flowchart / Process Flow
| Style | Suitability | Notes |
|-------|------------|-------|
| 1 Flat Icon | Excellent | Default; colorful decision diamonds |
| 2 Dark Terminal | Good | Works well for dev workflow diagrams |
| 3 Blueprint | Good | Formal process documentation |
| 4 Notion Clean | Good | Clean for SOPs and inline docs |
| 5 Glassmorphism | Good | Striking for product demos |
| 6 Claude Official | Good | Warm aesthetic for presentations |
| 7 OpenAI Official | Good | Clean and minimal |
| 8 Dark Luxury *(AI-authored)* | Good | Striking for premium process documentation |

## Mind Map / Concept Map
| Style | Suitability | Notes |
|-------|------------|-------|
| 1 Flat Icon | Excellent branches, engaging |
| 2 Dark Terminal | Good | Neon-like branches on dark bg |
| 3 Blueprint | Poor | Blueprint grid conflicts with radial layout |
| 4 Notion Clean | Excellent | Ideal for n brainstorming |
| 5 Glassmorphism | Excellent | Stunning visual for presentations |
| 6 Claude Official | Good | Warm, readable |
| 7 OpenAI Official | Good | Clean and minimal |
| 8 Dark Luxury *(AI-authored)* | Excellent | Gold accent branches on black; radial layouts stand out in presentations |

## Data Flow Diagram
| Style | Suitability | Notes |
|-------|------------|-------|
| 1 Flat Icon | Excellent | Color-coded arrows by data type |
| 2 Dark Terminal | Excellent | Glowing data paths on dark bg |
| 3 Blueprint | Excellent | Formal data flow documentation |
| 4 Notion Clean | Good | Minimal, clean |
| 5 Glassmorphism | Poor | Distracts from flow semantics |
| 6 Claude Official | Good | Readable |
| 7 OpenAI Official | Good | Precise, minimal |
| 8 Dark Luxury *(AI-authored)* | Excellent | Color-coded data paths shine against deep black; ideal for data engineering docs |

## Use Case Diagram
| Style | Suitability | Notes |
|-------|------------|-------|
| 1 Flat Icon | Good | Colorful use case ellipses |
| 2 Dark Terminal | Poor | Stick figures less visible on dark bg |
| 3 rint | Excellent | Classic UML aesthetic |
| 4 Notion Clean | Excellent | Perfect for product requirement docs |
| 5 Glassmorphism | Poor | Unnecessary visual noise |
| 6 Claude Official | Excellent | Warm, professional |
| 7 OpenAI Official | Excellent | Clean, precise UML |
| 8 Dark Luxury *(AI-authored)* | Fair | Stick figures less visible on deep black; use cautiously |

## State Machine Diagram
| Style | Suitability | Notes |
|-------|------------|-------|
Flat Icon | Good | Colorful states |
| 2 Dark Terminal | Good | Glowing states and transitions |
| 3 Blueprint | Excellent | Best for formal UML state machines |
| 4 Notion Clean | Excellent | Clean for documentation |
| 5 Glassmorphism | Poor | Distracts from state transitions |
| 6 Claude Official | Excellent | Readable |
| 7 OpenAI Official | Excellent | Minimal, precise |
| 8 Dark Luxury *(AI-authored)* | Good | High contrast for state transitions; editorial quality |

## Network Topology
| Style | Suitabili |
|-------|------------|-------|
| 1 Flat Icon | Excellent | Colorful device icons |
| 2 Dark Terminal | Excellent | Cyberpunk-style network maps |
| 3 Blueprint | Excellent | Ideal for infrastructure docs |
| 4 Notion Clean | Good | Clean for IT documentation |
| 5 Glassmorphism | Good | Striking for presentations |
| 6 Claude Official | Good | Professional network diagrams |
| 7 OpenAI Official | Good | Clean infrastructure diagrams |
| 8 Dark Luxury *(AI-authored)* | Excellent | Deep black classic for infrastructure docs; gold topology lines pop |

## Comparison / Feature Matrix
| Style | Suitability | Notes |
|-------|------------|-------|
| 1 Flat Icon | Excellent | Color-coded checkmarks |
| 2 Dark Terminal | Good | Works for dev tool comparisons |
| 3 Blor | Grid conflicts with table layout |
| 4 Notion Clean | Excellent | Perfect for Notion-embedded tables |
| 5 Glassmorphism | Poor | Distabular data |
| 6 Claude Official | Excellent | Clean, warm |
| 7 OpenAI Official | Excellent | Minimal, precise |
| 8 Dark Luxury *(AI-authored)* | Fair | Dark bg non-standard for comparison tables; use cautiously |

## Timeline / Gantt
| Style | Suitability | Notes |
|-------|------------|-------|
| 1 Flat Icon | Excellent | Colorful bars by category |
| 2 Dark Terminal | Good | Works for dev roadmaps |
| 3 Blueprint | Good | Formal project plans |
| 4 Notion Clean | Excellent | Ideal for Notion project docs |
| 5 Glassmorphism | Good | Striking for keynote presentations |
| 6 Claude Official | Good | Warm, professional |
| 7 OpenAI Official | Good | Clean timeline |
| 8 Dark Luxury *(AI-authored)* | Good | Premium project roadmaps and keynote presentations |

## Agent / Memory Architecture
| Style | Suitability | Notes |
|-------|------------|-------|
| 1 Flat Icon | Excellent | Colorful layers, engaging |
| 2 Dark Terminal | Excellent | Popular for AI/ML blog posts |
| 3 Blueprint | Good | Formal AI system documentation |
| 4 Notion Clean | Good | Clean for AI research notes |
| 5 Glassmorphism | Excellent | Stunning for AI product presentations |
| 6 Claude Official | Excellent | Anthropic AI aesthetic |
| 7 OpenAI Official | Excellent | OpenAI AI aesthetic |
| 8 Dark Luxury *(AI-authored)* | Excellent | Best for premium AI system docs; champagne gold on deep black |
schemas/workflow-v1.schema.json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://yizhiyanhua-ai.github.io/fireworks-tech-graph/schemas/workflow-v1.schema.json",
  "allOf": [
    { "$ref": "diagram-v1.schema.json" },
    { "properties": { "mode": { "enum": ["flowchart", "agent", "state-machine"] } } }
  ]
}
robots.txt
User-agent: *
Allow: /
Sitemap: https://yizhiyanhua-ai.github.io/fireworks-tech-graph/sitemap.xml
schemas/sequence-v1.schema.json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://yizhiyanhua-ai.github.io/fireworks-tech-graph/schemas/sequence-v1.schema.json",
  "allOf": [
    { "$ref": "diagram-v1.schema.json" },
    { "properties": { "mode": { "const": "sequence" } } }
  ]
}
scripts/fireworks.py
#!/usr/bin/env python3
"""Unified command line for rendering, validating, inspecting, and exporting diagrams."""

from __future__ import annotations

import argparse
import importlib.util
import json
import shutil
import sys
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Optional, Sequence


SCRIPT_DIR = Path(__file__).resolve().parent
ROOT = SCRIPT_DIR.parent
if str(SCRIPT_DIR) not in sys.path:
    sys.path.insert(0, str(SCRIPT_DIR))

from diagram_ir import normalize_diagram  # noqa: E402
from interactive_html import build_interactive_html  # noqa: E402
from motion import DEFAULT_MOTION_DURATION, MOTION_PRESETS, probe_motion_runtime, render_motion_gif  # noqa: E402
from validate_svg import run_check  # noqa: E402


def _load_generator():
    spec = importlib.util.spec_from_file_location("fireworks_template_generator", SCRIPT_DIR / "generate-from-template.py")
    if spec is None or spec.loader is None:
        raise RuntimeError("cannot load diagram generator")
    module = importlib.util.module_from_spec(spec)
    sys.modules[spec.name] = module
    spec.loader.exec_module(module)
    return module


def command_doctor(_: argparse.Namespace) -> int:
    motion_runtime = probe_motion_runtime()
    probes = {
        "python": {"ok": sys.version_info >= (3, 9), "value": sys.version.split()[0]},
        "cairosvg": {"ok": importlib.util.find_spec("cairosvg") is not None},
        "rsvg-convert": {"ok": shutil.which("rsvg-convert") is not None},
        "node": {"ok": shutil.which("node") is not None},
        "ffmpeg": {"ok": shutil.which("ffmpeg") is not None},
        "motion_renderer": motion_runtime,
    }
    probes["raster_export"] = {"ok": probes["cairosvg"]["ok"] or probes["rsvg-convert"]["ok"]}
    probes["animation_export"] = {"ok": motion_runtime["ok"], "optional": True}
    print(json.dumps(probes, indent=2, sort_keys=True))
    return 0 if probes["python"]["ok"] and probes["raster_export"]["ok"] else 1


def _read_json(path: Path) -> dict[str, object]:
    return json.loads(path.read_text(encoding="utf-8"))


def command_validate(args: argparse.Namespace) -> int:
    diagram = normalize_diagram(_read_json(args.input), args.mode)
    # Validation covers the same renderer boundary as ``render``. Style 8 is
    # intentionally AI-authored/static, so accepting JSON for it here would
    # promise a render path that does not exist.
    _load_generator().parse_style(diagram.style_index)
    print(json.dumps({
        "ok": True,
        "schema_version": diagram.schema_version,
        "input_schema": diagram.input_schema,
        "mode": diagram.mode,
        "style": {
            "id": diagram.style_index,
            "name": diagram.semantic_report["visual_theme"],
        },
        "semantics": dict(diagram.semantic_report),
        "nodes": len(diagram.nodes),
        "edges": len(diagram.edges),
    }, indent=2, sort_keys=True))
    return 0


def command_render(args: argparse.Namespace) -> int:
    generator = _load_generator()
    data = _read_json(args.input)
    svg, report = generator.build_svg_with_report(args.mode, data)
    args.output.parent.mkdir(parents=True, exist_ok=True)
    args.output.write_text(svg, encoding="utf-8")
    if args.report:
        args.report.parent.mkdir(parents=True, exist_ok=True)
        args.report.write_text(json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
    print(json.dumps({"ok": True, "svg": str(args.output), "report": str(args.report) if args.report else None}, sort_keys=True))
    return 0


def command_check(args: argparse.Namespace) -> int:
    checks = args.check or ["xml", "markers", "geometry", "composition"]
    results: dict[str, object] = {}
    ok = True
    for check in checks:
        passed, details = run_check(args.svg, check)
        results[check] = {"ok": passed, "details": details}
        ok = ok and passed
    print(json.dumps({"ok": ok, "checks": results}, indent=2, sort_keys=True))
    return 0 if ok else 1


def command_inspect(args: argparse.Namespace) -> int:
    root = ET.parse(args.svg).getroot()
    roles: dict[str, int] = {}
    for element in root.iter():
        role = element.get("data-graph-role")
        if role:
            roles[role] = roles.get(role, 0) + 1
    print(json.dumps({
        "generator": root.get("data-generator"),
        "schema_version": root.get("data-schema-version"),
        "style_id": root.get("data-style-id"),
        "visual_theme": root.get("data-visual-theme"),
        "diagram_type": root.get("data-diagram-type"),
        "semantic_profile": root.get("data-semantic-profile"),
        "semantic_valid": root.get("data-semantic-valid"),
        "viewBox": root.get("viewBox"),
        "roles": roles,
    }, indent=2, sort_keys=True))
    return 0


def command_export_html(args: argparse.Namespace) -> int:
    output = build_interactive_html(
        args.svg.read_text(encoding="utf-8"),
        args.title or args.svg.stem,
        {"slug": args.slug or args.svg.stem},
    )
    args.output.parent.mkdir(parents=True, exist_ok=True)
    args.output.write_text(output, encoding="utf-8")
    print(json.dumps({"ok": True, "html": str(args.output)}, sort_keys=True))
    return 0


def command_animate(args: argparse.Namespace) -> int:
    report = args.report or args.output.with_suffix(".motion.json")
    result = render_motion_gif(
        args.input,
        args.output,
        report_path=report,
        preset=args.preset,
        duration=args.duration,
        fps=args.fps,
        width=args.width,
        dry_run=args.dry_run,
    )
    print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
    return 0


def command_examples(_: argparse.Namespace) -> int:
    examples = [str(path.relative_to(ROOT)) for path in sorted((ROOT / "fixtures").glob("*")) if path.suffix in {".json", ".svg"}]
    print(json.dumps({"examples": examples}, indent=2))
    return 0


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description=__doc__)
    subparsers = parser.add_subparsers(dest="command", required=True)
    subparsers.add_parser("doctor").set_defaults(func=command_doctor)

    validate = subparsers.add_parser("validate", help="validate and normalize diagram JSON")
    validate.add_argument("mode")
    validate.add_argument("input", type=Path)
    validate.set_defaults(func=command_validate)

    render = subparsers.add_parser("render", help="render diagram JSON to SVG")
    render.add_argument("mode")
    render.add_argument("input", type=Path)
    render.add_argument("output", type=Path)
    render.add_argument("--report", type=Path)
    render.set_defaults(func=command_render)

    check = subparsers.add_parser("check", help="check an SVG artifact")
    check.add_argument("svg", type=Path)
    check.add_argument("--check", action="append", choices=("xml", "markers", "collisions", "geometry", "composition"))
    check.set_defaults(func=command_check)

    inspect = subparsers.add_parser("inspect", help="print semantic SVG metadata")
    inspect.add_argument("svg", type=Path)
    inspect.set_defaults(func=command_inspect)

    export = subparsers.add_parser("export-html", help="create an offline interactive HTML viewer")
    export.add_argument("svg", type=Path)
    export.add_argument("output", type=Path)
    export.add_argument("--title")
    export.add_argument("--slug")
    export.set_defaults(func=command_export_html)

    animate = subparsers.add_parser(
        "animate",
        help="create a validated animated GIF from a generated semantic SVG",
    )
    animate.add_argument("input", type=Path)
    animate.add_argument("output", type=Path)
    animate.add_argument("--preset", choices=("auto", *MOTION_PRESETS), default="auto")
    animate.add_argument("--duration", type=float, default=DEFAULT_MOTION_DURATION)
    animate.add_argument("--fps", type=int, default=20)
    animate.add_argument("--width", type=int, default=960)
    animate.add_argument("--report", type=Path)
    animate.add_argument("--dry-run", action="store_true")
    animate.set_defaults(func=command_animate)

    subparsers.add_parser("examples").set_defaults(func=command_examples)
    return parser


def main(argv: Optional[Sequence[str]] = None) -> int:
    args = build_parser().parse_args(argv)
    try:
        return int(args.func(args))
    except (OSError, RuntimeError, ValueError, json.JSONDecodeError, ET.ParseError) as error:
        print(json.dumps({"ok": False, "error": str(error)}, ensure_ascii=False), file=sys.stderr)
        return 1


if __name__ == "__main__":
    raise SystemExit(main())
scripts/composition_quality.py
#!/usr/bin/env python3
"""Shared composition-quality contract for generated and authored diagrams.

Geometry safety answers whether a diagram is technically valid.  This module
adds the stricter presentation rules used by the official showcase fixtures:
short orthogonal routes, deliberate whitespace, clear labels, and zero bridge
crossings whenever the topology can be composed without them.
"""

from __future__ import annotations

import math
from dataclasses import dataclass
from typing import Any, Mapping, Optional, Sequence


Point = tuple[float, float]
Bounds = tuple[float, float, float, float]


@dataclass(frozen=True)
class CompositionContract:
    profile: str
    max_bends_per_edge: int
    max_total_bends: int
    max_route_stretch: float
    max_bridged_crossings: int
    min_node_gap: float
    min_container_gutter: float
    min_label_clearance: float
    min_segment_length: float

    def as_dict(self) -> dict[str, Any]:
        return {
            "profile": self.profile,
            "max_bends_per_edge": self.max_bends_per_edge,
            "max_total_bends": self.max_total_bends,
            "max_route_stretch": self.max_route_stretch,
            "max_bridged_crossings": self.max_bridged_crossings,
            "min_node_gap": self.min_node_gap,
            "min_container_gutter": self.min_container_gutter,
            "min_label_clearance": self.min_label_clearance,
            "min_segment_length": self.min_segment_length,
        }


PROFILES: dict[str, CompositionContract] = {
    "standard": CompositionContract(
        profile="standard",
        max_bends_per_edge=12,
        max_total_bends=100,
        max_route_stretch=5.0,
        max_bridged_crossings=8,
        min_node_gap=0.0,
        min_container_gutter=0.0,
        min_label_clearance=2.0,
        min_segment_length=0.0,
    ),
    "showcase": CompositionContract(
        profile="showcase",
        max_bends_per_edge=2,
        max_total_bends=8,
        max_route_stretch=1.35,
        max_bridged_crossings=0,
        min_node_gap=40.0,
        min_container_gutter=20.0,
        min_label_clearance=4.0,
        min_segment_length=16.0,
    ),
}


def _number(value: Any, fallback: float) -> float:
    try:
        number = float(value)
    except (TypeError, ValueError):
        return fallback
    return number if math.isfinite(number) else fallback


def resolve_contract(raw: Any = None) -> CompositionContract:
    """Resolve a profile name or a mapping with profile overrides."""

    if isinstance(raw, Mapping):
        profile = str(raw.get("profile", "standard")).strip().lower()
        overrides = raw
    else:
        profile = str(raw or "standard").strip().lower()
        overrides = {}
    if profile not in PROFILES:
        raise ValueError(f"unsupported composition profile: {profile}")
    base = PROFILES[profile]
    values = base.as_dict()
    for key in values:
        if key == "profile" or key not in overrides:
            continue
        values[key] = _number(overrides[key], float(values[key]))
    for key in ("max_bends_per_edge", "max_total_bends", "max_bridged_crossings"):
        values[key] = int(values[key])
    if any(float(values[key]) < 0 for key in values if key != "profile"):
        raise ValueError("composition quality limits must be non-negative")
    return CompositionContract(**values)


def rectangle_gap(first: Bounds, second: Bounds) -> float:
    """Return the Euclidean whitespace between two axis-aligned rectangles."""

    horizontal = max(first[0] - second[2], second[0] - first[2], 0.0)
    vertical = max(first[1] - second[3], second[1] - first[3], 0.0)
    return math.hypot(horizontal, vertical)


def bounds_area(bounds: Bounds) -> float:
    return max(0.0, bounds[2] - bounds[0]) * max(0.0, bounds[3] - bounds[1])


def containing_container(node: Bounds, containers: Sequence[tuple[str, Bounds]]) -> Optional[tuple[str, Bounds]]:
    center = ((node[0] + node[2]) / 2, (node[1] + node[3]) / 2)
    matches = [
        item
        for item in containers
        if item[1][0] <= center[0] <= item[1][2] and item[1][1] <= center[1] <= item[1][3]
    ]
    return min(matches, key=lambda item: bounds_area(item[1])) if matches else None


def container_gutter(node: Bounds, container: Bounds) -> float:
    return min(
        node[0] - container[0],
        node[1] - container[1],
        container[2] - node[2],
        container[3] - node[3],
    )


def route_stretch(points: Sequence[Point]) -> float:
    if len(points) < 2:
        return 1.0
    length = sum(abs(x2 - x1) + abs(y2 - y1) for (x1, y1), (x2, y2) in zip(points, points[1:]))
    direct = abs(points[-1][0] - points[0][0]) + abs(points[-1][1] - points[0][1])
    return 1.0 if direct <= 1e-9 else length / direct


def segment_lengths(points: Sequence[Point]) -> list[float]:
    return [abs(x2 - x1) + abs(y2 - y1) for (x1, y1), (x2, y2) in zip(points, points[1:])]


def assess_composition(
    *,
    nodes: Sequence[tuple[str, Bounds]],
    containers: Sequence[tuple[str, Bounds]],
    edges: Sequence[Mapping[str, Any]],
    contract: CompositionContract,
) -> dict[str, Any]:
    """Return deterministic showcase metrics and actionable violations."""

    violations: list[dict[str, Any]] = []
    total_bends = 0
    total_bridges = 0
    max_stretch = 1.0
    shortest_segment: Optional[float] = None

    for edge in edges:
        edge_id = str(edge.get("id", "edge"))
        points = [tuple(map(float, point)) for point in edge.get("route", [])]
        bends = int(edge.get("bends", max(0, len(points) - 2)))
        bridges = len(edge.get("bridges", []))
        stretch = route_stretch(points)
        lengths = [length for length in segment_lengths(points) if length > 1e-6]
        local_shortest = min(lengths) if lengths else None
        total_bends += bends
        total_bridges += bridges
        max_stretch = max(max_stretch, stretch)
        if local_shortest is not None:
            shortest_segment = local_shortest if shortest_segment is None else min(shortest_segment, local_shortest)
        if bends > contract.max_bends_per_edge:
            violations.append({
                "code": "EDGE_BEND_BUDGET",
                "element": edge_id,
                "actual": bends,
                "limit": contract.max_bends_per_edge,
            })
        if stretch > contract.max_route_stretch + 1e-6:
            violations.append({
                "code": "EDGE_ROUTE_STRETCH",
                "element": edge_id,
                "actual": round(stretch, 3),
                "limit": contract.max_route_stretch,
            })
        if local_shortest is not None and local_shortest + 1e-6 < contract.min_segment_length:
            violations.append({
                "code": "EDGE_MICRO_SEGMENT",
                "element": edge_id,
                "actual": round(local_shortest, 2),
                "limit": contract.min_segment_length,
            })

    if total_bends > contract.max_total_bends:
        violations.append({
            "code": "TOTAL_BEND_BUDGET",
            "element": "diagram",
            "actual": total_bends,
            "limit": contract.max_total_bends,
        })
    if total_bridges > contract.max_bridged_crossings:
        violations.append({
            "code": "BRIDGE_BUDGET",
            "element": "diagram",
            "actual": total_bridges,
            "limit": contract.max_bridged_crossings,
        })

    minimum_gap: Optional[float] = None
    for index, (first_id, first) in enumerate(nodes):
        for second_id, second in nodes[index + 1 :]:
            gap = rectangle_gap(first, second)
            minimum_gap = gap if minimum_gap is None else min(minimum_gap, gap)
            if gap + 1e-6 < contract.min_node_gap:
                violations.append({
                    "code": "NODE_GAP",
                    "element": f"{first_id},{second_id}",
                    "actual": round(gap, 2),
                    "limit": contract.min_node_gap,
                })

    minimum_gutter: Optional[float] = None
    for node_id, node in nodes:
        match = containing_container(node, containers)
        if not match:
            continue
        container_id, container = match
        gutter = container_gutter(node, container)
        minimum_gutter = gutter if minimum_gutter is None else min(minimum_gutter, gutter)
        if gutter + 1e-6 < contract.min_container_gutter:
            violations.append({
                "code": "CONTAINER_GUTTER",
                "element": f"{node_id}@{container_id}",
                "actual": round(gutter, 2),
                "limit": contract.min_container_gutter,
            })

    penalty = (
        len(violations) * 12
        + total_bridges * 8
        + max(0, total_bends - len(edges)) * 2
    )
    return {
        "profile": contract.profile,
        "ok": not violations,
        "score": max(0, 100 - penalty),
        "metrics": {
            "total_bends": total_bends,
            "bridged_crossings": total_bridges,
            "max_route_stretch": round(max_stretch, 3),
            "minimum_node_gap": round(minimum_gap, 2) if minimum_gap is not None else None,
            "minimum_container_gutter": round(minimum_gutter, 2) if minimum_gutter is not None else None,
            "shortest_segment": round(shortest_segment, 2) if shortest_segment is not None else None,
        },
        "limits": contract.as_dict(),
        "violations": violations,
    }


__all__ = [
    "Bounds",
    "CompositionContract",
    "PROFILES",
    "assess_composition",
    "resolve_contract",
    "route_stretch",
]
schemas/diagram-v1.schema.json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://yizhiyanhua-ai.github.io/fireworks-tech-graph/schemas/diagram-v1.schema.json",
  "title": "Fireworks Tech Graph Diagram v1",
  "type": "object",
  "required": ["schema_version", "mode", "nodes", "arrows"],
  "properties": {
    "schema_version": { "const": 1 },
    "mode": { "type": "string", "minLength": 1 },
    "style": {
      "oneOf": [
        { "type": "integer", "minimum": 1, "maximum": 12 },
        { "type": "string", "minLength": 1 }
      ]
    },
    "visual_theme": {
      "oneOf": [
        { "type": "integer", "minimum": 1, "maximum": 12 },
        { "type": "string", "minLength": 1 }
      ]
    },
    "diagram_type": { "type": "string", "minLength": 1 },
    "semantic_profile": {
      "enum": ["generic", "c4-review", "cloud-fabric", "event-transit", "ops-pulse"]
    },
    "c4_level": { "enum": ["context", "container", "component"] },
    "review_state": { "type": "string", "minLength": 1 },
    "rough_seed": { "type": "integer" },
    "scope": { "type": "string", "minLength": 1 },
    "platform_profile": { "enum": ["provider-neutral", "aws", "azure", "gcp", "kubernetes"] },
    "deployment_mode": { "type": "string", "minLength": 1 },
    "icon_manifest_version": { "type": "string", "minLength": 1 },
    "line_code": { "type": "string", "minLength": 1 },
    "observation_window": { "type": "string", "minLength": 1 },
    "critical_path": {
      "type": "array",
      "minItems": 1,
      "uniqueItems": true,
      "items": { "type": "string", "minLength": 1 }
    },
    "topics": {
      "type": "array",
      "minItems": 1,
      "maxItems": 4,
      "items": {
        "type": "object",
        "required": ["id", "color"],
        "properties": {
          "id": { "type": "string", "minLength": 1 },
          "color": { "type": "string", "minLength": 1 }
        },
        "additionalProperties": true
      }
    },
    "quality_profile": { "enum": ["standard", "showcase"] },
    "composition": {
      "type": "object",
      "properties": {
        "profile": { "enum": ["standard", "showcase"] },
        "max_bends_per_edge": { "type": "integer", "minimum": 0 },
        "max_total_bends": { "type": "integer", "minimum": 0 },
        "max_route_stretch": { "type": "number", "minimum": 0 },
        "max_bridged_crossings": { "type": "integer", "minimum": 0 },
        "min_node_gap": { "type": "number", "minimum": 0 },
        "min_container_gutter": { "type": "number", "minimum": 0 },
        "min_label_clearance": { "type": "number", "minimum": 0 },
        "min_segment_length": { "type": "number", "minimum": 0 }
      },
      "additionalProperties": false
    },
    "legend_orientation": { "enum": ["vertical", "horizontal"] },
    "width": { "type": "number", "exclusiveMinimum": 0 },
    "height": { "type": "number", "exclusiveMinimum": 0 },
    "containers": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["id", "x", "y", "width", "height"],
        "properties": {
          "id": { "type": "string", "minLength": 1 },
          "parent": { "type": "string", "minLength": 1 },
          "deployment_kind": { "type": "string", "minLength": 1 },
          "x": { "type": "number" },
          "y": { "type": "number" },
          "width": { "type": "number", "exclusiveMinimum": 0 },
          "height": { "type": "number", "exclusiveMinimum": 0 }
        },
        "additionalProperties": true
      }
    },
    "nodes": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["id"],
        "properties": {
          "id": { "type": "string", "minLength": 1 },
          "parent": { "type": "string", "minLength": 1 },
          "c4_type": { "enum": ["person", "software_system", "external_system", "container", "component"] },
          "description": { "type": "string" },
          "technology": { "type": "string" },
          "deployment_id": { "type": "string", "minLength": 1 },
          "deployment_kind": { "type": "string", "minLength": 1 },
          "icon_id": { "type": "string", "minLength": 1 },
          "transit_role": { "enum": ["producer", "station", "junction", "consumer", "dlq", "state_store"] },
          "topic_id": { "type": "string", "minLength": 1 },
          "station_order": { "type": "integer", "minimum": 0 },
          "consumer_group": { "type": "string", "minLength": 1 },
          "ops_role": { "enum": ["service", "collector", "trace_span"] },
          "span_id": { "type": "string", "minLength": 1 },
          "parent_span": { "type": "string", "minLength": 1 },
          "start_ms": { "type": "number", "minimum": 0 },
          "duration_ms": { "type": "number", "exclusiveMinimum": 0 },
          "signals": {
            "type": "object",
            "required": ["latency", "traffic", "errors", "saturation"],
            "properties": {
              "latency": { "$ref": "#/$defs/goldenSignal" },
              "traffic": { "$ref": "#/$defs/goldenSignal" },
              "errors": { "$ref": "#/$defs/goldenSignal" },
              "saturation": { "$ref": "#/$defs/goldenSignal" }
            },
            "additionalProperties": false
          },
          "x": { "type": "number" },
          "y": { "type": "number" },
          "width": { "type": "number", "exclusiveMinimum": 0 },
          "height": { "type": "number", "exclusiveMinimum": 0 }
        },
        "additionalProperties": true
      }
    },
    "arrows": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "minLength": 1 },
          "source": { "type": "string" },
          "target": { "type": "string" },
          "edge_kind": { "enum": ["business", "telemetry"] },
          "transit_type": { "enum": ["rail", "publish", "branch", "consume", "retry", "state", "dead_letter"] },
          "topic_id": { "type": "string", "minLength": 1 },
          "protocol": { "type": "string", "minLength": 1 },
          "via": { "type": "string", "minLength": 1 },
          "route_points": {
            "type": "array",
            "items": {
              "type": "array",
              "prefixItems": [{ "type": "number" }, { "type": "number" }],
              "items": false,
              "minItems": 2,
              "maxItems": 2
            }
          }
        },
        "additionalProperties": true
      }
    }
  },
  "$defs": {
    "goldenSignal": {
      "type": "object",
      "required": ["value", "unit", "window", "status"],
      "properties": {
        "value": { "type": ["string", "number"] },
        "unit": { "type": "string", "minLength": 1 },
        "window": { "type": "string", "minLength": 1 },
        "status": { "enum": ["ok", "warn", "critical", "unknown"] }
      },
      "additionalProperties": false
    }
  },
  "additionalProperties": true
}
scripts/README.md
# Fireworks Tech Graph - Scripts

辅助脚本集合,用于提高 SVG 图表生成的稳定性和效率。

## 脚本列表

### 1. validate-svg.sh

SVG 验证脚本,检查 SVG 语法并报告详细错误。

**用法:**
```bash
./validate-svg.sh <svg-file>
```

**检查项目:**
- XML 结构、属性语法和实体转义(使用 XML parser,避免把 `top_k=5` 之类的文本误判为属性)
- `marker-start` / `marker-mid` / `marker-end` 引用完整性
- 箭头与组件碰撞(支持绝对/相对 `M/L/H/V/Q/C/S/T` 路径,曲线路径采样检测)
- 渲染验证(cairosvg 优先,rsvg-convert 兜底)

**示例:**
```bash
./validate-svg.sh /path/to/diagram.svg
```

### 2. generate-diagram.sh

SVG 图表生成脚本,提供自动验证和 PNG 导出。

**用法:**
```bash
./generate-diagram.sh [OPTIONS]
```

**选项:**
- `-t, --type TYPE` - 图表类型(见脚本帮助)
- `-s, --style STYLE` - 风格编号(1-12,默认:1)
- `-o, --output PATH` - 输出路径(默认:当前目录)
- `-w, --width WIDTH` - PNG 宽度(像素,默认:1920)
- `--no-validate` - 跳过验证
- `-h, --help` - 显示帮助

**示例:**
```bash
# 生成架构图(Style 1)
./generate-diagram.sh -t architecture -s 1 -o ./output/arch.svg

# 生成流程图(Style 2,2400px 宽)
./generate-diagram.sh -t flowchart -s 2 -w 2400
```

**注意:** SVG 内容需要先准备好;这个脚本只负责验证与导出。

### 3. generate-from-template.py

基于风格配置和 JSON 数据生成 SVG。当前版本不再只是简单塞入 `nodes/arrows`,
而是会执行 style guide 中的部分可计算规则,例如:

- `style` - 风格编号(1-12)或规范风格名
- `semantic_profile` - 可选语义契约;Style 9-12 默认分别启用 C4、云部署、事件流和可观测性契约
- `containers` - 泳道 / 分组容器
- `containers[].header_prefix` / `containers[].header_text` - 工程编号式分区标题
- `containers[].side_label` - 左侧 layer label
- `nodes[].kind` - 语义组件类型,例如 `double_rect`、`cylinder`、`document`、`terminal`、`circle_cluster`
- `arrows[].flow` - 语义箭头类型,例如 `control`、`write`、`read`、`data`
- `source_port` / `target_port` - 指定端口锚点
- `route_points` / `corridor_x` / `corridor_y` - 控制复杂图的走线质量
- `style_overrides` - 对现有 style 做局部覆盖
- `window_controls` / `meta_*` - 顶部终端 chrome
- `blueprint_title_block` - 工程蓝图右下角 title block

**用法:**
```bash
python3 ./generate-from-template.py architecture ./output/arch.svg '{"style":1,"title":"My Diagram","containers":[],"nodes":[],"arrows":[]}'
```

**示例:**
```bash
python3 ./generate-from-template.py memory ./output/mem0.svg '{
  "style": 1,
  "title": "Mem0 Memory Architecture",
  "containers": [
    {"x":30,"y":90,"width":900,"height":90,"label":"Input Layer","header_prefix":"01"}
  ],
  "nodes": [
    {"id":"manager","kind":"double_rect","x":360,"y":220,"width":300,"height":72,"label":"Memory Manager"},
    {"id":"vector","kind":"cylinder","x":90,"y":360,"width":140,"height":110,"label":"Vector Store"}
  ],
  "arrows": [
    {"source":"manager","target":"vector","flow":"write","dashed":true}
  ]
}'
```

### 4. test-all-styles.sh

批量测试脚本覆盖 12 种风格。Style 1-7 与 9-12 从 JSON fixture 生成,AI 手绘的 Style 8 使用静态 SVG fixture;任何风格缺少回归 fixture 都会使批测失败。

**用法:**
```bash
./test-all-styles.sh
```

**功能:**
- 检查所有风格的参考文件
- 渲染 `fixtures/*.json` 回归样例
- 验证生成出的 SVG 文件
- 导出 PNG 文件到 `test-output/` 目录
- 生成测试报告

**输出:**
- 测试摘要(通过/失败统计)
- PNG 文件(带时间戳)
- 详细的验证错误信息

**示例:**
```bash
./test-all-styles.sh
```

## 依赖

所有脚本需要至少一个 PNG 渲染器(推荐 cairosvg):

- **cairosvg**(推荐)- SVG 转 PNG,CSS 支持最好
  ```bash
  python3 -m pip install cairosvg
  ```

- **rsvg-convert**(备选)- 系统包;复杂 SVG 可能丢失 CSS / `<foreignObject>`
  ```bash
  brew install librsvg                # macOS
  sudo apt install librsvg2-bin       # Ubuntu/Debian
  ```

`generate-diagram.sh` 会优先调用 cairosvg,缺失时自动回退到 rsvg-convert。完整对比见 [PNG 导出参考](../references/png-export.md)。

聚焦后的语义动效由 `fireworks.py animate`、`motion.py` 和 `svg2gif.js` 协作完成,只接收带有 12 套已验收 role/stage/order 契约之一的生成器语义 SVG。精确源文件字节不锁定,但任意同风格拓扑不会自动套用动效。媒体输出只允许经过验证的 GIF,默认还会生成同名 `.motion.json` 报告。运行时需要 FFmpeg/FFprobe、Chrome/Chromium,以及 Skill 安装位置可解析到的 `puppeteer` 或 `puppeteer-core`;当前工作目录中的同名模块不会被隐式执行。依赖安装与最简命令:

```bash
for SKILL_ROOT in \
  "$HOME/.agents/skills/fireworks-tech-graph" \
  "$HOME/.claude/skills/fireworks-tech-graph"
do
  [ -d "$SKILL_ROOT" ] || continue
  npm install --prefix "$SKILL_ROOT" --ignore-scripts --no-save --package-lock=false puppeteer-core@25.3.0
done
SKILL_ROOT="${CLAUDE_SKILL_DIR:-$HOME/.agents/skills/fireworks-tech-graph}"
python3 "$SKILL_ROOT/scripts/fireworks.py" animate diagram.svg diagram.gif
```

默认自动识别风格,输出 5.75 秒、20fps、960px 宽、115 帧无限循环 GIF。第 1–36 帧保持既有 draw-on,第 36–38 帧淡入运行流,第 38–109 帧为完整稳定数据流,第 110–114 帧按 `[1,.7575,.515,.2725,.03]` reset。Style 1–12 的 signature、速度、路径、几何和构建合同均为 `user-approved`,包括 `persistent-data-flow-head`、`terminal-evidence-stream`、`blueprint-registration-bead`、14×10 `notion-memory-card`、`glass-task-capsule`、`policy-seal`、`token-train`、`gem-tracer`、`review-cursor`、region chevrons、event train 与 ops scanner;共享 `+2s-settled-flow` 时间修订也已于 2026-07-17 验收,默认新包的 `review_status` 为 `user-approved`。显式 3.75 秒/75 帧和 2.75 秒/55 帧继续支持。75 帧及以下要求全部 raster 唯一;更长时间线允许非相邻重复出现在 full-opacity 区间,frame 110 是 reset opacity 为 1.00 的唯一例外并分类为 `intentional_reset_boundary_repeat`,frame 111–114 必须全局不同;至少保留 75 个唯一 raster 且相邻重复数为零。75-vs-115 gate 分开统计 binary / decoded-RGBA / guarded-antialias 三类;guarded 等价要求 AE ≤ 128、normalized RMSE ≤ 0.001、component 宽或高不超过 2px 且只落在 edge/node border,DOM 和 signature geometry 仍 strict-exact。完整约束见 [动效参考](../references/motion-effects.md)。

- **grep, sed, awk** - 文本处理(macOS 自带)

## 目录结构

```
fireworks-tech-graph/
├── SKILL.md                    # Skill 主文档
├── references/                 # 风格参考文件
│   ├── style-1-flat-icon.md
│   ├── style-2-dark-terminal.md
│   └── ...
├── fixtures/                   # 回归测试样例(JSON)
│   ├── mem0-style1.json
│   ├── tool-call-style2.json
│   └── ...
├── scripts/                    # 辅助脚本(本目录)
│   ├── README.md              # 本文档
│   ├── validate-svg.sh        # SVG 验证
│   ├── generate-diagram.sh    # SVG 验证与 PNG 导出
│   ├── generate-from-template.py # 模板化生成 SVG
│   ├── motion.py              # SVG 转 GIF 校验、编码与原子报告
│   ├── svg2gif.js             # Chromium 手动时间轴逐帧渲染
│   └── test-all-styles.sh     # 批量测试
└── test-output/               # 测试输出目录(自动创建)
```

## 使用场景

### 场景 1:验证现有 SVG

```bash
SKILL_ROOT=~/.agents/skills/fireworks-tech-graph # Codex
# SKILL_ROOT=~/.claude/skills/fireworks-tech-graph # Claude Code
"$SKILL_ROOT/scripts/validate-svg.sh" /path/to/your-diagram.svg
```

### 场景 2:生成并验证图表

1. 使用 Codex 或 Claude Code 生成 SVG 内容
2. 运行验证和导出:
   ```bash
   SKILL_ROOT=~/.agents/skills/fireworks-tech-graph # Codex
   # SKILL_ROOT=~/.claude/skills/fireworks-tech-graph # Claude Code
   "$SKILL_ROOT/scripts/generate-diagram.sh" -t architecture -s 1 -o ./output/arch.svg
   ```

### 场景 3:批量测试所有风格

```bash
SKILL_ROOT=~/.agents/skills/fireworks-tech-graph # Codex
# SKILL_ROOT=~/.claude/skills/fireworks-tech-graph # Claude Code
"$SKILL_ROOT/scripts/test-all-styles.sh"
```

测试脚本会自动:
1. 读取 `../fixtures/*.json`
2. 按 `template_type + style` 调用 `generate-from-template.py`
3. 运行 `validate-svg.sh`
4. 导出 PNG 到 `../test-output/`

### 场景 4:validator 单元测试

```bash
python3 -m unittest discover -s tests -p 'test_validate_svg.py' -v
```

覆盖 marker 双端引用、文本等号、`H/V` 路径、曲线路径、虚线组件和容器排除等正反例。

对启发式难以区分的形状,可显式添加 `data-graph-role="node|container|legend|decoration|label|background"`。`node` 会强制纳入障碍物检测,其余角色会从组件障碍物中排除;`legend` 组内的示例箭头也不会被当作业务流。

查看测试输出:
```bash
ls -lh ../test-output/
```

## 故障排除

### 问题:找不到 PNG 渲染器

**解决方案(任选其一,推荐 cairosvg):**
```bash
python3 -m pip install cairosvg        # 推荐
brew install librsvg                   # macOS 系统包
sudo apt install librsvg2-bin          # Ubuntu/Debian
```

### 问题:rsvg-convert 渲染缺框/缺文字

**原因:** rsvg-convert 对 `<foreignObject>`、CSS `filter`、复杂 `<style>` 块支持有限。

**解决方案:** 切换到 cairosvg:
```bash
python3 -m pip install cairosvg
```
脚本会自动优先使用 cairosvg。如果仍需要像素级还原(例如浏览器生成的 SVG),按 [PNG 导出参考](../references/png-export.md) 使用 `svg2png.js`。

### 问题:权限被拒绝

**解决方案:**
```bash
chmod +x *.sh
```

### 问题:SVG 验证失败

**解决方案:**
1. 查看详细错误信息
2. 使用 Edit 工具修复语法错误
3. 重新运行验证

## 开发说明

### 添加新的验证规则

编辑 `validate-svg.sh`,在现有检查项后添加新的检查逻辑:

```bash
# Check N: Your new check
echo -n "Checking something... "
# Your validation logic here
if [ condition ]; then
    echo -e "${GREEN}✓ Pass${NC}"
else
    echo -e "${RED}✗ Fail${NC}"
fi
```

### 扩展支持的图表类型

编辑 `generate-diagram.sh`,在 `--type` 参数处理中添加新类型。

## 版本历史

- **v1.2.0** (2026-07-17) - 语义动效与动态展示
  - 12 种已验收的 SVG→GIF 场景动效与 5.75 秒 settled-flow 默认时间线
  - README 全动态图集、GIF manifest、媒体回读与安装副本全风格门禁
  - 动效源 SVG 保持语义契约约束,不绑定标题和内容字节
- **v1.1.0** (2026-07-15) - 几何与分发升级
  - Schema v1 与类型化 Diagram IR
  - 正交路由、端口分流、图例/标签避让、跨线桥与确定性布局报告
  - `fireworks.py` 统一 CLI 与离线交互 HTML 导出
  - 完整 npx Skill 镜像、CI、Release archive parity 与安装 canary
- **v1.0.0** (2026-04-11) - 初始版本
  - SVG 验证脚本
  - 图表生成脚本
  - 批量测试脚本

## 许可证

MIT License - 与 fireworks-tech-graph skill 相同
scripts/fireworks_geometry.py
#!/usr/bin/env python3
"""Deterministic geometry primitives shared by the renderer and SVG checker.

The module deliberately uses only the Python standard library.  Generated
diagrams can therefore enforce the same routing contract in a fresh Agent
Skill install, in CI, and in the post-render artifact checker.
"""

from __future__ import annotations

import math
import unicodedata
from dataclasses import dataclass
from typing import Iterable, Optional, Sequence


Point = tuple[float, float]
Bounds = tuple[float, float, float, float]
EPSILON = 1e-6


@dataclass(frozen=True)
class SegmentInteraction:
    kind: str
    point: Optional[Point] = None
    overlap_length: float = 0.0


@dataclass(frozen=True)
class RouteInteractions:
    crossings: tuple[Point, ...]
    overlap_count: int
    overlap_length: float


def almost_equal(first: float, second: float, epsilon: float = EPSILON) -> bool:
    return abs(first - second) <= epsilon


def same_point(first: Point, second: Point, epsilon: float = EPSILON) -> bool:
    return almost_equal(first[0], second[0], epsilon) and almost_equal(first[1], second[1], epsilon)


def segment_axis(first: Point, second: Point) -> str:
    if almost_equal(first[1], second[1]):
        return "horizontal"
    if almost_equal(first[0], second[0]):
        return "vertical"
    return "other"


def route_is_orthogonal(points: Sequence[Point]) -> bool:
    return all(segment_axis(first, second) != "other" for first, second in zip(points, points[1:]))


def route_length(points: Sequence[Point]) -> float:
    return sum(math.hypot(second[0] - first[0], second[1] - first[1]) for first, second in zip(points, points[1:]))


def bend_count(points: Sequence[Point]) -> int:
    axes = [segment_axis(first, second) for first, second in zip(points, points[1:]) if not same_point(first, second)]
    return sum(first != second for first, second in zip(axes, axes[1:]))


def bounds_intersect(first: Bounds, second: Bounds, padding: float = 0.0) -> bool:
    left_a, top_a, right_a, bottom_a = first
    left_b, top_b, right_b, bottom_b = second
    return not (
        right_a + padding <= left_b
        or right_b + padding <= left_a
        or bottom_a + padding <= top_b
        or bottom_b + padding <= top_a
    )


def expand_bounds(bounds: Bounds, padding: float) -> Bounds:
    left, top, right, bottom = bounds
    return (left - padding, top - padding, right + padding, bottom + padding)


def point_in_bounds(point: Point, bounds: Bounds, *, padding: float = 0.0, interior: bool = False) -> bool:
    x, y = point
    left, top, right, bottom = bounds
    if interior:
        return left + padding < x < right - padding and top + padding < y < bottom - padding
    return left - padding <= x <= right + padding and top - padding <= y <= bottom + padding


def bounds_inside(inner: Bounds, outer: Bounds, padding: float = 0.0) -> bool:
    left, top, right, bottom = inner
    outer_left, outer_top, outer_right, outer_bottom = outer
    return (
        left >= outer_left + padding
        and top >= outer_top + padding
        and right <= outer_right - padding
        and bottom <= outer_bottom - padding
    )


def route_inside_canvas(points: Sequence[Point], canvas: Bounds, margin: float = 0.0) -> bool:
    left, top, right, bottom = canvas
    safe = (left + margin, top + margin, right - margin, bottom - margin)
    return all(point_in_bounds(point, safe) for point in points)


def _orientation(first: Point, second: Point, third: Point) -> float:
    return (second[0] - first[0]) * (third[1] - first[1]) - (second[1] - first[1]) * (third[0] - first[0])


def _on_segment(point: Point, first: Point, second: Point, epsilon: float = EPSILON) -> bool:
    return (
        min(first[0], second[0]) - epsilon <= point[0] <= max(first[0], second[0]) + epsilon
        and min(first[1], second[1]) - epsilon <= point[1] <= max(first[1], second[1]) + epsilon
        and abs(_orientation(first, second, point)) <= epsilon
    )


def segment_interaction(first_a: Point, first_b: Point, second_a: Point, second_b: Point) -> Optional[SegmentInteraction]:
    """Return a proper crossing, touch, or collinear overlap for two segments."""

    first_axis = segment_axis(first_a, first_b)
    second_axis = segment_axis(second_a, second_b)

    if first_axis == second_axis == "horizontal" and almost_equal(first_a[1], second_a[1]):
        start = max(min(first_a[0], first_b[0]), min(second_a[0], second_b[0]))
        end = min(max(first_a[0], first_b[0]), max(second_a[0], second_b[0]))
        if end - start > EPSILON:
            return SegmentInteraction("overlap", overlap_length=end - start)
        if almost_equal(start, end):
            return SegmentInteraction("touch", (start, first_a[1]))
        return None

    if first_axis == second_axis == "vertical" and almost_equal(first_a[0], second_a[0]):
        start = max(min(first_a[1], first_b[1]), min(second_a[1], second_b[1]))
        end = min(max(first_a[1], first_b[1]), max(second_a[1], second_b[1]))
        if end - start > EPSILON:
            return SegmentInteraction("overlap", overlap_length=end - start)
        if almost_equal(start, end):
            return SegmentInteraction("touch", (first_a[0], start))
        return None

    if first_axis == "horizontal" and second_axis == "vertical":
        point = (second_a[0], first_a[1])
        if _on_segment(point, first_a, first_b) and _on_segment(point, second_a, second_b):
            return SegmentInteraction("crossing", point)
        return None

    if first_axis == "vertical" and second_axis == "horizontal":
        point = (first_a[0], second_a[1])
        if _on_segment(point, first_a, first_b) and _on_segment(point, second_a, second_b):
            return SegmentInteraction("crossing", point)
        return None

    # General line intersection keeps the artifact checker useful for authored
    # SVGs and sampled curves. Collinear diagonal overlap is intentionally
    # treated conservatively as an overlap.
    o1 = _orientation(first_a, first_b, second_a)
    o2 = _orientation(first_a, first_b, second_b)
    o3 = _orientation(second_a, second_b, first_a)
    o4 = _orientation(second_a, second_b, first_b)
    if abs(o1) <= EPSILON and abs(o2) <= EPSILON and abs(o3) <= EPSILON and abs(o4) <= EPSILON:
        candidates = [point for point in (first_a, first_b, second_a, second_b) if _on_segment(point, first_a, first_b) and _on_segment(point, second_a, second_b)]
        unique = unique_points(candidates)
        if len(unique) >= 2:
            return SegmentInteraction("overlap", overlap_length=max(math.dist(a, b) for a in unique for b in unique))
        if unique:
            return SegmentInteraction("touch", unique[0])
        return None
    if o1 * o2 <= EPSILON and o3 * o4 <= EPSILON:
        denominator = (first_a[0] - first_b[0]) * (second_a[1] - second_b[1]) - (first_a[1] - first_b[1]) * (second_a[0] - second_b[0])
        if abs(denominator) <= EPSILON:
            return None
        determinant_first = first_a[0] * first_b[1] - first_a[1] * first_b[0]
        determinant_second = second_a[0] * second_b[1] - second_a[1] * second_b[0]
        x = (determinant_first * (second_a[0] - second_b[0]) - (first_a[0] - first_b[0]) * determinant_second) / denominator
        y = (determinant_first * (second_a[1] - second_b[1]) - (first_a[1] - first_b[1]) * determinant_second) / denominator
        point = (x, y)
        if _on_segment(point, first_a, first_b) and _on_segment(point, second_a, second_b):
            return SegmentInteraction("crossing", point)
    return None


def unique_points(points: Iterable[Point], tolerance: float = 0.01) -> list[Point]:
    result: list[Point] = []
    for point in points:
        rounded = (round(point[0], 2), round(point[1], 2))
        if not any(same_point(rounded, existing, tolerance) for existing in result):
            result.append(rounded)
    return result


def is_shared_route_endpoint(point: Point, first: Sequence[Point], second: Sequence[Point], tolerance: float = 0.01) -> bool:
    return any(same_point(point, endpoint, tolerance) for endpoint in (first[0], first[-1])) and any(
        same_point(point, endpoint, tolerance) for endpoint in (second[0], second[-1])
    )


def route_interactions(route: Sequence[Point], others: Sequence[Sequence[Point]]) -> RouteInteractions:
    crossings: list[Point] = []
    overlap_count = 0
    overlap_length = 0.0
    for other in others:
        if len(other) < 2:
            continue
        for first_a, first_b in zip(route, route[1:]):
            for second_a, second_b in zip(other, other[1:]):
                interaction = segment_interaction(first_a, first_b, second_a, second_b)
                if interaction is None:
                    continue
                if interaction.kind == "overlap":
                    overlap_count += 1
                    overlap_length += interaction.overlap_length
                    continue
                if interaction.point is None or is_shared_route_endpoint(interaction.point, route, other):
                    continue
                if interaction.kind in {"crossing", "touch"}:
                    crossings.append(interaction.point)
    return RouteInteractions(tuple(unique_points(crossings)), overlap_count, round(overlap_length, 2))


def route_crossing_count(route: Sequence[Point], others: Sequence[Sequence[Point]]) -> int:
    return len(route_interactions(route, others).crossings)


def route_overlap_length(route: Sequence[Point], others: Sequence[Sequence[Point]]) -> float:
    return route_interactions(route, others).overlap_length


def estimate_text_width(text: str, font_size: float = 12.0, weight: float = 1.0) -> float:
    units = 0.0
    for character in text:
        if unicodedata.combining(character):
            continue
        east_asian = unicodedata.east_asian_width(character)
        if east_asian in {"W", "F"}:
            units += 1.0
        elif character.isspace():
            units += 0.36
        elif character in "ilI.,:;!'`|":
            units += 0.32
        elif character in "MW@#%&":
            units += 0.82
        else:
            units += 0.58
    return max(font_size * 1.5, units * font_size * weight)


def estimate_text_bounds(
    x: float,
    y: float,
    text: str,
    *,
    font_size: float = 12.0,
    anchor: str = "start",
    padding: float = 0.0,
) -> Bounds:
    width = estimate_text_width(text, font_size)
    if anchor == "middle":
        left = x - width / 2
    elif anchor == "end":
        left = x - width
    else:
        left = x
    top = y - font_size * 0.82
    return (left - padding, top - padding, left + width + padding, y + font_size * 0.24 + padding)


def parse_bridge_points(raw: Optional[str]) -> list[Point]:
    if not raw:
        return []
    points: list[Point] = []
    for token in raw.split(";"):
        parts = token.strip().split(",")
        if len(parts) != 2:
            continue
        try:
            points.append((float(parts[0]), float(parts[1])))
        except ValueError:
            continue
    return points


def bridge_declared(point: Point, bridges: Sequence[Point], tolerance: float = 1.0) -> bool:
    return any(same_point(point, bridge, tolerance) for bridge in bridges)


def format_number(value: float) -> str:
    rounded = round(value, 2)
    if float(rounded).is_integer():
        return str(int(rounded))
    return str(rounded)


def path_with_bridges(route: Sequence[Point], bridges: Sequence[Point], radius: float = 5.0) -> str:
    """Build an SVG path, adding a deterministic arc at declared crossings."""

    if not route:
        return ""
    commands = [f"M {format_number(route[0][0])},{format_number(route[0][1])}"]
    remaining = unique_points(bridges)
    for start, end in zip(route, route[1:]):
        axis = segment_axis(start, end)
        if axis == "horizontal":
            direction = 1.0 if end[0] >= start[0] else -1.0
            candidates = [
                point
                for point in remaining
                if almost_equal(point[1], start[1], 0.1)
                and min(start[0], end[0]) + radius * 1.5 < point[0] < max(start[0], end[0]) - radius * 1.5
            ]
            candidates.sort(key=lambda point: direction * point[0])
            last_coordinate = start[0]
            for x, y in candidates:
                if abs(x - last_coordinate) < radius * 2.5:
                    continue
                before = x - direction * radius
                after = x + direction * radius
                commands.append(f"L {format_number(before)},{format_number(y)}")
                sweep = 0 if direction > 0 else 1
                commands.append(f"A {format_number(radius)} {format_number(radius)} 0 0 {sweep} {format_number(after)},{format_number(y)}")
                last_coordinate = after
            commands.append(f"L {format_number(end[0])},{format_number(end[1])}")
        elif axis == "vertical":
            direction = 1.0 if end[1] >= start[1] else -1.0
            candidates = [
                point
                for point in remaining
                if almost_equal(point[0], start[0], 0.1)
                and min(start[1], end[1]) + radius * 1.5 < point[1] < max(start[1], end[1]) - radius * 1.5
            ]
            candidates.sort(key=lambda point: direction * point[1])
            last_coordinate = start[1]
            for x, y in candidates:
                if abs(y - last_coordinate) < radius * 2.5:
                    continue
                before = y - direction * radius
                after = y + direction * radius
                commands.append(f"L {format_number(x)},{format_number(before)}")
                sweep = 1 if direction > 0 else 0
                commands.append(f"A {format_number(radius)} {format_number(radius)} 0 0 {sweep} {format_number(x)},{format_number(after)}")
                last_coordinate = after
            commands.append(f"L {format_number(end[0])},{format_number(end[1])}")
        else:
            commands.append(f"L {format_number(end[0])},{format_number(end[1])}")
    return " ".join(commands)
scripts/generate-diagram.sh
#!/bin/bash
# SVG Diagram Generation - Validates and exports SVG diagrams with PNG export

set -euo pipefail

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'

# Default values
STYLE="1"
WIDTH="1920"
OUTPUT_DIR="."
VALIDATE=true

# Valid diagram types
VALID_TYPES="architecture|data-flow|flowchart|sequence|comparison|timeline|mind-map|agent|memory|use-case|class|state-machine|er-diagram|network-topology"

usage() {
    cat << USAGE
Usage: $0 [OPTIONS]

Options:
    -t, --type TYPE        Diagram type ($VALID_TYPES)
    -s, --style STYLE      Style number (1-12, default: 1)
    -o, --output PATH      Output path (default: current directory)
    -w, --width WIDTH      PNG width in pixels (default: 1920)
    --no-validate          Skip validation
    -h, --help             Show this help

Examples:
    $0 -t architecture -s 1 -o ./output/arch.svg
    $0 -t class -s 2 -w 2400
    $0 -t sequence -s 6
USAGE
    exit "${1:-0}"
}

# Parse arguments
require_arg() { if [[ $# -lt 2 || "$2" == -* ]]; then echo -e "${RED}Error: $1 requires a value${NC}"; usage 1; fi; }
while [[ $# -gt 0 ]]; do
    case $1 in
        -t|--type)
            require_arg "$@"
            TYPE="$2"
            shift 2
            ;;
        -s|--style)
            require_arg "$@"
            STYLE="$2"
            shift 2
            ;;
        -o|--output)
            require_arg "$@"
            OUTPUT_PATH="$2"
            shift 2
            ;;
        -w|--width)
            require_arg "$@"
            WIDTH="$2"
            shift 2
            ;;
        --no-validate)
            VALIDATE=false
            shift
            ;;
        -h|--help)
            usage
            ;;
        *)
            echo -e "${RED}Unknown option: $1${NC}"
            usage 1
            ;;
    esac
done

# Check required parameters
if [ -z "${TYPE:-}" ]; then
    echo -e "${RED}Error: Diagram type is required${NC}"
    usage 1
fi

# Validate type
VALID_TYPE=false
for t in architecture data-flow flowchart sequence comparison timeline mind-map agent memory use-case class state-machine er-diagram network-topology; do
    if [ "$TYPE" = "$t" ]; then
        VALID_TYPE=true
        break
    fi
done

if [ "$VALID_TYPE" = false ]; then
    echo -e "${RED}Error: Invalid diagram type '$TYPE'${NC}"
    echo "Valid types: $VALID_TYPES"
    exit 1
fi

if ! [[ "$WIDTH" =~ ^[1-9][0-9]*$ ]]; then
    echo -e "${RED}Error: Width must be a positive integer${NC}"
    exit 1
fi

# Determine output path
if [ -z "${OUTPUT_PATH:-}" ]; then
    BASENAME="${TYPE}-style${STYLE}"
    SVG_FILE="${OUTPUT_DIR}/${BASENAME}.svg"
    PNG_FILE="${OUTPUT_DIR}/${BASENAME}.png"
else
    SVG_FILE="$OUTPUT_PATH"
    PNG_FILE="${OUTPUT_PATH%.svg}.png"
fi

echo -e "${BLUE}Processing ${TYPE} diagram (style ${STYLE})...${NC}"
echo "Output: $SVG_FILE"

# Load style reference
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
STYLE_FILE=$(find "${SKILL_DIR}/references" -maxdepth 1 -type f -name "style-${STYLE}-*.md" | head -n 1)

if [ -z "${STYLE_FILE:-}" ] || [ ! -f "$STYLE_FILE" ]; then
    echo -e "${RED}Error: Style file not found: ${STYLE_FILE}${NC}"
    echo "Available styles: 1-12"
    exit 1
fi

# Note: Actual SVG generation is done by the invoking AI coding agent
# This script provides validation and export only

echo -e "${YELLOW}Note: SVG content generation is handled by Codex or Claude Code${NC}"
echo -e "${YELLOW}This script provides validation and export only${NC}"

# Validate if SVG exists
if [ -f "$SVG_FILE" ]; then
    if [ "$VALIDATE" = true ]; then
        echo -e "\n${BLUE}Validating SVG...${NC}"
        if "${SKILL_DIR}/scripts/validate-svg.sh" "$SVG_FILE"; then
            echo -e "${GREEN}Validation passed${NC}"
        else
            echo -e "${RED}Validation failed${NC}"
            exit 1
        fi
    fi

    # Export PNG
    echo -e "\n${BLUE}Exporting PNG (width: ${WIDTH}px)...${NC}"

    PNG_OK=false

    # Method 1 (preferred): cairosvg — best CSS support, good fidelity
    if python3 -c "import cairosvg" 2>/dev/null; then
        echo -e "${BLUE}Using cairosvg (recommended)...${NC}"
        if python3 -c "import sys, cairosvg; cairosvg.svg2png(url=sys.argv[1], write_to=sys.argv[2], output_width=int(sys.argv[3]))" "$SVG_FILE" "$PNG_FILE" "$WIDTH" 2>/dev/null; then
            PNG_OK=true
        else
            echo -e "${YELLOW}cairosvg failed, falling back...${NC}"
        fi
    fi

    # Method 2 (fallback): rsvg-convert — may drop CSS / foreignObject
    if [ "$PNG_OK" = false ] && command -v rsvg-convert &> /dev/null; then
        echo -e "${BLUE}Using rsvg-convert (fallback)...${NC}"
        echo -e "${YELLOW}Warning: rsvg-convert may drop CSS styles or <foreignObject> — install cairosvg for better fidelity${NC}"
        echo -e "${YELLOW}  python3 -m pip install cairosvg${NC}"
        if rsvg-convert -w "$WIDTH" "$SVG_FILE" -o "$PNG_FILE" 2>/dev/null; then
            PNG_OK=true
        fi
    fi

    if [ "$PNG_OK" = true ]; then
        PNG_SIZE=$(du -h "$PNG_FILE" | cut -f1)
        echo -e "${GREEN}PNG exported: $PNG_FILE (${PNG_SIZE})${NC}"
    else
        echo -e "${RED}PNG export failed${NC}"
        echo -e "${YELLOW}Install one of:${NC}"
        echo -e "  ${YELLOW}python3 -m pip install cairosvg${NC} (recommended)"
        echo -e "  ${YELLOW}brew install librsvg${NC}        (macOS)  /  apt install librsvg2-bin (Debian)"
        exit 1
    fi
else
    echo -e "${YELLOW}SVG file not found. Generate it first with Codex or Claude Code.${NC}"
    exit 1
fi

echo -e "\n${GREEN}Done${NC}"
scripts/export-interactive-html.py
#!/usr/bin/env python3
"""Compatibility entrypoint for the interactive HTML exporter."""

from interactive_html import main


if __name__ == "__main__":
    raise SystemExit(main())
scripts/test-all-styles.sh
#!/bin/bash
# Batch Test Script
# Renders regression fixtures, validates SVGs, and exports PNGs

set -euo pipefail

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'

SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TEST_DIR="${TEST_OUTPUT_DIR:-${SKILL_DIR}/test-output}"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)

echo -e "${BLUE}=== Fireworks Tech Graph - Batch Test ===${NC}"
echo "Test directory: $TEST_DIR"
echo "Timestamp: $TIMESTAMP"
echo ""

# Create test directory
mkdir -p "$TEST_DIR"

# Test configuration
STYLES=(1 2 3 4 5 6 7 8 9 10 11 12)
STYLE_NAMES=("Flat Icon" "Dark Terminal" "Blueprint" "Notion Clean" "Glassmorphism" "Claude Official" "OpenAI" "Dark Luxury" "C4 Review Canvas" "Cloud Fabric" "Event Transit" "Ops Pulse")
PNG_WIDTH=1920

export_png() {
    local svg_file="$1"
    local png_file="$2"

    # Keep both renderer branches on the same public 1920px contract. Using
    # CairoSVG's scale would make output width depend on each SVG viewBox.
    if python3 -c "import cairosvg" 2>/dev/null \
        && python3 -c "import sys, cairosvg; cairosvg.svg2png(url=sys.argv[1], write_to=sys.argv[2], output_width=int(sys.argv[3]))" \
            "$svg_file" "$png_file" "$PNG_WIDTH" 2>/dev/null; then
        return 0
    fi
    if command -v rsvg-convert &> /dev/null \
        && rsvg-convert -w "$PNG_WIDTH" "$svg_file" -o "$png_file" 2>/dev/null; then
        return 0
    fi
    return 1
}

# Summary counters
TOTAL=0
PASSED=0
FAILED=0

FIXTURES_DIR="${SKILL_DIR}/fixtures"

echo -e "${BLUE}Testing all styles...${NC}"
echo "----------------------------------------"

for i in "${!STYLES[@]}"; do
    STYLE="${STYLES[$i]}"
    STYLE_NAME="${STYLE_NAMES[$i]}"

    echo -e "\n${YELLOW}Style $STYLE: $STYLE_NAME${NC}"

    # Check if style reference exists
    STYLE_FILE=$(find "${SKILL_DIR}/references" -maxdepth 1 -type f -name "style-${STYLE}-*.md" | head -n 1)
    if [ -z "${STYLE_FILE:-}" ] || [ ! -f "$STYLE_FILE" ]; then
        echo -e "${RED}✗ Style file not found: $STYLE_FILE${NC}"
        FAILED=$((FAILED + 1))
        TOTAL=$((TOTAL + 1))
        continue
    fi

    echo -e "${GREEN}✓ Style file found${NC}"

    if [ ! -d "$FIXTURES_DIR" ]; then
        echo -e "${RED}✗ Fixtures directory not found: $FIXTURES_DIR${NC}"
        FAILED=$((FAILED + 1))
        TOTAL=$((TOTAL + 1))
        continue
    fi

    FIXTURE_FILES=$(find "$FIXTURES_DIR" -maxdepth 1 -type f -name "*.json" | sort || true)
    MATCHED_FIXTURES=()
    MATCHED_COUNT=0
    for FIXTURE in $FIXTURE_FILES; do
        FIXTURE_STYLE=$(python3 - "$FIXTURE" <<'PY'
import json
import sys
from pathlib import Path
data = json.loads(Path(sys.argv[1]).read_text(encoding='utf-8'))
print(data.get("style", ""))
PY
)
        if [ "$FIXTURE_STYLE" = "$STYLE" ]; then
            MATCHED_FIXTURES+=("$FIXTURE")
            MATCHED_COUNT=$((MATCHED_COUNT + 1))
        fi
    done

    STATIC_FIXTURE_FILES=$(find "$FIXTURES_DIR" -maxdepth 1 -type f -name "*-style${STYLE}.svg" | sort || true)
    if [ "$MATCHED_COUNT" -eq 0 ] && [ -z "$STATIC_FIXTURE_FILES" ]; then
        echo -e "${RED}✗ No regression fixtures found for style $STYLE${NC}"
        FAILED=$((FAILED + 1))
        TOTAL=$((TOTAL + 1))
        continue
    fi

    # Render, validate, and export each fixture
    if [ "$MATCHED_COUNT" -gt 0 ]; then
      for FIXTURE in "${MATCHED_FIXTURES[@]}"; do
        BASENAME=$(basename "$FIXTURE" .json)
        SVG_FILE="${TEST_DIR}/${BASENAME}_${TIMESTAMP}.svg"
        PNG_FILE="${TEST_DIR}/${BASENAME}_${TIMESTAMP}.png"
        TEMPLATE_TYPE=$(python3 - "$FIXTURE" <<'PY'
import json
import sys
from pathlib import Path
data = json.loads(Path(sys.argv[1]).read_text(encoding='utf-8'))
print(data.get("template_type", "architecture"))
PY
)

        echo -n "  Rendering $BASENAME... "
        TOTAL=$((TOTAL + 1))

        if python3 "${SKILL_DIR}/scripts/generate-from-template.py" "$TEMPLATE_TYPE" "$SVG_FILE" "$(cat "$FIXTURE")" > /dev/null 2>&1 \
            && "${SKILL_DIR}/scripts/validate-svg.sh" "$SVG_FILE" > /dev/null 2>&1; then
            # Prefer CairoSVG (best CSS support); fall back to rsvg-convert.
            if export_png "$SVG_FILE" "$PNG_FILE"; then
                PNG_SIZE=$(du -h "$PNG_FILE" | cut -f1)
                echo -e "${GREEN}✓ Pass${NC} (${PNG_SIZE})"
                PASSED=$((PASSED + 1))
            else
                echo -e "${RED}✗ Fail${NC} (PNG export failed)"
                FAILED=$((FAILED + 1))
            fi
        else
            echo -e "${RED}✗ Fail${NC}"
            FAILED=$((FAILED + 1))
            if [ -f "$SVG_FILE" ]; then
                "${SKILL_DIR}/scripts/validate-svg.sh" "$SVG_FILE" 2>&1 | grep -E "✗|Error" | sed 's/^/    /' || true
            fi
        fi
      done
    fi

    # AI-authored styles use static SVG fixtures because the template generator
    # intentionally does not own their visual composition.
    for FIXTURE in $STATIC_FIXTURE_FILES; do
        BASENAME=$(basename "$FIXTURE" .svg)
        SVG_FILE="${TEST_DIR}/${BASENAME}_${TIMESTAMP}.svg"
        PNG_FILE="${TEST_DIR}/${BASENAME}_${TIMESTAMP}.png"
        echo -n "  Validating $BASENAME... "
        TOTAL=$((TOTAL + 1))
        cp "$FIXTURE" "$SVG_FILE"

        if "${SKILL_DIR}/scripts/validate-svg.sh" "$SVG_FILE" > /dev/null 2>&1; then
            if export_png "$SVG_FILE" "$PNG_FILE"; then
                PNG_SIZE=$(du -h "$PNG_FILE" | cut -f1)
                echo -e "${GREEN}✓ Pass${NC} (${PNG_SIZE})"
                PASSED=$((PASSED + 1))
            else
                echo -e "${RED}✗ Fail${NC} (PNG export failed)"
                FAILED=$((FAILED + 1))
            fi
        else
            echo -e "${RED}✗ Fail${NC}"
            FAILED=$((FAILED + 1))
            "${SKILL_DIR}/scripts/validate-svg.sh" "$SVG_FILE" 2>&1 | grep -E "✗|Error|intersects|missing marker" | sed 's/^/    /' || true
        fi
    done
done

# Print summary
echo ""
echo "========================================"
echo -e "${BLUE}Test Summary${NC}"
echo "----------------------------------------"
echo "Total tests: $TOTAL"
echo -e "${GREEN}Passed: $PASSED${NC}"
echo -e "${RED}Failed: $FAILED${NC}"

if [ "$FAILED" -eq 0 ]; then
    echo -e "\n${GREEN}✓ All tests passed!${NC}"
    exit 0
else
    echo -e "\n${RED}✗ Some tests failed${NC}"
    exit 1
fi
scripts/semantic_contracts.py
#!/usr/bin/env python3
"""Style catalog and semantic contracts for engineering-specific diagrams.

The visual style and engineering meaning are separate inputs.  Styles 9-12
select a useful default semantic profile, while ``semantic_profile: generic``
keeps the visual theme available to other diagram types and regression tests.
"""

from __future__ import annotations

import json
import math
from functools import lru_cache
from pathlib import Path
from typing import Any, Mapping, MutableMapping, Optional, Sequence


class SemanticContractError(ValueError):
    """Raised when a diagram violates a selected engineering contract."""


STYLE_NAMES: dict[int, str] = {
    1: "Flat Icon",
    2: "Dark Terminal",
    3: "Blueprint",
    4: "Notion Clean",
    5: "Glassmorphism",
    6: "Claude Official",
    7: "OpenAI",
    8: "Dark Luxury",
    9: "C4 Review Canvas",
    10: "Cloud Fabric",
    11: "Event Transit",
    12: "Ops Pulse",
}

STYLE_DEFAULT_PROFILES = {
    9: "c4-review",
    10: "cloud-fabric",
    11: "event-transit",
    12: "ops-pulse",
}

PROFILE_ALIASES = {
    "generic": "generic",
    "none": "generic",
    "c4": "c4-review",
    "c4 review": "c4-review",
    "c4 review canvas": "c4-review",
    "architecture review board": "c4-review",
    "c4 评审画布": "c4-review",
    "架构评审画布": "c4-review",
    "c4-review": "c4-review",
    "cloud": "cloud-fabric",
    "cloud deployment": "cloud-fabric",
    "deployment topology": "cloud-fabric",
    "multi region deployment map": "cloud-fabric",
    "云部署拓扑": "cloud-fabric",
    "多区域部署图": "cloud-fabric",
    "cloud fabric": "cloud-fabric",
    "cloud-fabric": "cloud-fabric",
    "event stream": "event-transit",
    "event-stream": "event-transit",
    "event metro map": "event-transit",
    "topic rail map": "event-transit",
    "事件地铁图": "event-transit",
    "事件轨道图": "event-transit",
    "kafka": "event-transit",
    "event transit": "event-transit",
    "event-transit": "event-transit",
    "observability": "ops-pulse",
    "reliability pulse": "ops-pulse",
    "golden signals trace": "ops-pulse",
    "可靠性脉冲": "ops-pulse",
    "sre trace 评审": "ops-pulse",
    "otel": "ops-pulse",
    "ops pulse": "ops-pulse",
    "ops-pulse": "ops-pulse",
}


def _token(value: object) -> str:
    return " ".join(str(value).strip().lower().replace("_", " ").replace("-", " ").split())


STYLE_ALIASES: dict[str, int] = {}
for _style_id, _style_name in STYLE_NAMES.items():
    STYLE_ALIASES[_token(_style_name)] = _style_id
    STYLE_ALIASES[f"style {_style_id}"] = _style_id
    STYLE_ALIASES[f"风格 {_style_id}"] = _style_id
    STYLE_ALIASES[f"风格{_style_id}"] = _style_id
STYLE_ALIASES.update(
    {
        "flat": 1,
        "terminal": 2,
        "dark terminal": 2,
        "notion": 4,
        "glass": 5,
        "claude": 6,
        "openai official": 7,
        "review canvas": 9,
        "c4 canvas": 9,
        "c4 review": 9,
        "adr review canvas": 9,
        "architecture review board": 9,
        "c4 评审": 9,
        "c4 评审画布": 9,
        "adr 评审图": 9,
        "架构评审画布": 9,
        "职责边界评审图": 9,
        "cloud deployment": 10,
        "deployment topology": 10,
        "multi region deployment map": 10,
        "region vpc ownership map": 10,
        "cloud landing zone map": 10,
        "云部署拓扑": 10,
        "多区域部署图": 10,
        "region vpc 归属图": 10,
        "云 landing zone 图": 10,
        "event stream": 11,
        "event metro": 11,
        "event metro map": 11,
        "topic rail map": 11,
        "kafka topology": 11,
        "stream choreography map": 11,
        "事件轨道图": 11,
        "事件地铁图": 11,
        "topic 线路图": 11,
        "kafka 拓扑图": 11,
        "sre": 12,
        "observability": 12,
        "reliability pulse": 12,
        "incident investigation view": 12,
        "sre trace review": 12,
        "golden signals trace": 12,
        "运维脉冲图": 12,
        "可靠性脉冲": 12,
        "事故排查视图": 12,
        "sre trace 评审": 12,
        "黄金信号追踪图": 12,
    }
)


def resolve_style_index(data: Mapping[str, Any]) -> int:
    """Resolve numeric/name selectors and reject ambiguous or unknown themes."""

    selectors: list[tuple[str, object]] = []
    if data.get("style") is not None:
        selectors.append(("style", data["style"]))
    if data.get("visual_theme") is not None:
        selectors.append(("visual_theme", data["visual_theme"]))
    if not selectors:
        return 1

    resolved: list[tuple[str, int]] = []
    for field, raw in selectors:
        if isinstance(raw, bool):
            raise SemanticContractError(f"STYLE_SELECTOR: {field} must be a style id or name")
        if isinstance(raw, int):
            style_id = raw
        else:
            text = str(raw).strip()
            if text.isdigit():
                style_id = int(text)
            else:
                normalized = _token(text)
                if normalized not in STYLE_ALIASES:
                    raise SemanticContractError(f"STYLE_SELECTOR: unsupported {field}: {raw}")
                style_id = STYLE_ALIASES[normalized]
        if style_id not in STYLE_NAMES:
            raise SemanticContractError(f"STYLE_SELECTOR: unsupported {field}: {raw}")
        resolved.append((field, style_id))

    if len({style_id for _, style_id in resolved}) > 1:
        details = ", ".join(f"{field}={style_id}" for field, style_id in resolved)
        raise SemanticContractError(f"STYLE_SELECTOR_CONFLICT: {details}")
    return resolved[0][1]


def _fail(code: str, message: str) -> None:
    raise SemanticContractError(f"{code}: {message}")


def _require_text(item: Mapping[str, Any], field: str, path: str) -> str:
    value = str(item.get(field, "")).strip()
    if not value:
        _fail("SEMANTIC_REQUIRED", f"{path}.{field} is required")
    return value


def _number(value: Any, path: str) -> float:
    try:
        number = float(value)
    except (TypeError, ValueError) as error:
        raise SemanticContractError(f"SEMANTIC_NUMBER: {path} must be finite") from error
    if not math.isfinite(number):
        _fail("SEMANTIC_NUMBER", f"{path} must be finite")
    return number


def _bounds(item: Mapping[str, Any], path: str) -> tuple[float, float, float, float]:
    x = _number(item.get("x"), f"{path}.x")
    y = _number(item.get("y"), f"{path}.y")
    width = _number(item.get("width"), f"{path}.width")
    height = _number(item.get("height"), f"{path}.height")
    if width <= 0 or height <= 0:
        _fail("SEMANTIC_BOUNDS", f"{path} must have positive width and height")
    return (x, y, x + width, y + height)


def _inside(inner: Sequence[float], outer: Sequence[float], inset: float) -> bool:
    return (
        inner[0] >= outer[0] + inset
        and inner[1] >= outer[1] + inset
        and inner[2] <= outer[2] - inset
        and inner[3] <= outer[3] - inset
    )


def _rectangle_gap(first: Sequence[float], second: Sequence[float]) -> float:
    horizontal = max(first[0] - second[2], second[0] - first[2], 0.0)
    vertical = max(first[1] - second[3], second[1] - first[3], 0.0)
    return math.hypot(horizontal, vertical)


def _node_map(data: Mapping[str, Any]) -> dict[str, MutableMapping[str, Any]]:
    return {
        str(node.get("id")): node
        for node in data.get("nodes", [])
        if isinstance(node, MutableMapping)
    }


def _edge_map(data: Mapping[str, Any]) -> dict[str, MutableMapping[str, Any]]:
    return {
        str(edge.get("id")): edge
        for edge in data.get("arrows", [])
        if isinstance(edge, MutableMapping)
    }


def _require_graph_endpoints(
    data: Mapping[str, Any], nodes: Mapping[str, Mapping[str, Any]]
) -> dict[str, MutableMapping[str, Any]]:
    """Keep engineering profiles tied to semantic nodes, never loose coordinates."""

    edges = _edge_map(data)
    for edge_id, edge in edges.items():
        source_id = str(edge.get("source", "")).strip()
        target_id = str(edge.get("target", "")).strip()
        if not source_id or not target_id:
            _fail(
                "SEMANTIC_EDGE_ENDPOINT",
                f"arrows[{edge_id}] must declare both source and target node ids",
            )
        if source_id not in nodes or target_id not in nodes:
            _fail(
                "SEMANTIC_EDGE_ENDPOINT",
                f"arrows[{edge_id}] references unknown endpoint {source_id!r} -> {target_id!r}",
            )
    return edges


def _validate_c4(data: MutableMapping[str, Any]) -> dict[str, Any]:
    if str(data.get("diagram_type", "")).strip() != "c4":
        _fail("C4_DIAGRAM_TYPE", "diagram_type must be 'c4'")
    level = _require_text(data, "c4_level", "diagram")
    if level not in {"context", "container", "component"}:
        _fail("C4_LEVEL", f"unsupported c4_level: {level}")
    _require_text(data, "title", "diagram")
    _require_text(data, "scope", "diagram")
    seed = data.get("rough_seed")
    if isinstance(seed, bool) or not isinstance(seed, int):
        _fail("C4_ROUGH_SEED", "rough_seed must be an integer")
    if not isinstance(data.get("legend"), list) or not data.get("legend"):
        _fail("C4_LEGEND", "a non-empty legend is required")

    allowed = {
        "context": {"person", "software_system", "external_system"},
        "container": {"person", "software_system", "external_system", "container"},
        "component": {"person", "software_system", "external_system", "container", "component"},
    }[level]
    boundaries = {
        str(item.get("id")): item
        for item in data.get("containers", [])
        if isinstance(item, Mapping)
    }
    nodes = _node_map(data)
    for node_id, node in nodes.items():
        c4_type = _require_text(node, "c4_type", f"nodes[{node_id}]")
        if c4_type not in allowed:
            _fail("C4_MIXED_ABSTRACTION", f"{node_id} type {c4_type!r} is invalid for {level} view")
        _require_text(node, "label", f"nodes[{node_id}]")
        _require_text(node, "description", f"nodes[{node_id}]")
        if c4_type in {"container", "component"}:
            _require_text(node, "technology", f"nodes[{node_id}]")
            bounds = _bounds(node, f"nodes[{node_id}]")
            if bounds[2] - bounds[0] < 170 or bounds[3] - bounds[1] < 96:
                _fail("C4_CARD_SIZE", f"{node_id} must be at least 170x96")
        parent = str(node.get("parent", "")).strip()
        if parent and parent not in boundaries:
            _fail("C4_PARENT", f"{node_id} references unknown boundary {parent}")
        if parent and not _inside(
            _bounds(node, f"nodes[{node_id}]"),
            _bounds(boundaries[parent], f"containers[{parent}]"),
            20,
        ):
            _fail("C4_BOUNDARY_ESCAPE", f"{node_id} must stay 20px inside {parent}")

    edges = _require_graph_endpoints(data, nodes)
    for edge_id, edge in edges.items():
        _require_text(edge, "label", f"arrows[{edge_id}]")
        _require_text(edge, "protocol", f"arrows[{edge_id}]")
    return {"level": level, "rough_seed": seed, "elements": len(nodes)}


@lru_cache(maxsize=1)
def _cloud_icons() -> tuple[str, dict[str, Mapping[str, Any]]]:
    path = Path(__file__).resolve().parents[1] / "assets" / "icons" / "cloud" / "manifest-v1.json"
    payload = json.loads(path.read_text(encoding="utf-8"))
    version = str(payload.get("version", ""))
    icons: dict[str, Mapping[str, Any]] = {}
    aliases: set[str] = set()
    for item in payload.get("icons", []):
        icon_id = str(item.get("id", "")).strip()
        if not icon_id or icon_id in icons:
            _fail("CLOUD_ICON_MANIFEST", f"duplicate or empty icon id: {icon_id}")
        icons[icon_id] = item
        for alias in item.get("aliases", []):
            normalized = _token(alias)
            if normalized in aliases:
                _fail("CLOUD_ICON_MANIFEST", f"duplicate icon alias: {alias}")
            aliases.add(normalized)
    return version, icons


def _validate_cloud(data: MutableMapping[str, Any]) -> dict[str, Any]:
    if str(data.get("diagram_type", "")).strip() != "deployment":
        _fail("CLOUD_DIAGRAM_TYPE", "diagram_type must be 'deployment'")
    platform = _require_text(data, "platform_profile", "diagram")
    if platform not in {"provider-neutral", "aws", "azure", "gcp", "kubernetes"}:
        _fail("CLOUD_PLATFORM", f"unsupported platform_profile: {platform}")
    manifest_version, icon_catalog = _cloud_icons()
    if str(data.get("icon_manifest_version", "")) != manifest_version:
        _fail("CLOUD_ICON_VERSION", f"icon_manifest_version must be {manifest_version}")

    containers = {
        str(item.get("id")): item
        for item in data.get("containers", [])
        if isinstance(item, Mapping)
    }
    if not containers or not any(item.get("deployment_kind") == "region" for item in containers.values()):
        _fail("CLOUD_BOUNDARY", "at least one region deployment boundary is required")

    def depth(container_id: str, trail: tuple[str, ...] = ()) -> int:
        if container_id in trail:
            _fail("CLOUD_BOUNDARY_CYCLE", " -> ".join((*trail, container_id)))
        parent = str(containers[container_id].get("parent", "")).strip()
        if not parent:
            return 1
        if parent not in containers:
            _fail("CLOUD_BOUNDARY_PARENT", f"{container_id} references unknown parent {parent}")
        return 1 + depth(parent, (*trail, container_id))

    depths = {container_id: depth(container_id) for container_id in containers}
    if max(depths.values()) > 4:
        _fail("CLOUD_BOUNDARY_DEPTH", "deployment nesting depth cannot exceed 4")
    for container_id, container in containers.items():
        _require_text(container, "deployment_kind", f"containers[{container_id}]")
        parent = str(container.get("parent", "")).strip()
        if parent and not _inside(_bounds(container, f"containers[{container_id}]"), _bounds(containers[parent], f"containers[{parent}]"), 16):
            _fail("CLOUD_BOUNDARY_ESCAPE", f"{container_id} must be inset inside {parent}")
    ordered_containers = list(containers.items())
    for index, (first_id, first) in enumerate(ordered_containers):
        first_parent = str(first.get("parent", "")).strip()
        for second_id, second in ordered_containers[index + 1 :]:
            if first_parent != str(second.get("parent", "")).strip():
                continue
            gap = _rectangle_gap(
                _bounds(first, f"containers[{first_id}]"),
                _bounds(second, f"containers[{second_id}]"),
            )
            if gap < 16:
                _fail("CLOUD_BOUNDARY_GAP", f"siblings {first_id} and {second_id} need 16px clearance")

    nodes = _node_map(data)
    for node_id, node in nodes.items():
        deployment_id = _require_text(node, "deployment_id", f"nodes[{node_id}]")
        if deployment_id not in containers:
            _fail("CLOUD_DEPLOYMENT", f"{node_id} references unknown deployment {deployment_id}")
        if not _inside(_bounds(node, f"nodes[{node_id}]"), _bounds(containers[deployment_id], f"containers[{deployment_id}]"), 20):
            _fail("CLOUD_NODE_ESCAPE", f"{node_id} must stay 20px inside {deployment_id}")
        icon_id = _require_text(node, "icon_id", f"nodes[{node_id}]")
        if icon_id not in icon_catalog:
            _fail("CLOUD_ICON_UNKNOWN", f"unknown icon_id: {icon_id}")
        icon = icon_catalog[icon_id]
        node.setdefault("icon_badge", icon.get("badge", "CLOUD"))
        node.setdefault("icon_color", icon.get("color", "#2563eb"))
        node.setdefault("glyph", icon.get("glyph", "service"))
        node.setdefault("icon_source", "builtin-neutral")
        node.setdefault("icon_version", manifest_version)
        lineage: list[str] = []
        current = deployment_id
        while current:
            boundary = containers[current]
            lineage.append(str(boundary.get("label", current)))
            current = str(boundary.get("parent", "")).strip()
        node.setdefault("deployment_path", " › ".join(reversed(lineage)))

    edges = _require_graph_endpoints(data, nodes)
    for edge_id, edge in edges.items():
        source = nodes[str(edge["source"])]
        target = nodes[str(edge["target"])]
        if source.get("deployment_id") != target.get("deployment_id"):
            _require_text(edge, "via", f"arrows[{edge_id}]")
    return {
        "platform": platform,
        "manifest_version": manifest_version,
        "boundaries": len(containers),
        "max_depth": max(depths.values()),
    }


def _validate_event(data: MutableMapping[str, Any]) -> dict[str, Any]:
    if str(data.get("diagram_type", "")).strip() != "event_stream":
        _fail("EVENT_DIAGRAM_TYPE", "diagram_type must be 'event_stream'")
    topics = data.get("topics", [])
    if not isinstance(topics, list) or not topics:
        _fail("EVENT_TOPICS", "topics must be a non-empty array")
    if len(topics) > 4:
        _fail("EVENT_TOPIC_LIMIT", "showcase diagrams support at most four topic rails")
    topic_colors: dict[str, str] = {}
    for index, topic in enumerate(topics):
        if not isinstance(topic, Mapping):
            _fail("EVENT_TOPIC", f"topics[{index}] must be an object")
        topic_id = _require_text(topic, "id", f"topics[{index}]")
        color = _require_text(topic, "color", f"topics[{index}]")
        if topic_id in topic_colors:
            _fail("EVENT_TOPIC_DUPLICATE", f"duplicate topic id: {topic_id}")
        topic_colors[topic_id] = color

    nodes = _node_map(data)
    allowed_roles = {"producer", "station", "junction", "consumer", "dlq", "state_store"}
    orders: dict[tuple[str, int], str] = {}
    for node_id, node in nodes.items():
        role = _require_text(node, "transit_role", f"nodes[{node_id}]")
        if role not in allowed_roles:
            _fail("EVENT_ROLE", f"unsupported transit_role {role!r} on {node_id}")
        topic_id = str(node.get("topic_id", "")).strip()
        if topic_id and topic_id not in topic_colors:
            _fail("EVENT_TOPIC_UNKNOWN", f"{node_id} references unknown topic {topic_id}")
        if topic_id:
            node.setdefault("rail_color", topic_colors[topic_id])
        if role in {"station", "junction"}:
            _require_text(node, "operation", f"nodes[{node_id}]")
        if role == "consumer":
            _require_text(node, "consumer_group", f"nodes[{node_id}]")
        if role not in {"dlq", "state_store"}:
            order = node.get("station_order")
            if isinstance(order, bool) or not isinstance(order, int):
                _fail("EVENT_STATION_ORDER", f"{node_id}.station_order must be an integer")
            key = (topic_id, order)
            if key in orders:
                _fail("EVENT_STATION_ORDER", f"duplicate order {order} for topic {topic_id}")
            orders[key] = node_id

    edges = _require_graph_endpoints(data, nodes)
    rail_outgoing: dict[str, int] = {}
    for edge_id, edge in edges.items():
        transit_type = _require_text(edge, "transit_type", f"arrows[{edge_id}]")
        source = nodes[str(edge["source"])]
        target = nodes[str(edge["target"])]
        if transit_type == "rail":
            topic_id = _require_text(edge, "topic_id", f"arrows[{edge_id}]")
            if source.get("topic_id") != topic_id or target.get("topic_id") != topic_id:
                _fail("EVENT_TOPIC_DRIFT", f"{edge_id} must connect nodes on topic {topic_id}")
            if int(target.get("station_order", -1)) != int(source.get("station_order", -2)) + 1:
                _fail("EVENT_RAIL_ORDER", f"{edge_id} must connect adjacent increasing stations")
            source_bounds = _bounds(source, f"nodes[{source.get('id')}]")
            target_bounds = _bounds(target, f"nodes[{target.get('id')}]")
            if abs((source_bounds[1] + source_bounds[3]) - (target_bounds[1] + target_bounds[3])) > 1e-6:
                _fail("EVENT_RAIL_ALIGNMENT", f"{edge_id} rail endpoints must share one horizontal centerline")
            if target_bounds[0] - source_bounds[2] < 64:
                _fail("EVENT_RAIL_LENGTH", f"{edge_id} rail must have at least 64px clearance")
            if edge.get("source_port") != "right" or edge.get("target_port") != "left":
                _fail("EVENT_RAIL_PORT", f"{edge_id} must use right-to-left ports")
            rail_outgoing[str(edge.get("source"))] = rail_outgoing.get(str(edge.get("source")), 0) + 1
        elif transit_type == "dead_letter":
            if target.get("transit_role") != "dlq":
                _fail("EVENT_DLQ_TARGET", f"{edge_id} must target a dlq node")
            edge.setdefault("dashed", True)
        elif transit_type == "branch":
            if source.get("transit_role") != "junction":
                _fail("EVENT_BRANCH_JUNCTION", f"{edge_id} must depart from a junction node")
        elif transit_type not in {"publish", "branch", "consume", "retry", "state"}:
            _fail("EVENT_EDGE_TYPE", f"unsupported transit_type: {transit_type}")
    for node_id, count in rail_outgoing.items():
        if count > 1 and nodes[node_id].get("transit_role") != "junction":
            _fail("EVENT_BRANCH_JUNCTION", f"{node_id} branches without a junction role")
    return {"topics": len(topics), "stations": len(nodes)}


def _validate_ops(data: MutableMapping[str, Any]) -> dict[str, Any]:
    if str(data.get("diagram_type", "")).strip() != "observability":
        _fail("OPS_DIAGRAM_TYPE", "diagram_type must be 'observability'")
    observation_window = _require_text(data, "observation_window", "diagram")
    nodes = _node_map(data)
    edges = _require_graph_endpoints(data, nodes)
    services = [node for node in nodes.values() if node.get("ops_role") == "service"]
    if not services or len(services) > 12:
        _fail("OPS_SERVICE_LIMIT", "an Ops Pulse view requires 1-12 service nodes")
    expected_signals = {"latency", "traffic", "errors", "saturation"}
    allowed_status = {"ok", "warn", "critical", "unknown"}
    for node in services:
        node_id = str(node.get("id"))
        status = _require_text(node, "status", f"nodes[{node_id}]")
        if status not in allowed_status:
            _fail("OPS_SERVICE_STATUS", f"unsupported status {status!r} on {node_id}")
        metrics = node.get("signals")
        if not isinstance(metrics, Mapping) or set(metrics) != expected_signals:
            _fail("OPS_GOLDEN_SIGNALS", f"{node_id} must define exactly latency, traffic, errors, saturation")
        _require_text(node, "status_label", f"nodes[{node_id}]")
        bounds = _bounds(node, f"nodes[{node_id}]")
        if bounds[2] - bounds[0] < 180 or bounds[3] - bounds[1] < 108:
            _fail("OPS_CARD_SIZE", f"{node_id} must be at least 180x108")
        normalized_signals: list[dict[str, str]] = []
        for signal_name in ("latency", "traffic", "errors", "saturation"):
            signal = metrics[signal_name]
            if not isinstance(signal, Mapping):
                _fail("OPS_SIGNAL", f"{node_id}.{signal_name} must be an object")
            value = _require_text(signal, "value", f"nodes[{node_id}].signals.{signal_name}")
            unit = _require_text(signal, "unit", f"nodes[{node_id}].signals.{signal_name}")
            window = _require_text(signal, "window", f"nodes[{node_id}].signals.{signal_name}")
            if window != observation_window:
                _fail(
                    "OPS_OBSERVATION_WINDOW",
                    f"{node_id}.{signal_name} window {window!r} must match diagram observation_window {observation_window!r}",
                )
            status = _require_text(signal, "status", f"nodes[{node_id}].signals.{signal_name}")
            if status not in allowed_status:
                _fail("OPS_SIGNAL_STATUS", f"unsupported status {status!r} on {node_id}.{signal_name}")
            normalized_signals.append(
                {"name": signal_name, "value": value, "unit": unit, "window": window, "status": status}
            )
        node["metric_badges"] = normalized_signals

    critical_path = data.get("critical_path", [])
    if not isinstance(critical_path, list) or not critical_path:
        _fail("OPS_CRITICAL_PATH", "critical_path must be a non-empty ordered edge-id list")
    if len(set(map(str, critical_path))) != len(critical_path):
        _fail("OPS_CRITICAL_PATH", "critical_path cannot repeat an edge")
    previous_target = None
    visited_services: set[str] = set()
    service_ids = {str(node.get("id")) for node in services}
    for critical_index, raw_edge_id in enumerate(critical_path):
        edge_id = str(raw_edge_id)
        if edge_id not in edges:
            _fail("OPS_CRITICAL_PATH", f"unknown critical edge: {edge_id}")
        edge = edges[edge_id]
        if edge.get("edge_kind", "business") != "business":
            _fail("OPS_CRITICAL_PATH", f"critical edge {edge_id} must be a business edge")
        source_id = str(edge.get("source"))
        target_id = str(edge.get("target"))
        if source_id not in service_ids or target_id not in service_ids:
            _fail("OPS_CRITICAL_PATH", f"critical edge {edge_id} must connect service nodes")
        if previous_target is not None and str(edge.get("source")) != previous_target:
            _fail("OPS_CRITICAL_PATH", f"critical path is discontinuous before {edge_id}")
        if not visited_services:
            visited_services.add(source_id)
        if target_id in visited_services:
            _fail("OPS_CRITICAL_PATH", f"critical path repeats service {target_id}")
        visited_services.add(target_id)
        previous_target = target_id
        edge["critical"] = True
        edge["critical_path_id"] = str(data.get("critical_path_id", "critical-1"))
        edge["critical_hop"] = critical_index + 1
        edge["critical_hops"] = len(critical_path)

    business_flows = {
        str(edge.get("flow", "control"))
        for edge in edges.values()
        if edge.get("edge_kind", "business") == "business"
    }
    telemetry_flows = {
        str(edge.get("flow", "async"))
        for edge in edges.values()
        if edge.get("edge_kind") == "telemetry"
    }
    if business_flows & telemetry_flows:
        _fail("OPS_FLOW_SEMANTICS", "business and telemetry edges must use different flow tokens")

    spans = [node for node in nodes.values() if node.get("ops_role") == "trace_span"]
    if not spans:
        _fail("OPS_TRACE_REQUIRED", "an Ops Pulse view requires one correlated trace waterfall")
    span_map = {str(span.get("span_id")): span for span in spans}
    if len(span_map) != len(spans):
        _fail("OPS_SPAN_ID", "trace span ids must be unique")
    roots = 0
    root_span: Optional[Mapping[str, Any]] = None
    for span in spans:
        span_id = _require_text(span, "span_id", f"nodes[{span.get('id')}]")
        start = _number(span.get("start_ms"), f"spans[{span_id}].start_ms")
        duration = _number(span.get("duration_ms"), f"spans[{span_id}].duration_ms")
        if duration <= 0:
            _fail("OPS_SPAN_DURATION", f"{span_id} duration must be positive")
        parent_id = str(span.get("parent_span", "")).strip()
        if not parent_id:
            roots += 1
            root_span = span
            continue
        parent = span_map.get(parent_id)
        if parent is None:
            _fail("OPS_SPAN_PARENT", f"{span_id} references unknown parent span {parent_id}")
        parent_start = _number(parent.get("start_ms"), f"spans[{parent_id}].start_ms")
        parent_duration = _number(parent.get("duration_ms"), f"spans[{parent_id}].duration_ms")
        if start < parent_start or start + duration > parent_start + parent_duration:
            _fail("OPS_SPAN_COVERAGE", f"{span_id} must be contained by {parent_id}")
    if spans and roots != 1:
        _fail("OPS_SPAN_ROOT", "trace waterfall must contain exactly one root span")
    for span_id, span in span_map.items():
        seen: set[str] = set()
        current_id = span_id
        current = span
        while current.get("parent_span"):
            if current_id in seen:
                _fail("OPS_SPAN_CYCLE", f"trace parent cycle contains {current_id}")
            seen.add(current_id)
            current_id = str(current.get("parent_span"))
            if current_id not in span_map:
                break
            current = span_map[current_id]
    if root_span is not None:
        root_bounds = _bounds(root_span, f"spans[{root_span.get('span_id')}]")
        root_start = _number(root_span.get("start_ms"), "root_span.start_ms")
        root_duration = _number(root_span.get("duration_ms"), "root_span.duration_ms")
        pixels_per_ms = (root_bounds[2] - root_bounds[0]) / root_duration
        origin_x = root_bounds[0] - root_start * pixels_per_ms
        for span in spans:
            span_id = str(span.get("span_id"))
            span_bounds = _bounds(span, f"spans[{span_id}]")
            start = _number(span.get("start_ms"), f"spans[{span_id}].start_ms")
            duration = _number(span.get("duration_ms"), f"spans[{span_id}].duration_ms")
            expected_x = origin_x + start * pixels_per_ms
            expected_width = duration * pixels_per_ms
            if abs(span_bounds[0] - expected_x) > 1.5 or abs((span_bounds[2] - span_bounds[0]) - expected_width) > 1.5:
                _fail("OPS_SPAN_SCALE", f"{span_id} x/width must encode start_ms/duration_ms on the root time scale")
    return {
        "services": len(services),
        "spans": len(spans),
        "critical_edges": len(critical_path),
        "observation_window": observation_window,
    }


def validate_semantic_contract(data: MutableMapping[str, Any]) -> dict[str, Any]:
    """Validate and enrich a normalized diagram payload."""

    style_index = resolve_style_index(data)
    raw_profile = data.get("semantic_profile")
    if raw_profile is None:
        profile = STYLE_DEFAULT_PROFILES.get(style_index, "generic")
    else:
        normalized = _token(raw_profile)
        if normalized not in PROFILE_ALIASES:
            _fail("SEMANTIC_PROFILE", f"unsupported semantic_profile: {raw_profile}")
        profile = PROFILE_ALIASES[normalized]
    data["semantic_profile"] = profile

    validators = {
        "c4-review": _validate_c4,
        "cloud-fabric": _validate_cloud,
        "event-transit": _validate_event,
        "ops-pulse": _validate_ops,
    }
    details = validators[profile](data) if profile in validators else {}
    return {
        "ok": True,
        "style": style_index,
        "visual_theme": STYLE_NAMES[style_index],
        "profile": profile,
        "details": details,
    }


__all__ = [
    "PROFILE_ALIASES",
    "STYLE_ALIASES",
    "STYLE_DEFAULT_PROFILES",
    "STYLE_NAMES",
    "SemanticContractError",
    "resolve_style_index",
    "validate_semantic_contract",
]
scripts/motion.py
#!/usr/bin/env python3
"""Validated SVG-to-GIF motion planning for semantic technical diagrams."""

from __future__ import annotations

import hashlib
import json
import math
import os
import re
import shutil
import subprocess
import tempfile
import uuid
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Optional

from interactive_html import sanitize_svg
from validate_svg import run_check


SCRIPT_DIR = Path(__file__).resolve().parent
MOTION_WORKER = SCRIPT_DIR / "svg2gif.js"
STYLE_PRESETS = {
    1: "memory-weave",
    2: "tool-grounding",
    3: "service-blueprint",
    4: "memory-lifecycle",
    5: "agent-orchestration",
    6: "governed-runtime",
    7: "token-stream",
    8: "golden-circuit",
    9: "review-trace",
    10: "cloud-flow",
    11: "event-transit",
    12: "ops-pulse",
}
PRESET_STYLES = {preset: style for style, preset in STYLE_PRESETS.items()}
MOTION_PRESETS = tuple(STYLE_PRESETS.values())
REVIEWED_STYLE_IDS = frozenset(range(1, 13))
STYLE_MOTION_ROLES = {
    1: {"ingress", "reason", "extract", "transform", "resolve", "memory-write", "memory-read", "response-context"},
    2: {"ingress", "delegate", "tool-call", "inspect", "index", "grounding", "answer"},
    3: {"ingress", "policy", "fanout", "data-write", "event", "telemetry"},
    4: {"sample", "attend", "invoke", "remember", "consolidate", "recall"},
    5: {"ingress", "delegate", "evidence", "artifact", "context", "deliver", "approval"},
    6: {"ingress", "dispatch", "runtime-branch", "foundation", "promote"},
    7: {"connect", "prepare", "invoke", "tool-call", "token-stream", "govern", "measure", "promote"},
    8: {"primary", "memory-read", "tool-call", "data", "trace", "feedback"},
    9: {"review-entry", "review-request", "review-async", "review-state", "review-external"},
    10: {"global-route", "regional-write", "cross-region"},
    11: {"topic-rail", "dead-letter", "state-project"},
    12: {"critical-request", "telemetry-export"},
}
CHECKS = ("xml", "markers", "geometry", "composition")
MAX_INPUT_BYTES = 20 * 1024 * 1024
MAX_RENDERED_PIXELS = 600_000_000
MOTION_FORMAT = {"suffix": ".gif", "name": "gif", "mime": "image/gif", "loop_playback": "embedded-infinite"}
MOTION_TIMEOUTS = {"runtime_probe": 15, "render": 120, "encode": 120, "media_probe": 30}
MOTION_SIZE_TARGET_BYTES = 500_000
DEFAULT_MOTION_DURATION = 5.75
DEFAULT_MOTION_FPS = 20
DEFAULT_MOTION_FRAME_COUNT = 115
MINIMUM_MOTION_FRAME_COUNT = 55
MOTION_GRAMMAR_VERSION = "3.4"
APPROVED_BASELINE_DURATION = 3.75
APPROVED_BASELINE_FRAME_COUNT = 75
TIMING_REVISION_ID = "+2s-settled-flow"
TIMING_REVISION_APPROVED_AT = "2026-07-17"
MOTION_CURVES = {
    "draw": "linear",
    "persistent-data-flow": "linear",
    "reset": "linear",
}
SCENE_SIGNATURES = {
    1: {
        "name": "memory-weave-draw-on-persistent-data-flow",
        "distinctive_primitives": [
            "semantic-route-build",
            "settled-marker-arrival",
            "eight-route-persistent-data-flow",
        ],
    },
    2: {
        "name": "tool-grounding-terminal-evidence-trace",
        "distinctive_primitives": [
            "semantic-route-build",
            "settled-marker-arrival",
            "eight-route-terminal-evidence-stream",
            "terminal-prompt-cursor",
        ],
    },
    3: {
        "name": "service-blueprint-distribution-wave",
        "distinctive_primitives": [
            "semantic-route-build",
            "settled-marker-arrival",
            "ten-route-blueprint-distribution-wave",
            "blueprint-registration-bead",
        ],
    },
    4: {
        "name": "memory-lifecycle-notion-card-handoff",
        "distinctive_primitives": [
            "semantic-route-build",
            "settled-marker-arrival",
            "six-route-notion-memory-rail",
            "notion-memory-card",
        ],
    },
    5: {
        "name": "glassmorphism-multi-agent-task-capsules",
        "distinctive_primitives": ["semantic-route-build", "glass-handoff-rail", "glass-task-capsule", "coordinator-halo"],
    },
    6: {
        "name": "claude-official-governed-runtime-policy-seals",
        "distinctive_primitives": ["semantic-route-build", "governance-thread", "policy-seal"],
    },
    7: {
        "name": "openai-official-api-token-train",
        "distinctive_primitives": ["semantic-route-build", "api-token-rail", "token-train"],
    },
    8: {
        "name": "dark-luxury-golden-gem-circuit",
        "distinctive_primitives": ["semantic-route-build", "luxury-circuit-rail", "gem-tracer"],
    },
    9: {
        "name": "c4-review-canvas-moving-review-cursors",
        "distinctive_primitives": ["semantic-route-build", "review-trace-rail", "review-cursor"],
    },
    10: {
        "name": "cloud-fabric-active-active-synchronized-regions",
        "distinctive_primitives": ["semantic-route-build", "cloud-flow-rail", "region-chevron-pair", "replication-capsule", "availability-pulse"],
    },
    11: {
        "name": "event-transit-three-car-event-trains",
        "distinctive_primitives": ["semantic-route-build", "event-transit-rail", "event-train", "exception-car", "projection-car", "station-dwell-ring"],
    },
    12: {
        "name": "ops-pulse-incident-path-waterfall-scanner",
        "distinctive_primitives": ["semantic-route-build", "incident-pulse-rail", "ecg-head", "telemetry-export-packet", "trace-span-reveal", "waterfall-scanner"],
    },
}
STYLE_1_DRAW_SCHEDULE = [
    {"role": "ingress", "frames": [1, 8]},
    {"role": "reason", "frames": [5, 12]},
    {"role": "extract", "frames": [9, 16]},
    {"role": "transform", "frames": [13, 20]},
    {"role": "resolve", "frames": [17, 24]},
    {"role": "memory-write", "frames": [21, 28]},
    {"role": "memory-read", "frames": [25, 32]},
    {"role": "response-context", "frames": [29, 36]},
]
STYLE_2_DRAW_SCHEDULE = [
    {"role": "ingress", "stage": 1, "order": 0, "frames": [1, 8]},
    {"role": "delegate", "stage": 2, "order": 0, "frames": [5, 12]},
    {"role": "tool-call", "stage": 3, "order": 0, "frames": [9, 16]},
    {"role": "inspect", "stage": 4, "order": 0, "frames": [13, 20]},
    {"role": "index", "stage": 5, "order": 0, "frames": [17, 24]},
    {"role": "grounding", "stage": 6, "order": 0, "frames": [21, 28]},
    {"role": "grounding", "stage": 6, "order": 1, "frames": [25, 32]},
    {"role": "answer", "stage": 7, "order": 0, "frames": [29, 36]},
]
STYLE_3_DRAW_SCHEDULE = [
    {"role": "ingress", "stage": 1, "order": 0, "frames": [1, 6]},
    {"role": "policy", "stage": 2, "order": 0, "frames": [4, 9]},
    {"role": "fanout", "stage": 3, "order": 0, "frames": [8, 13]},
    {"role": "fanout", "stage": 3, "order": 1, "frames": [11, 16]},
    {"role": "fanout", "stage": 3, "order": 2, "frames": [14, 19]},
    {"role": "data-write", "stage": 4, "order": 0, "frames": [18, 23]},
    {"role": "data-write", "stage": 4, "order": 1, "frames": [21, 26]},
    {"role": "data-write", "stage": 4, "order": 2, "frames": [24, 29]},
    {"role": "event", "stage": 5, "order": 0, "frames": [28, 33]},
    {"role": "telemetry", "stage": 6, "order": 0, "frames": [31, 36]},
]
STYLE_4_DRAW_SCHEDULE = [
    {"role": "sample", "stage": 1, "order": 0, "frames": [1, 4]},
    {"role": "attend", "stage": 2, "order": 0, "frames": [5, 8]},
    {"role": "invoke", "stage": 3, "order": 0, "frames": [9, 12]},
    {"role": "remember", "stage": 4, "order": 0, "frames": [13, 22]},
    {"role": "consolidate", "stage": 5, "order": 0, "frames": [23, 26]},
    {"role": "recall", "stage": 6, "order": 0, "frames": [27, 36]},
]
STYLE_5_DRAW_SCHEDULE = [
    {"role": "ingress", "stage": 1, "order": 0, "frames": [1, 6]},
    {"role": "delegate", "stage": 2, "order": 0, "frames": [5, 12]},
    {"role": "delegate", "stage": 2, "order": 1, "frames": [8, 15]},
    {"role": "delegate", "stage": 2, "order": 2, "frames": [11, 18]},
    {"role": "evidence", "stage": 3, "order": 0, "frames": [17, 24]},
    {"role": "artifact", "stage": 3, "order": 1, "frames": [20, 27]},
    {"role": "context", "stage": 4, "order": 0, "frames": [25, 30]},
    {"role": "deliver", "stage": 5, "order": 0, "frames": [29, 36]},
    {"role": "approval", "stage": 5, "order": 1, "frames": [29, 36]},
]
STYLE_6_DRAW_SCHEDULE = [
    {"role": "ingress", "stage": 1, "order": 0, "frames": [1, 6]},
    {"role": "dispatch", "stage": 2, "order": 0, "frames": [5, 12]},
    {"role": "runtime-branch", "stage": 3, "order": 0, "frames": [10, 17]},
    {"role": "runtime-branch", "stage": 3, "order": 1, "frames": [13, 20]},
    {"role": "runtime-branch", "stage": 3, "order": 2, "frames": [16, 23]},
    {"role": "foundation", "stage": 4, "order": 0, "frames": [21, 28]},
    {"role": "foundation", "stage": 4, "order": 1, "frames": [24, 31]},
    {"role": "foundation", "stage": 4, "order": 2, "frames": [27, 34]},
    {"role": "promote", "stage": 5, "order": 0, "frames": [31, 36]},
]
STYLE_7_DRAW_SCHEDULE = [
    {"role": "connect", "stage": 1, "order": 0, "frames": [1, 6]},
    {"role": "prepare", "stage": 2, "order": 0, "frames": [5, 12]},
    {"role": "invoke", "stage": 3, "order": 0, "frames": [10, 17]},
    {"role": "tool-call", "stage": 4, "order": 0, "frames": [15, 22]},
    {"role": "token-stream", "stage": 4, "order": 1, "frames": [18, 27]},
    {"role": "govern", "stage": 5, "order": 0, "frames": [25, 32]},
    {"role": "measure", "stage": 5, "order": 1, "frames": [25, 32]},
    {"role": "promote", "stage": 6, "order": 0, "frames": [31, 36]},
]
STYLE_8_DRAW_SCHEDULE = [
    {"role": "primary", "stage": 1, "order": 0, "frames": [1, 6]},
    {"role": "primary", "stage": 2, "order": 0, "frames": [5, 10]},
    {"role": "memory-read", "stage": 3, "order": 0, "frames": [9, 18]},
    {"role": "tool-call", "stage": 3, "order": 1, "frames": [12, 21]},
    {"role": "data", "stage": 4, "order": 0, "frames": [20, 25]},
    {"role": "trace", "stage": 5, "order": 0, "frames": [24, 29]},
    {"role": "feedback", "stage": 6, "order": 0, "frames": [28, 36]},
]
STYLE_9_DRAW_SCHEDULE = [
    {"role": "review-entry", "stage": 1, "order": 0, "frames": [1, 7]},
    {"role": "review-request", "stage": 2, "order": 0, "frames": [7, 13]},
    {"role": "review-async", "stage": 3, "order": 0, "frames": [13, 22]},
    {"role": "review-state", "stage": 4, "order": 0, "frames": [22, 30]},
    {"role": "review-external", "stage": 4, "order": 1, "frames": [28, 36]},
]
STYLE_10_DRAW_SCHEDULE = [
    {"role": "global-route", "stage": 1, "order": 0, "frames": [1, 12]},
    {"role": "global-route", "stage": 1, "order": 1, "frames": [1, 12]},
    {"role": "regional-write", "stage": 2, "order": 0, "frames": [13, 22]},
    {"role": "regional-write", "stage": 2, "order": 1, "frames": [13, 22]},
    {"role": "cross-region", "stage": 3, "order": 0, "frames": [23, 36]},
]
STYLE_11_DRAW_SCHEDULE = [
    {"role": "topic-rail", "stage": 1, "order": 0, "frames": [1, 6]},
    {"role": "topic-rail", "stage": 2, "order": 0, "frames": [7, 12]},
    {"role": "topic-rail", "stage": 3, "order": 0, "frames": [13, 18]},
    {"role": "topic-rail", "stage": 4, "order": 0, "frames": [19, 24]},
    {"role": "dead-letter", "stage": 5, "order": 0, "frames": [25, 32]},
    {"role": "state-project", "stage": 5, "order": 1, "frames": [29, 36]},
]
STYLE_12_DRAW_SCHEDULE = [
    {"role": "critical-request", "stage": 1, "order": 0, "frames": [1, 6]},
    {"role": "critical-request", "stage": 2, "order": 0, "frames": [7, 12]},
    {"role": "critical-request", "stage": 3, "order": 0, "frames": [13, 18]},
    {"role": "telemetry-export", "stage": 4, "order": 0, "frames": [19, 26]},
]
# Backwards-compatible public name for the approved Style 1 report contract.
DRAW_SCHEDULE = STYLE_1_DRAW_SCHEDULE
RESET_OPACITY_SAMPLES = [1.0, 0.7575, 0.515, 0.2725, 0.03]

STYLE_SCENE_CONTRACTS = {
    1: {
        "preset": "memory-weave",
        "draw_schedule": STYLE_1_DRAW_SCHEDULE,
        "schedule_keys": [
            ("ingress", 0),
            ("reason", 0),
            ("extract", 0),
            ("transform", 0),
            ("resolve", 1),
            ("memory-write", 0),
            ("memory-read", 0),
            ("response-context", 0),
        ],
        "expected_stages": [1, 2, 3, 4, 4, 5, 6, 7],
        "stream_primitive": "persistent-data-flow-stream",
        "packet_head_primitive": "persistent-data-flow-head",
        "signature_primitive": None,
        "route_label_count": 8,
    },
    2: {
        "preset": "tool-grounding",
        "draw_schedule": STYLE_2_DRAW_SCHEDULE,
        "schedule_keys": [
            ("ingress", 0),
            ("delegate", 0),
            ("tool-call", 0),
            ("inspect", 0),
            ("index", 0),
            ("grounding", 0),
            ("grounding", 1),
            ("answer", 0),
        ],
        "expected_stages": [1, 2, 3, 4, 5, 6, 6, 7],
        "stream_primitive": "terminal-evidence-stream",
        "packet_head_primitive": "terminal-command-head",
        "signature_primitive": "terminal-prompt-cursor",
        "route_label_count": 8,
    },
    3: {
        "preset": "service-blueprint",
        "draw_schedule": STYLE_3_DRAW_SCHEDULE,
        "schedule_keys": [
            ("ingress", 0),
            ("policy", 0),
            ("fanout", 0),
            ("fanout", 1),
            ("fanout", 2),
            ("data-write", 0),
            ("data-write", 1),
            ("data-write", 2),
            ("event", 0),
            ("telemetry", 0),
        ],
        "expected_stages": [1, 2, 3, 3, 3, 4, 4, 4, 5, 6],
        "stream_primitive": "blueprint-distribution-wave",
        "packet_head_primitive": None,
        "registration_bead_primitive": "blueprint-registration-bead",
        "signature_primitive": None,
        "route_label_count": 7,
        "source_sha256": "b8f55d9ea0c6111176d8ff50d2e844b2001ee5087a3940621e635e1b875d470d",
    },
    4: {
        "preset": "memory-lifecycle",
        "draw_schedule": STYLE_4_DRAW_SCHEDULE,
        "schedule_keys": [
            ("sample", 0),
            ("attend", 0),
            ("invoke", 0),
            ("remember", 0),
            ("consolidate", 0),
            ("recall", 0),
        ],
        "expected_stages": [1, 2, 3, 4, 5, 6],
        "stream_primitive": "notion-memory-rail",
        "packet_head_primitive": None,
        "memory_card_primitive": "notion-memory-card",
        "signature_primitive": None,
        "route_label_count": 2,
        "source_sha256": "04cf833659e82c3e1743db4042cacf839a6d784a99c32d076e36fd4776e70c1b",
    },
    5: {
        "preset": "agent-orchestration",
        "draw_schedule": STYLE_5_DRAW_SCHEDULE,
        "schedule_keys": [("ingress", 0), ("delegate", 0), ("delegate", 1), ("delegate", 2), ("evidence", 0), ("artifact", 1), ("context", 0), ("deliver", 0), ("approval", 1)],
        "expected_stages": [1, 2, 2, 2, 3, 3, 4, 5, 5],
        "stream_primitive": "glass-handoff-rail",
        "signature_primitive": "glass-task-capsule",
        "route_label_count": 9,
        "maximum_concurrent_draws": 2,
        "fixture_sha256": "f4664045331c73179c312482b4d68d474513a059ede426891e097e615722b6a9",
        "source_sha256": "52bf52e8ac0b129fcfad8dcd06e93468b3b79e29e9e0f919be80cb58046c0991",
    },
    6: {
        "preset": "governed-runtime",
        "draw_schedule": STYLE_6_DRAW_SCHEDULE,
        "schedule_keys": [("ingress", 0), ("dispatch", 0), ("runtime-branch", 0), ("runtime-branch", 1), ("runtime-branch", 2), ("foundation", 0), ("foundation", 1), ("foundation", 2), ("promote", 0)],
        "expected_stages": [1, 2, 3, 3, 3, 4, 4, 4, 5],
        "stream_primitive": "governance-thread",
        "signature_primitive": "policy-seal",
        "route_label_count": 9,
        "maximum_concurrent_draws": 2,
        "fixture_sha256": "427127297757d7672ab365e37983a2a09bc55b26a420be9b44dc67e7ac5b9553",
        "source_sha256": "25847c17def77f9b9da1b9320504e31841c28cbcf35f56602d2f4dd76a40c772",
    },
    7: {
        "preset": "token-stream",
        "draw_schedule": STYLE_7_DRAW_SCHEDULE,
        "schedule_keys": [("connect", 0), ("prepare", 0), ("invoke", 0), ("tool-call", 0), ("token-stream", 1), ("govern", 0), ("measure", 1), ("promote", 0)],
        "expected_stages": [1, 2, 3, 4, 4, 5, 5, 6],
        "stream_primitive": "api-token-rail",
        "signature_primitive": "token-train",
        "route_label_count": 8,
        "maximum_concurrent_draws": 3,
        "fixture_sha256": "4d03096787cceb3e2be61567cf12996291dd46d2289bd5394cb30360b48a4473",
        "source_sha256": "ce07fd5279c5709b4546c59007068ca678b92122ed740d43270ecd22f7bbf82b",
    },
    8: {
        "preset": "golden-circuit",
        "draw_schedule": STYLE_8_DRAW_SCHEDULE,
        "schedule_keys": [("primary", 1, 0), ("primary", 2, 0), ("memory-read", 3, 0), ("tool-call", 3, 1), ("data", 4, 0), ("trace", 5, 0), ("feedback", 6, 0)],
        "expected_stages": [1, 2, 3, 3, 4, 5, 6],
        "stage_aware_schedule_key": True,
        "stream_primitive": "luxury-circuit-rail",
        "signature_primitive": "gem-tracer",
        "route_label_count": 7,
        "maximum_concurrent_draws": 2,
        "fixture_sha256": "6ade2db83f0fa772c4791e1a09a2c128373125da3bcfa2624434daff72316122",
        "source_sha256": "6ade2db83f0fa772c4791e1a09a2c128373125da3bcfa2624434daff72316122",
    },
    9: {
        "preset": "review-trace",
        "draw_schedule": STYLE_9_DRAW_SCHEDULE,
        "schedule_keys": [("review-entry", 0), ("review-request", 0), ("review-async", 0), ("review-state", 0), ("review-external", 1)],
        "expected_stages": [1, 2, 3, 4, 4],
        "stream_primitive": "review-trace-rail",
        "signature_primitive": "review-cursor",
        "route_label_count": 5,
        "maximum_concurrent_draws": 2,
        "fixture_sha256": "a8a0bccddc4b9b762286f3a7f21a5c1fdb98ea32f436d573bb7d3c14d7be27a9",
        "source_sha256": "b45264d17910fda296a7b52e7a338f361d8272ff14da55d2cad8c6e8dabe2717",
    },
    10: {
        "preset": "cloud-flow",
        "draw_schedule": STYLE_10_DRAW_SCHEDULE,
        "schedule_keys": [("global-route", 0), ("global-route", 1), ("regional-write", 0), ("regional-write", 1), ("cross-region", 0)],
        "expected_stages": [1, 1, 2, 2, 3],
        "stream_primitive": "cloud-flow-rail",
        "signature_primitive": "region-chevron-pair-or-replication-capsule",
        "route_label_count": 3,
        "maximum_concurrent_draws": 2,
        "fixture_sha256": "8c180ef8cecdc2419c79912245329ac9fffcc9ff08f8faeeb65541d21c747d4e",
        "source_sha256": "a739db50e30e0669dcfc926f4ce141c655f0b8f2e4e71c6a45444c9b3e61074a",
    },
    11: {
        "preset": "event-transit",
        "draw_schedule": STYLE_11_DRAW_SCHEDULE,
        "schedule_keys": [("topic-rail", 1, 0), ("topic-rail", 2, 0), ("topic-rail", 3, 0), ("topic-rail", 4, 0), ("dead-letter", 5, 0), ("state-project", 5, 1)],
        "expected_stages": [1, 2, 3, 4, 5, 5],
        "stage_aware_schedule_key": True,
        "stream_primitive": "event-transit-rail",
        "signature_primitive": "event-train-or-branch-car",
        "route_label_count": 0,
        "maximum_concurrent_draws": 2,
        "fixture_sha256": "a9fbd96129c9fc54b97b80024cb954d03d471d4c2963f99e71eff276d15d6140",
        "source_sha256": "e4ca3aa987b2495868765a0b66d11763a67890f13079c513f58451a40d5432e4",
    },
    12: {
        "preset": "ops-pulse",
        "draw_schedule": STYLE_12_DRAW_SCHEDULE,
        "schedule_keys": [("critical-request", 1, 0), ("critical-request", 2, 0), ("critical-request", 3, 0), ("telemetry-export", 4, 0)],
        "expected_stages": [1, 2, 3, 4],
        "stage_aware_schedule_key": True,
        "stream_primitive": "incident-pulse-rail-or-telemetry-export-rail",
        "signature_primitive": "ecg-head-or-telemetry-export-packet",
        "route_label_count": 0,
        "maximum_concurrent_draws": 1,
        "fixture_sha256": "2ea1d7a153ca8a39c37e9f5c5fd18a47c98a59f35694cd2bd68919d6b86b132c",
        "source_sha256": "f6170771acdd376fd78fa103214ff3125155754b6e19b87971d11c0afbab5a21",
    },
}

STYLE_SPECIALIZED_LIVE_SPECS: dict[int, dict[str, object]] = {
    5: {
        "body_primitive": "glass-handoff-rail", "signature_primitive": "glass-task-capsule",
        "dash_pattern": [13, 30], "dash_period": 43, "step": 6, "body_opacity": 0.88,
        "maximum_live_width": 2.2, "endpoint_clearance": 8,
        "phase_policy": "(motionStage * 7 + motionOrder * 3) mod 43",
        "expected_initial_phases": [7, 14, 17, 20, 21, 24, 28, 35, 38],
        "resolved_widths": [2.2] * 9,
        "geometry": {
            "shape": "rounded-translucent-plate", "width": 14, "height": 9, "rx": 3,
            "highlight_stroke_width": 1, "work_item_dot_radius": 2, "work_item_dot_count": 2,
            "tangent_aware_rotation": True,
        },
        "auxiliary": {"primitive": "coordinator-halo", "node_id": "coordinator", "period_frames": 16, "opacity_range": [0.12, 0.32], "movement": "opacity-only"},
        "direction_sentinels": [
            {"key": "ingress/0", "directions": ["right"]},
            {"key": "delegate/0", "directions": ["down", "left", "down"]},
            {"key": "delegate/2", "directions": ["down", "right", "down"]},
            {"key": "evidence/0", "directions": ["down"]},
            {"key": "context/0", "directions": ["right"]},
        ],
    },
    6: {
        "body_primitive": "governance-thread", "signature_primitive": "policy-seal",
        "dash_pattern": [11, 36], "dash_period": 47, "step": 6, "body_opacity": 0.82,
        "maximum_live_width": 2.8, "endpoint_clearance": 8,
        "phase_policy": "(motionStage * 7 + motionOrder * 3) mod 47",
        "expected_initial_phases": [7, 14, 21, 24, 27, 28, 31, 34, 35],
        "resolved_widths": [2.0] * 9,
        "geometry": {"shape": "warm-white-hexagonal-outline", "width": 12, "height": 12, "center_dot_diameter": 3, "approval_bar_width": 4, "shadow": False, "glow": False},
        "direction_sentinels": [
            {"key": "ingress/0", "directions": ["right"]}, {"key": "dispatch/0", "directions": ["down"]},
            {"key": "runtime-branch/0", "directions": ["left"]}, {"key": "runtime-branch/1", "directions": ["right"]},
            {"key": "foundation/0", "directions": ["down"]}, {"key": "promote/0", "directions": ["right"]},
        ],
    },
    7: {
        "body_primitive": "api-token-rail", "signature_primitive": "token-train",
        "dash_pattern": [10, 33], "dash_period": 43, "step": 6, "body_opacity": 0.86,
        "maximum_live_width": 2.5, "endpoint_clearance": 10,
        "phase_policy": "(motionStage * 7 + motionOrder * 3) mod 43",
        "expected_initial_phases": [7, 14, 21, 28, 31, 35, 38, 42],
        "resolved_widths": [2.0] * 8,
        "geometry": {"shape": "three-cell-token-train", "group_width": 18, "group_height": 8, "cell_width": 4, "cell_height": 4, "cell_gap": 2, "cell_opacities": [1.0, 0.72, 0.44], "tangent_aware_rotation": True},
        "direction_sentinels": [
            {"key": "connect/0", "directions": ["right"]}, {"key": "prepare/0", "directions": ["down", "left", "down"]},
            {"key": "tool-call/0", "directions": ["right"]}, {"key": "token-stream/1", "directions": ["down", "left", "down"]},
            {"key": "govern/0", "directions": ["down"]}, {"key": "promote/0", "directions": ["right"]},
        ],
    },
    8: {
        "body_primitive": "luxury-circuit-rail", "signature_primitive": "gem-tracer",
        "dash_pattern": [14, 33], "dash_period": 47, "step": 6, "body_opacity": 0.86,
        "maximum_live_width": 2.8, "endpoint_clearance": 8,
        "phase_policy": "(motionStage * 7 + motionOrder * 3) mod 47",
        "expected_initial_phases": [7, 14, 21, 24, 28, 35, 42],
        "resolved_widths": [2.0, 2.0, 1.7, 1.7, 1.7, 2.0, 1.7],
        "geometry": {"shape": "diamond-with-tapered-tail", "diamond_width": 7, "diamond_height": 7, "diamond_rotation": 45, "specular_diameter": 2, "tail_length": 12, "filtered_elements_per_tracer": 1},
        "direction_sentinels": [
            {"key": "primary/1/0", "directions": ["right"]}, {"key": "primary/2/0", "directions": ["right"]},
            {"key": "memory-read/3/0", "directions": ["down", "left", "down"]},
            {"key": "feedback/6/0", "directions": ["up", "left", "up"]},
        ],
    },
    9: {
        "body_primitive": "review-trace-rail", "signature_primitive": "review-cursor",
        "dash_pattern": [8, 33], "dash_period": 41, "step": 5, "body_opacity": 0.82,
        "maximum_live_width": 2.6, "endpoint_clearance": 9,
        "phase_policy": "(motionStage * 7 + motionOrder * 3) mod 41",
        "expected_initial_phases": [7, 14, 21, 28, 31],
        "resolved_widths": [2.0] * 5,
        "geometry": {"shape": "review-mark", "outline_circle_diameter": 11, "diagonal_handle_length": 5, "internal_check_extent": 3, "shadow": False, "glow": False},
        "direction_sentinels": [
            {"key": "review-entry/0", "directions": ["right"]}, {"key": "review-request/0", "directions": ["right"]},
            {"key": "review-async/0", "directions": ["down"]}, {"key": "review-state/0", "directions": ["left"]},
            {"key": "review-external/1", "directions": ["right"]},
        ],
    },
    10: {
        "body_primitive": "cloud-flow-rail", "signature_primitive": "region-chevron-pair-or-replication-capsule",
        "dash_pattern": [12, 31], "dash_period": 43, "step": 6, "body_opacity": 0.82,
        "maximum_live_width": 2.7, "endpoint_clearance": 8,
        "phase_policy": "motionStage * 7 mod 43; A/B orders are phase-locked",
        "expected_initial_phases": [7, 7, 14, 14, 21],
        "resolved_widths": [2.2] * 5,
        "geometry": {"routing_write": {"shape": "region-chevron-pair", "chevron_width": 6, "chevron_height": 5, "separation": 5}, "replication": {"shape": "replication-capsule", "width": 14, "height": 7, "data_cell_count": 2, "direction": "left-to-right"}},
        "auxiliary": {"primitive": "availability-pulse", "container_ids": ["region-a", "region-b"], "movement": "opacity-only", "phase_locked": True},
        "direction_sentinels": [
            {"key": "global-route/0", "directions": ["down"]}, {"key": "global-route/1", "directions": ["down"]},
            {"key": "regional-write/0", "directions": ["down"]}, {"key": "regional-write/1", "directions": ["down"]},
            {"key": "cross-region/0", "directions": ["right"]},
        ],
    },
    11: {
        "body_primitive": "event-transit-rail", "signature_primitive": "event-train-or-branch-car",
        "dash_pattern": [8, 33], "dash_period": 41, "step": 5, "body_opacity": 0.78,
        "maximum_live_width": 2.2, "endpoint_clearance": 7,
        "phase_policy": "(motionStage * 5 + motionOrder * 3) mod 41",
        "expected_initial_phases": [5, 10, 15, 20, 25, 28],
        "resolved_widths": [2.2] * 6,
        "geometry": {"main": {"shape": "three-car-event-train", "car_diameter": 5, "car_gap": 3, "car_count": 3}, "dead_letter": {"shape": "red-outlined-exception-car"}, "state_project": {"shape": "teal-two-cell-projection-car", "cell_count": 2}},
        "auxiliary": {"primitive": "station-dwell-ring", "period_frames": 10, "movement": "opacity-only", "geometry_expansion": 0, "count": 4},
        "direction_sentinels": [
            {"key": "topic-rail/1/0", "directions": ["right"]}, {"key": "topic-rail/2/0", "directions": ["right"]},
            {"key": "topic-rail/3/0", "directions": ["right"]}, {"key": "topic-rail/4/0", "directions": ["right"]},
            {"key": "dead-letter/5/0", "directions": ["down"]}, {"key": "state-project/5/1", "directions": ["down"]},
        ],
    },
    12: {
        "body_primitive": "incident-pulse-rail-or-telemetry-export-rail", "signature_primitive": "ecg-head-or-telemetry-export-packet",
        "dash_pattern": [12, 31], "dash_period": 43, "step": 5, "body_opacity": 0.84,
        "maximum_live_width": 2.2, "endpoint_clearance": 8,
        "phase_policy": "motionStage * 5 mod 43",
        "expected_initial_phases": [5, 10, 15, 20],
        "resolved_widths": [2.2] * 4,
        "geometry": {"critical": {"shape": "compact-ecg-head", "stroke_width": 1.6}, "telemetry": {"shape": "cyan-three-dot-export-packet", "dot_count": 3, "dot_diameter": 4}},
        "trace_reveal": {"primitive": "trace-span-reveal", "span_ids": ["span-root", "span-api", "span-checkout", "span-payment"], "rendered_frames": [[24, 27], [27, 30], [30, 33], [33, 36]], "source_geometry_mutated": False},
        "scanner": {"primitive": "waterfall-scanner", "width": 2, "tail_width": 12, "period_frames": 34, "movement": "horizontal-within-trace-plot"},
        "auxiliary": {"primitive": "checkout-degraded-halo", "node_id": "checkout-service", "period_frames": 18, "opacity_range": [0.10, 0.28], "movement": "opacity-only"},
        "direction_sentinels": [
            {"key": "critical-request/1/0", "directions": ["right"]}, {"key": "critical-request/2/0", "directions": ["right"]},
            {"key": "critical-request/3/0", "directions": ["right"]}, {"key": "telemetry-export/4/0", "directions": ["down"]},
        ],
    },
}


def _style_1_persistent_stream_contract(frame_count: int) -> dict[str, object]:
    reset_start = frame_count - len(RESET_OPACITY_SAMPLES)
    dash_period = 41
    dash_step = 6
    stream_interval_frame_count = frame_count - 36
    return {
        "primitive": "persistent-data-flow-stream",
        "packet_head_primitive": "persistent-data-flow-head",
        "stream_count": 8,
        "packet_head_count": 8,
        "roles": [entry["role"] for entry in DRAW_SCHEDULE],
        "rendered_frames": [36, frame_count - 1],
        "fade_in_frames": [36, 38],
        "fade_in_factors": [0.30, 0.65, 1.0],
        "full_opacity_frames": [38, reset_start - 1],
        "body": {
            "primitive": "persistent-data-flow-stream",
            "stroke_width": "min(4.0, max(3.0, source_stroke * 1.60))",
            "resolved_style_1_source_stroke_width": 2.4,
            "resolved_style_1_stroke_width": 3.84,
            "color": "#06b6d4",
            "opacity": 0.90,
            "dash_pattern": [16, 25],
            "linecap": "round",
            "linejoin": "round",
            "marker_free": True,
            "filter_free": True,
        },
        "packet_head": {
            "primitive": "persistent-data-flow-head",
            "stroke_width": 2.20,
            "color": "#e0f2fe",
            "opacity": 0.98,
            "dash_pattern": [6, 35],
            "dash_offset_from_body": -10,
            "linecap": "round",
            "linejoin": "round",
            "marker_free": True,
            "filter_free": True,
            "appended_immediately_after_body": True,
        },
        "dash_period": dash_period,
        "dash_offset_per_rendered_frame": -6.0,
        "travel_user_units_per_rendered_frame": dash_step,
        "travel_pixels_per_frame_at_960px": 6,
        "travel_pixels_per_second_at_960px_20fps": 120,
        "travel_pixels_per_frame_at_50_percent": 3,
        "travel_pixels_per_second_at_50_percent_20fps": 60,
        "phase_policy": "(motionStage * 7 + motionOrder * 3) mod 41",
        "expected_initial_phases": [7, 14, 21, 28, 31, 35, 1, 8],
        "period_step_coprime": math.gcd(dash_period, dash_step) == 1,
        "stream_interval_frame_count": stream_interval_frame_count,
        "phase_repeat_within_stream_interval": stream_interval_frame_count > dash_period,
        "direction": "source-to-target",
        "direction_sentinels": {
            "ingress": ["right"],
            "resolve": ["left"],
            "memory-write": ["down", "left", "down"],
        },
        "travel_easing": "linear",
        "minimum_review_scale": "50%",
        "reset_range": [reset_start, frame_count - 1],
        "reset_opacity_samples": RESET_OPACITY_SAMPLES,
        "reset_behavior": "all eight body/head pairs keep advancing while topology, labels, and both flow layers fade together",
    }


def _style_2_persistent_stream_contract(frame_count: int) -> dict[str, object]:
    reset_start = frame_count - len(RESET_OPACITY_SAMPLES)
    dash_period = 41
    dash_step = 6
    stream_interval_frame_count = frame_count - 36
    return {
        "primitive": "terminal-evidence-stream",
        "packet_head_primitive": "terminal-command-head",
        "stream_count": 8,
        "packet_head_count": 8,
        "route_keys": [
            {"role": role, "order": order}
            for role, order in STYLE_SCENE_CONTRACTS[2]["schedule_keys"]
        ],
        "roles": [entry["role"] for entry in STYLE_2_DRAW_SCHEDULE],
        "rendered_frames": [36, frame_count - 1],
        "fade_in_frames": [36, 38],
        "fade_in_factors": [0.30, 0.65, 1.0],
        "full_opacity_frames": [38, reset_start - 1],
        "body": {
            "primitive": "terminal-evidence-stream",
            "stroke_width": "min(3.8, max(3.0, source_stroke * 1.50))",
            "resolved_style_2_source_stroke_width": 2.3,
            "resolved_style_2_stroke_width": 3.45,
            "color": "inherit-source-stroke",
            "source_colors_in_schedule_order": [
                "#a855f7",
                "#a855f7",
                "#38bdf8",
                "#38bdf8",
                "#22c55e",
                "#fb7185",
                "#fb7185",
                "#f97316",
            ],
            "semantic_colors": {
                "control": "#a855f7",
                "tool_read": "#38bdf8",
                "index_write": "#22c55e",
                "grounding_data": "#fb7185",
                "answer": "#f97316",
            },
            "opacity": 0.94,
            "dash_pattern": [15, 26],
            "linecap": "round",
            "linejoin": "round",
            "marker_free": True,
            "filter_free": True,
        },
        "packet_head": {
            "primitive": "terminal-command-head",
            "stroke_width": 2.00,
            "color": "#f8fafc",
            "opacity": 1.00,
            "dash_pattern": [5, 36],
            "dash_offset_from_body": -10,
            "linecap": "round",
            "linejoin": "round",
            "marker_free": True,
            "filter_free": True,
            "appended_immediately_after_body": True,
        },
        "dash_period": dash_period,
        "dash_offset_per_rendered_frame": -6.0,
        "travel_user_units_per_rendered_frame": dash_step,
        "travel_pixels_per_frame_at_960px": 6,
        "travel_pixels_per_second_at_960px_20fps": 120,
        "travel_pixels_per_frame_at_50_percent": 3,
        "travel_pixels_per_second_at_50_percent_20fps": 60,
        "phase_policy": "(motionStage * 6 + motionOrder * 3) mod 41",
        "expected_initial_phases": [6, 12, 18, 24, 30, 36, 39, 1],
        "period_step_coprime": math.gcd(dash_period, dash_step) == 1,
        "stream_interval_frame_count": stream_interval_frame_count,
        "phase_repeat_within_stream_interval": stream_interval_frame_count > dash_period,
        "direction": "source-to-target",
        "direction_sentinels": {
            "ingress/0": ["right"],
            "delegate/0": ["down", "left", "down"],
            "tool-call/0": ["right"],
            "inspect/0": ["down"],
            "index/0": ["right", "up"],
            "grounding/0": ["up", "left", "up"],
            "grounding/1": ["right"],
            "answer/0": ["right"],
        },
        "travel_easing": "linear",
        "minimum_review_scale": "50%",
        "reset_range": [reset_start, frame_count - 1],
        "reset_opacity_samples": RESET_OPACITY_SAMPLES,
        "reset_behavior": "all eight body/head pairs keep advancing while topology, labels, cursor, and both flow layers fade together",
    }


def _style_3_persistent_stream_contract(frame_count: int) -> dict[str, object]:
    reset_start = frame_count - len(RESET_OPACITY_SAMPLES)
    dash_period = 43
    dash_step = 6
    stream_interval_frame_count = frame_count - 36
    return {
        "primitive": "blueprint-distribution-wave",
        "registration_bead_primitive": "blueprint-registration-bead",
        "stream_count": 10,
        "registration_bead_count": 10,
        "route_keys": [
            {"role": role, "order": order}
            for role, order in STYLE_SCENE_CONTRACTS[3]["schedule_keys"]
        ],
        "roles": [entry["role"] for entry in STYLE_3_DRAW_SCHEDULE],
        "rendered_frames": [36, frame_count - 1],
        "fade_in_frames": [36, 38],
        "fade_in_factors": [0.30, 0.65, 1.0],
        "full_opacity_frames": [38, reset_start - 1],
        "body": {
            "primitive": "blueprint-distribution-wave",
            "stroke_width": "min(3.4, max(2.8, source_stroke * 1.40))",
            "resolved_style_3_source_stroke_width": 2.1,
            "resolved_style_3_stroke_width": 2.94,
            "resolved_style_3_stroke_width_at_50_percent": 1.47,
            "color": "inherit-source-stroke",
            "source_colors_in_schedule_order": [
                "#38bdf8",
                "#67e8f9",
                "#38bdf8",
                "#38bdf8",
                "#38bdf8",
                "#fde047",
                "#fde047",
                "#fde047",
                "#fb7185",
                "#fb7185",
            ],
            "opacity": 0.92,
            "dash_pattern": [12, 31],
            "linecap": "round",
            "linejoin": "round",
            "marker_free": True,
            "filter_free": True,
        },
        "registration_bead": {
            "primitive": "blueprint-registration-bead",
            "shape": "circle",
            "radius": 3.0,
            "diameter_at_960px": 6,
            "diameter_at_50_percent": 3,
            "fill": "#e0f2fe",
            "stroke": "inherit-source-stroke",
            "stroke_width": 1.2,
            "opacity": 0.98,
            "initial_path_distance": "stage-locked-phase",
            "path_advance_per_rendered_frame": 6.0,
            "direction": "source-to-target",
            "wrap": "target-end-to-source-start",
            "animated_attributes": ["cx", "cy", "opacity"],
            "marker_free": True,
            "filter_free": True,
        },
        "dash_period": dash_period,
        "dash_offset_per_rendered_frame": -6.0,
        "bead_advance_per_rendered_frame": 6.0,
        "travel_user_units_per_rendered_frame": dash_step,
        "travel_pixels_per_frame_at_960px": 6,
        "travel_pixels_per_second_at_960px_20fps": 120,
        "travel_pixels_per_frame_at_50_percent": 3,
        "travel_pixels_per_second_at_50_percent_20fps": 60,
        "phase_policy": "(motionStage * 7 + motionOrder * 0) mod 43",
        "expected_initial_phases": [7, 14, 21, 21, 21, 28, 28, 28, 35, 42],
        "stage_locks": {
            "fanout": {"orders": [0, 1, 2], "phase": 21},
            "data-write": {"orders": [0, 1, 2], "phase": 28, "equal_length_paths": True},
        },
        "period_step_coprime": math.gcd(dash_period, dash_step) == 1,
        "stream_interval_frame_count": stream_interval_frame_count,
        "phase_repeat_within_stream_interval": stream_interval_frame_count > dash_period,
        "direction": "source-to-target",
        "direction_sentinels": {
            "ingress/0": ["right"],
            "policy/0": ["right"],
            "fanout/0": ["down", "left", "down"],
            "fanout/1": ["down"],
            "fanout/2": ["down", "right", "down"],
            "data-write/0": ["down"],
            "data-write/1": ["down"],
            "data-write/2": ["down"],
            "event/0": ["right"],
            "telemetry/0": ["down"],
        },
        "travel_easing": "linear",
        "minimum_review_scale": "50%",
        "reset_range": [reset_start, frame_count - 1],
        "reset_opacity_samples": RESET_OPACITY_SAMPLES,
        "reset_behavior": (
            "all ten bodies and registration beads keep advancing while topology, labels, "
            "and both Blueprint flow layers fade together"
        ),
    }


def _style_4_persistent_stream_contract(frame_count: int) -> dict[str, object]:
    reset_start = frame_count - len(RESET_OPACITY_SAMPLES)
    dash_period = 47
    dash_step = 6
    stream_interval_frame_count = frame_count - 36
    semantic_colors = [
        "#3b82f6",
        "#3b82f6",
        "#7c3aed",
        "#059669",
        "#ea580c",
        "#ea580c",
    ]
    progress_vector = [0.08, 0.22, 0.36, 0.50, 0.64, 0.78]
    return {
        "primitive": "notion-memory-rail",
        "memory_card_primitive": "notion-memory-card",
        "stream_count": 6,
        "memory_card_count": 6,
        "route_keys": [
            {"role": role, "order": order}
            for role, order in STYLE_SCENE_CONTRACTS[4]["schedule_keys"]
        ],
        "roles": [entry["role"] for entry in STYLE_4_DRAW_SCHEDULE],
        "rendered_frames": [36, frame_count - 1],
        "fade_in_frames": [36, 38],
        "fade_in_factors": [0.30, 0.65, 1.0],
        "full_opacity_frames": [38, reset_start - 1],
        "body": {
            "primitive": "notion-memory-rail",
            "stroke_width": "min(3.0, max(2.4, source_stroke * 1.50))",
            "resolved_style_4_source_stroke_width": 1.8,
            "resolved_style_4_stroke_width": 2.70,
            "resolved_style_4_stroke_width_at_50_percent": 1.35,
            "color": "semantic-memory-destination",
            "source_colors_in_schedule_order": ["#3b82f6"] * 6,
            "semantic_colors_in_schedule_order": semantic_colors,
            "semantic_color_meanings": {
                "active_context": "#3b82f6",
                "procedural_memory": "#7c3aed",
                "episodic_memory": "#059669",
                "semantic_memory": "#ea580c",
            },
            "opacity": 0.88,
            "dash_pattern": [12, 35],
            "linecap": "round",
            "linejoin": "round",
            "marker_free": True,
            "filter_free": True,
            "appended_below_labels_and_nodes": True,
        },
        "memory_card": {
            "primitive": "notion-memory-card",
            "shape": "group",
            "outer_rect": {
                "x": -7,
                "y": -5,
                "width": 14,
                "height": 10,
                "rx": 2,
                "fill": "#ffffff",
                "stroke": "semantic-memory-destination",
                "stroke_width": 1.4,
            },
            "ink_lines": [
                {"x1": -4.5, "y1": -2, "x2": 4, "y2": -2},
                {"x1": -4.5, "y1": 2, "x2": 0.5, "y2": 2},
            ],
            "ink_stroke": "semantic-memory-destination",
            "ink_stroke_width": 2.0,
            "ink_linecap": "butt",
            "ink_shape_rendering": "crispEdges",
            "opacity": 0.98,
            "semantic_colors_in_schedule_order": semantic_colors,
            "initial_normalized_progress_by_stage": progress_vector,
            "initial_path_distance": "8 + progress * (pathLength - 16)",
            "endpoint_clearance": 8,
            "path_advance_per_rendered_frame": 6.0,
            "direction": "source-to-target",
            "wrap": "target-clearance-to-source-clearance",
            "tangent_rotations_in_schedule_order": [0, 0, 0, 90, 0, -90],
            "animated_attributes": ["transform", "opacity"],
            "marker_free": True,
            "filter_free": True,
            "shadow_free": True,
            "appended_below_labels_and_nodes": True,
        },
        "dash_period": dash_period,
        "dash_offset_per_rendered_frame": -6.0,
        "card_advance_per_rendered_frame": 6.0,
        "travel_user_units_per_rendered_frame": dash_step,
        "travel_pixels_per_frame_at_960px": 6,
        "travel_pixels_per_second_at_960px_20fps": 120,
        "travel_pixels_per_frame_at_50_percent": 3,
        "travel_pixels_per_second_at_50_percent_20fps": 60,
        "phase_policy": "(motionStage * 7 + motionOrder * 0) mod 47",
        "expected_initial_phases": [7, 14, 21, 28, 35, 42],
        "initial_normalized_progress_by_stage": progress_vector,
        "period_step_coprime": math.gcd(dash_period, dash_step) == 1,
        "stream_interval_frame_count": stream_interval_frame_count,
        "phase_repeat_within_stream_interval": stream_interval_frame_count > dash_period,
        "direction": "source-to-target",
        "direction_sentinels": {
            "sample/0": {"directions": ["right"], "tangent_rotation": 0},
            "attend/0": {"directions": ["right"], "tangent_rotation": 0},
            "invoke/0": {"directions": ["right"], "tangent_rotation": 0},
            "remember/0": {"directions": ["down"], "tangent_rotation": 90},
            "consolidate/0": {"directions": ["right"], "tangent_rotation": 0},
            "recall/0": {"directions": ["up"], "tangent_rotation": -90},
        },
        "travel_easing": "linear",
        "minimum_review_scale": "50%",
        "reset_range": [reset_start, frame_count - 1],
        "reset_opacity_samples": RESET_OPACITY_SAMPLES,
        "reset_behavior": (
            "all six rails and six memory cards keep advancing while topology, labels, "
            "rails, and cards fade together"
        ),
    }


def _terminal_signature_contract(frame_count: int) -> dict[str, object]:
    reset_start = frame_count - len(RESET_OPACITY_SAMPLES)
    return {
        "primitive": "terminal-prompt-cursor",
        "count": 1,
        "node_id": "terminal",
        "source_text": "_",
        "source_text_hidden": False,
        "source_text_mutated": False,
        "geometry": "2.2px-high rectangle derived from underscore getBBox",
        "height": 2.2,
        "fill": "#a7f3d0",
        "marker_free": True,
        "filter_free": True,
        "movement": "opacity-only",
        "visible_after_route": {"role": "tool-call", "order": 0, "settled_frame": 16},
        "cadence_frames": [16, reset_start - 1],
        "period_frames": 16,
        "bright_frames_per_period": 8,
        "absent_frames_per_period": 8,
        "bright_opacity": 0.95,
        "reset_range": [reset_start, frame_count - 1],
        "reset_behavior": "bright opacity multiplied by shared reset opacity",
    }


def _specialized_persistent_stream_contract(frame_count: int, style_id: int) -> dict[str, object]:
    spec = STYLE_SPECIALIZED_LIVE_SPECS[style_id]
    scene = STYLE_SCENE_CONTRACTS[style_id]
    reset_start = frame_count - len(RESET_OPACITY_SAMPLES)
    step = int(spec["step"])
    stage_aware = bool(scene.get("stage_aware_schedule_key"))
    route_keys = []
    for key in scene["schedule_keys"]:
        if stage_aware:
            role, stage, order = key
            route_keys.append({"role": role, "stage": stage, "order": order})
        else:
            role, order = key
            route_keys.append({"role": role, "order": order})
    contract: dict[str, object] = {
        "primitive": spec["body_primitive"],
        "signature_primitive": spec["signature_primitive"],
        "stream_count": len(scene["schedule_keys"]),
        "signature_count": len(scene["schedule_keys"]),
        "route_keys": route_keys,
        "rendered_frames": [36, frame_count - 1],
        "fade_in_frames": [36, 38],
        "fade_in_factors": [0.30, 0.65, 1.0],
        "full_opacity_frames": [38, reset_start - 1],
        "body": {
            "primitive": spec["body_primitive"],
            "stroke_width": "min(maximum_live_width, source_stroke)",
            "maximum_live_width": spec["maximum_live_width"],
            "resolved_widths_in_schedule_order": spec["resolved_widths"],
            "color": "inherit-source-stroke",
            "opacity": spec["body_opacity"],
            "dash_pattern": spec["dash_pattern"],
            "linecap": "round",
            "linejoin": "round",
            "marker_free": True,
            "filter_free": True,
            "appended_below_labels_and_nodes": True,
        },
        "signature": {
            "primitive": spec["signature_primitive"],
            "geometry": spec["geometry"],
            "endpoint_clearance": spec["endpoint_clearance"],
            "path_advance_per_rendered_frame": step,
            "direction": "source-to-target",
            "wrap": "target-clearance-to-source-clearance",
            "tangent_aware_rotation": True,
            "animated_attributes": ["transform", "opacity"],
            "appended_below_labels_and_nodes": True,
        },
        "dash_period": spec["dash_period"],
        "dash_offset_per_rendered_frame": -step,
        "signature_advance_per_rendered_frame": step,
        "travel_user_units_per_rendered_frame": step,
        "travel_pixels_per_frame_at_100_percent": step,
        "travel_pixels_per_frame_at_50_percent": step / 2,
        "phase_policy": spec["phase_policy"],
        "expected_initial_phases": spec["expected_initial_phases"],
        "period_step_coprime": math.gcd(int(spec["dash_period"]), step) == 1,
        "direction": "source-to-target",
        "direction_sentinels": spec["direction_sentinels"],
        "travel_easing": "linear",
        "minimum_review_scale": "50%",
        "reset_range": [reset_start, frame_count - 1],
        "reset_opacity_samples": RESET_OPACITY_SAMPLES,
        "reset_behavior": "live rails, signatures, and scene auxiliaries keep advancing while the shared reset opacity fades to 0.03",
    }
    for optional in ("auxiliary", "trace_reveal", "scanner"):
        if optional in spec:
            contract[optional] = spec[optional]
    return contract


def _persistent_stream_contract(frame_count: int, style_id: int = 1) -> dict[str, object]:
    if style_id == 1:
        return _style_1_persistent_stream_contract(frame_count)
    if style_id == 2:
        return _style_2_persistent_stream_contract(frame_count)
    if style_id == 3:
        return _style_3_persistent_stream_contract(frame_count)
    if style_id == 4:
        return _style_4_persistent_stream_contract(frame_count)
    if style_id in STYLE_SPECIALIZED_LIVE_SPECS:
        return _specialized_persistent_stream_contract(frame_count, style_id)
    raise ValueError(f"MOTION_STYLE_REVIEW: Style {style_id} has no reviewed stream contract")


def _draw_on_contract(frame_count: int, style_id: int = 1) -> dict[str, object]:
    reset_start = frame_count - len(RESET_OPACITY_SAMPLES)
    contract: dict[str, object] = {
        "primitive": "connector-draw-on-with-persistent-data-flow",
        "empty_opening_frame": 0,
        "draw_schedule": STYLE_SCENE_CONTRACTS[style_id]["draw_schedule"],
        "reset_range": [reset_start, frame_count - 1],
        "reset_opacity_samples": RESET_OPACITY_SAMPLES,
        "connectors_visible_at_opening": False,
        "nodes_visible_every_frame": True,
        "topology_draw_on": True,
        "settled_topology_dynamic": True,
        "source_edges_hidden_by_transient_css": True,
        "draw_easing": "linear",
        "settled_markers": "original marker appears only after route arrival",
        "persistent_data_flow": _persistent_stream_contract(frame_count, style_id),
        "route_label_opacity_states": STYLE_SCENE_CONTRACTS[style_id]["route_label_count"],
        "text_geometry_motion": 0,
        "maximum_concurrent_draws": STYLE_SCENE_CONTRACTS[style_id].get("maximum_concurrent_draws", 2),
        "forbidden": ["node-motion", "text-motion", "halo", "ripple", "zoom", "breathing"],
    }
    if style_id == 2:
        contract["schedule_key"] = "(data-motion-role, data-motion-order)"
        contract["terminal_signature"] = _terminal_signature_contract(frame_count)
        contract["forbidden"] = [
            "node-motion",
            "text-motion",
            "terminal-text-typing",
            "scan-line",
            "halo",
            "ripple",
            "zoom",
            "camera-motion",
            "animated-background",
            "breathing",
        ]
    elif style_id == 3:
        contract["schedule_key"] = "(data-motion-role, data-motion-order)"
        contract["maximum_concurrent_draws"] = 2
        contract["forbidden"] = [
            "node-motion",
            "text-motion",
            "glow",
            "blur",
            "shadow",
            "scan-line",
            "halo",
            "ripple",
            "terminal-cursor",
            "camera-motion",
            "animated-background",
            "breathing",
        ]
    elif style_id == 4:
        contract["schedule_key"] = "(data-motion-role, data-motion-order)"
        contract["maximum_concurrent_draws"] = 1
        contract["forbidden"] = [
            "node-motion",
            "text-motion",
            "glow",
            "blur",
            "shadow",
            "scan-line",
            "halo",
            "ripple",
            "terminal-cursor",
            "circular-bead",
            "camera-motion",
            "animated-background",
            "breathing",
        ]
    elif style_id >= 5:
        contract["schedule_key"] = (
            "(data-motion-role, data-motion-stage, data-motion-order)"
            if STYLE_SCENE_CONTRACTS[style_id].get("stage_aware_schedule_key")
            else "(data-motion-role, data-motion-order)"
        )
        contract["forbidden"] = [
            "source-node-motion",
            "source-text-motion",
            "source-geometry-mutation",
            "source-marker-mutation",
            "camera-motion",
            "animated-background",
        ]
    return contract


PERSISTENT_STREAM_CONTRACT = _persistent_stream_contract(DEFAULT_MOTION_FRAME_COUNT)
DRAW_ON_CONTRACT = _draw_on_contract(DEFAULT_MOTION_FRAME_COUNT)
STYLE_2_PERSISTENT_STREAM_CONTRACT = _persistent_stream_contract(DEFAULT_MOTION_FRAME_COUNT, 2)
STYLE_2_DRAW_ON_CONTRACT = _draw_on_contract(DEFAULT_MOTION_FRAME_COUNT, 2)
STYLE_3_PERSISTENT_STREAM_CONTRACT = _persistent_stream_contract(DEFAULT_MOTION_FRAME_COUNT, 3)
STYLE_3_DRAW_ON_CONTRACT = _draw_on_contract(DEFAULT_MOTION_FRAME_COUNT, 3)
STYLE_4_PERSISTENT_STREAM_CONTRACT = _persistent_stream_contract(DEFAULT_MOTION_FRAME_COUNT, 4)
STYLE_4_DRAW_ON_CONTRACT = _draw_on_contract(DEFAULT_MOTION_FRAME_COUNT, 4)
STYLE_5_DRAW_ON_CONTRACT = _draw_on_contract(DEFAULT_MOTION_FRAME_COUNT, 5)
STYLE_6_DRAW_ON_CONTRACT = _draw_on_contract(DEFAULT_MOTION_FRAME_COUNT, 6)
STYLE_7_DRAW_ON_CONTRACT = _draw_on_contract(DEFAULT_MOTION_FRAME_COUNT, 7)
STYLE_8_DRAW_ON_CONTRACT = _draw_on_contract(DEFAULT_MOTION_FRAME_COUNT, 8)
STYLE_9_DRAW_ON_CONTRACT = _draw_on_contract(DEFAULT_MOTION_FRAME_COUNT, 9)
STYLE_10_DRAW_ON_CONTRACT = _draw_on_contract(DEFAULT_MOTION_FRAME_COUNT, 10)
STYLE_11_DRAW_ON_CONTRACT = _draw_on_contract(DEFAULT_MOTION_FRAME_COUNT, 11)
STYLE_12_DRAW_ON_CONTRACT = _draw_on_contract(DEFAULT_MOTION_FRAME_COUNT, 12)
TERMINAL_SIGNATURE_CONTRACT = _terminal_signature_contract(DEFAULT_MOTION_FRAME_COUNT)


def _motion_grammar(duration: float, fps: int, frame_count: int, style_id: int = 1) -> dict[str, object]:
    reset_start = frame_count - len(RESET_OPACITY_SAMPLES)
    phase_ranges = {
        "empty": (0, 0),
        "draw": (1, 36),
        "stream": (36, frame_count - 1),
        "full_opacity": (38, reset_start - 1),
        "reset": (reset_start, frame_count - 1),
    }
    phases: dict[str, object] = {}
    for name, (start_frame, end_frame) in phase_ranges.items():
        phases[name] = {
            "frames": [start_frame, end_frame],
            "seconds": [
                round((start_frame + 0.5) / fps, 6),
                round((end_frame + 0.5) / fps, 6),
            ],
        }
    return {
        "version": MOTION_GRAMMAR_VERSION,
        "phases": phases,
        "curves": MOTION_CURVES,
        "draw_on": _draw_on_contract(frame_count, style_id),
        "frame_duration_ms": round(1000 / fps, 6),
        "sampling": "uniform-frame-centers",
        "sample_index_expression": "time * fps - 0.5",
        "duration_seconds": duration,
    }


def _timing_revision(duration: float, fps: int, frame_count: int) -> dict[str, object]:
    if (
        fps == DEFAULT_MOTION_FPS
        and frame_count == DEFAULT_MOTION_FRAME_COUNT
        and math.isclose(duration, DEFAULT_MOTION_DURATION, rel_tol=0, abs_tol=1e-9)
    ):
        return {
            "id": TIMING_REVISION_ID,
            "status": "user-approved",
            "approved_at": TIMING_REVISION_APPROVED_AT,
            "only_pending_item": False,
            "baseline": {
                "duration_seconds": APPROVED_BASELINE_DURATION,
                "frame_count": APPROVED_BASELINE_FRAME_COUNT,
            },
            "candidate": {
                "duration_seconds": DEFAULT_MOTION_DURATION,
                "frame_count": DEFAULT_MOTION_FRAME_COUNT,
                "added_full_opacity_frames": [70, 109],
            },
        }
    if (
        fps == DEFAULT_MOTION_FPS
        and frame_count == APPROVED_BASELINE_FRAME_COUNT
        and math.isclose(duration, APPROVED_BASELINE_DURATION, rel_tol=0, abs_tol=1e-9)
    ):
        return {
            "id": "approved-3.75s-baseline",
            "status": "user-approved",
            "only_pending_item": False,
        }
    return {
        "id": "explicit-custom-timing",
        "status": "custom_timing",
        "only_pending_item": False,
    }


def _sha256(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def _sha256_bytes(value: bytes) -> str:
    return hashlib.sha256(value).hexdigest()


def _elements(root: ET.Element, role: str) -> list[ET.Element]:
    return [element for element in root.iter() if element.get("data-graph-role") == role]


def _parse_viewbox(root: ET.Element) -> tuple[float, float, float, float]:
    raw = root.get("viewBox") or root.get("viewbox")
    if not raw:
        raise ValueError("MOTION_VIEWBOX: generated SVG must define a viewBox")
    try:
        values = tuple(float(value) for value in raw.replace(",", " ").split())
    except ValueError as error:
        raise ValueError("MOTION_VIEWBOX: viewBox values must be numeric") from error
    if len(values) != 4 or values[2] <= 0 or values[3] <= 0:
        raise ValueError("MOTION_VIEWBOX: viewBox must contain four positive dimensions")
    return values  # type: ignore[return-value]


def _validate_settings(duration: float, fps: int, width: int) -> int:
    if isinstance(duration, bool) or not isinstance(duration, (int, float)) or not math.isfinite(duration):
        raise ValueError("MOTION_DURATION: duration must be a finite number")
    if duration < 0.5 or duration > 20:
        raise ValueError("MOTION_DURATION: duration must be between 0.5 and 20 seconds")
    if isinstance(fps, bool) or not isinstance(fps, int):
        raise ValueError("MOTION_FPS: fps must be a whole number")
    if fps < 1 or fps > 25:
        raise ValueError("MOTION_FPS: fps must be between 1 and 25")
    if isinstance(width, bool) or not isinstance(width, int):
        raise ValueError("MOTION_WIDTH: width must be a whole number")
    if width < 320 or width > 4096:
        raise ValueError("MOTION_WIDTH: width must be between 320 and 4096 pixels")
    requested_frames = duration * fps
    frame_count = round(requested_frames)
    if abs(requested_frames - frame_count) > 1e-9:
        raise ValueError("MOTION_TIMELINE: duration multiplied by fps must be a whole number of frames")
    if frame_count < MINIMUM_MOTION_FRAME_COUNT:
        raise ValueError(
            f"MOTION_TIMELINE: semantic GIF motion requires at least {MINIMUM_MOTION_FRAME_COUNT} rendered frames"
        )
    if frame_count > 500:
        raise ValueError("MOTION_FRAME_BUDGET: animation may not exceed 500 frames")
    return frame_count


def _validate_render_budget(width: int, height: int, frame_count: int) -> int:
    rendered_pixels = width * height * frame_count
    if rendered_pixels > MAX_RENDERED_PIXELS:
        raise ValueError(
            "MOTION_TOTAL_PIXEL_BUDGET: output dimensions multiplied by frame count may not exceed 600 million pixels"
        )
    return rendered_pixels


def _read_input(path: Path) -> bytes:
    if not path.is_file():
        raise ValueError(f"MOTION_INPUT: input does not exist: {path}")
    size = path.stat().st_size
    if size <= 0:
        raise ValueError("MOTION_INPUT: input is empty")
    if size > MAX_INPUT_BYTES:
        raise ValueError("MOTION_INPUT_SIZE: input may not exceed 20 MiB")
    return path.read_bytes()


def _input_kind(path: Path, source: bytes) -> str:
    if path.suffix.lower() == ".svg" and b"<svg" in source[:4096].lower():
        return "svg"
    raise ValueError("MOTION_INPUT_TYPE: input must be a generated .svg file")


def _validate_semantics(style_id: int, edges: list[ET.Element], nodes: list[ET.Element]) -> dict[str, object]:
    if style_id not in REVIEWED_STYLE_IDS:
        raise ValueError(f"MOTION_STYLE: Style {style_id} does not have a motion preset yet")
    if not edges or not nodes:
        raise ValueError(f"MOTION_METADATA: Style {style_id} requires semantic nodes and edges")
    incomplete = [
        edge.get("data-edge-id", "unknown")
        for edge in edges
        if not edge.get("data-source") or not edge.get("data-target")
    ]
    if incomplete:
        raise ValueError(
            "MOTION_METADATA: semantic edges require source and target metadata: "
            + ", ".join(incomplete)
        )
    explicit: list[ET.Element] = []
    partial: list[str] = []
    unknown_roles: set[str] = set()
    supported_roles = STYLE_MOTION_ROLES[style_id]
    for edge in edges:
        values = (
            edge.get("data-motion-role", ""),
            edge.get("data-motion-stage", ""),
            edge.get("data-motion-order", ""),
        )
        populated = sum(bool(value) for value in values)
        if 0 < populated < 3:
            partial.append(edge.get("data-edge-id", "unknown"))
        elif populated == 3:
            explicit.append(edge)
            if values[0] not in supported_roles:
                unknown_roles.add(values[0])
    if partial:
        raise ValueError(
            "MOTION_METADATA: motion role, stage, and order must be provided together: " + ", ".join(partial)
        )
    if unknown_roles:
        raise ValueError(
            f"MOTION_METADATA: Style {style_id} has unsupported motion roles: "
            + ", ".join(sorted(unknown_roles))
        )
    if not explicit:
        metadata_mode = "topology-fallback"
    elif len(explicit) == len(edges):
        metadata_mode = "explicit"
    else:
        metadata_mode = "hybrid"
    result: dict[str, object] = {
        "edges": len(edges),
        "nodes": len(nodes),
        "motion_metadata": metadata_mode,
        "staged_edges": len(explicit),
        "edges_without_flow": sum(not edge.get("data-flow") for edge in edges),
    }
    if style_id == 10:
        flows = [edge.get("data-flow", "") for edge in edges]
        missing = sorted({"read", "write", "async"} - set(flows))
        if missing:
            raise ValueError(f'MOTION_METADATA: Style 10 is missing flow metadata: {", ".join(missing)}')
        result["flows"] = {flow: flows.count(flow) for flow in sorted(set(flows))}
    elif style_id == 11:
        stations = [node for node in nodes if node.get("data-station-order", "")]
        rails = [edge for edge in edges if edge.get("data-edge-kind") == "rail"]
        branch_kinds = {edge.get("data-edge-kind") for edge in edges}
        if len(stations) < 2 or not rails:
            raise ValueError("MOTION_METADATA: Style 11 requires ordered stations and rail edges")
        if not {"dead_letter", "state"}.issubset(branch_kinds):
            raise ValueError("MOTION_METADATA: Style 11 requires dead-letter and state branches")
        orders = sorted(int(node.get("data-station-order", "-1")) for node in stations)
        if orders != list(range(len(orders))):
            raise ValueError("MOTION_METADATA: Style 11 station order must be contiguous from zero")
        result.update({"stations": len(stations), "rails": len(rails), "branches": 2})
    elif style_id == 12:
        critical = [edge for edge in edges if edge.get("data-critical") == "true"]
        spans = [node for node in nodes if node.get("data-span-id")]
        if not critical or any(not edge.get("data-critical-hop") for edge in critical):
            raise ValueError("MOTION_METADATA: Style 12 critical edges require hop metadata")
        if not spans or any(not node.get("data-duration-ms") for node in spans):
            raise ValueError("MOTION_METADATA: Style 12 trace spans require timing metadata")
        result.update({"critical_hops": len(critical), "trace_spans": len(spans)})
    return result


def _validate_scene_schedule(style_id: int, edges: list[ET.Element]) -> list[dict[str, object]]:
    scene_contract = STYLE_SCENE_CONTRACTS[style_id]
    expected_keys = list(scene_contract["schedule_keys"])
    expected_stages = list(scene_contract["expected_stages"])
    stage_aware = bool(scene_contract.get("stage_aware_schedule_key"))
    keyed_edges: dict[tuple[object, ...], list[ET.Element]] = {}
    for edge in edges:
        role = edge.get("data-motion-role", "")
        try:
            stage = int(edge.get("data-motion-stage", ""))
            order = int(edge.get("data-motion-order", ""))
        except ValueError as error:
            raise ValueError(
                f'MOTION_METADATA: edge {edge.get("data-edge-id", "unknown")} has a non-integer motion stage or order'
            ) from error
        key: tuple[object, ...] = (role, stage, order) if stage_aware else (role, order)
        keyed_edges.setdefault(key, []).append(edge)

    actual_keys = set(keyed_edges)
    expected_key_set = set(expected_keys)
    duplicate_keys = [key for key, values in keyed_edges.items() if len(values) != 1]
    if len(edges) != len(expected_keys) or actual_keys != expected_key_set or duplicate_keys:
        missing = sorted(expected_key_set - actual_keys)
        unexpected = sorted(actual_keys - expected_key_set)
        raise ValueError(
            f"MOTION_METADATA: Style {style_id} schedule must resolve exactly one edge per "
            f"{'(role, stage, order)' if stage_aware else '(role, order)'}; "
            f"missing={missing}, unexpected={unexpected}, duplicate={sorted(duplicate_keys)}"
        )

    resolved: list[dict[str, object]] = []
    for expected_key, expected_stage in zip(expected_keys, expected_stages):
        if stage_aware:
            role, key_stage, order = expected_key
            if key_stage != expected_stage:
                raise ValueError(f"MOTION_CONTRACT: Style {style_id} stage-aware schedule is internally inconsistent")
        else:
            role, order = expected_key
        edge = keyed_edges[expected_key][0]
        actual_stage = int(edge.get("data-motion-stage", ""))
        if actual_stage != expected_stage:
            raise ValueError(
                f"MOTION_METADATA: Style {style_id} route {role}/{order} must use motion stage {expected_stage}"
            )
        resolved.append(
            {
                "edge_id": edge.get("data-edge-id", ""),
                "role": role,
                "stage": actual_stage,
                "order": order,
                "source_stroke": edge.get("stroke", ""),
            }
        )

    if style_id in {2, 3, 4}:
        expected_colors = _persistent_stream_contract(DEFAULT_MOTION_FRAME_COUNT, style_id)["body"][
            "source_colors_in_schedule_order"
        ]
        actual_colors = [entry["source_stroke"] for entry in resolved]
        if actual_colors != expected_colors:
            raise ValueError(
                f"MOTION_METADATA: Style {style_id} semantic route colors changed: "
                f"expected {expected_colors}, got {actual_colors}"
            )
    if style_id == 4:
        semantic_colors = _persistent_stream_contract(DEFAULT_MOTION_FRAME_COUNT, 4)["body"][
            "semantic_colors_in_schedule_order"
        ]
        for entry, semantic_color in zip(resolved, semantic_colors):
            entry["semantic_color"] = semantic_color
    return resolved


def build_motion_plan(
    svg_path: Path,
    preset: str = "auto",
    duration: float = DEFAULT_MOTION_DURATION,
    fps: int = DEFAULT_MOTION_FPS,
    width: Optional[int] = None,
) -> tuple[dict[str, object], object]:
    """Validate a semantic SVG and return its focused GIF motion plan."""

    source_bytes = _read_input(svg_path)
    _input_kind(svg_path, source_bytes)
    resolved_width = 960 if width is None else width
    frame_count = _validate_settings(duration, fps, resolved_width)
    try:
        source_text = source_bytes.decode("utf-8")
    except UnicodeDecodeError as error:
        raise ValueError("MOTION_INPUT: SVG must be UTF-8 encoded") from error
    safe_svg = sanitize_svg(source_text)
    root = ET.fromstring(safe_svg)
    if root.get("data-generator") != "fireworks-tech-graph":
        raise ValueError("MOTION_INPUT: only fireworks-tech-graph generated SVGs are supported")
    if root.get("data-semantic-valid") != "true":
        raise ValueError("MOTION_INPUT: SVG must carry a valid semantic contract")
    try:
        style_id = int(root.get("data-style-id", "0"))
    except ValueError as error:
        raise ValueError("MOTION_STYLE: SVG has an invalid style id") from error
    if style_id not in REVIEWED_STYLE_IDS:
        raise ValueError(
            f"MOTION_STYLE_REVIEW: Style {style_id} is unsupported; enabled styles are 1-12"
        )
    scene_contract = STYLE_SCENE_CONTRACTS[style_id]
    resolved_preset = STYLE_PRESETS.get(style_id) if preset == "auto" else preset
    if not resolved_preset or resolved_preset not in PRESET_STYLES:
        raise ValueError(f"MOTION_PRESET: unsupported preset: {preset}")
    expected_style = PRESET_STYLES[resolved_preset]
    if style_id != expected_style:
        raise ValueError(
            f"MOTION_PRESET: {resolved_preset} belongs to Style {expected_style}, input is Style {style_id}"
        )

    edges = _elements(root, "edge")
    nodes = _elements(root, "node")
    semantics = _validate_semantics(style_id, edges, nodes)
    if semantics.get("motion_metadata") != "explicit":
        raise ValueError("MOTION_METADATA: reviewed styles require explicit motion metadata on every edge")
    resolved_schedule = _validate_scene_schedule(style_id, edges)
    if style_id in set(range(2, 13)):
        semantics["schedule_key"] = "(data-motion-role, data-motion-order)"
        if scene_contract.get("stage_aware_schedule_key"):
            semantics["schedule_key"] = "(data-motion-role, data-motion-stage, data-motion-order)"
        semantics["resolved_schedule"] = resolved_schedule
    check_results: dict[str, object] = {}
    for check in CHECKS:
        passed, details = run_check(svg_path, check)
        check_results[check] = {"ok": passed, "details": details}
        if not passed:
            raise ValueError(f"MOTION_SOURCE_CHECK: {check} failed: {details}")

    viewbox = _parse_viewbox(root)
    height = max(1, round(resolved_width * viewbox[3] / viewbox[2]))
    if height > 4096 or resolved_width * height > 16_777_216:
        raise ValueError("MOTION_DIMENSIONS: output must stay within 4096px per side and 16 megapixels")
    rendered_pixels = _validate_render_budget(resolved_width, height, frame_count)
    timing_revision = _timing_revision(duration, fps, frame_count)
    style_contract: dict[str, object] = {
        "status": "user-approved",
        "scope": "signature-speed-path-geometry-and-construction-schedule",
        "source_policy": "semantic-schedule-and-geometry-not-exact-byte-hash",
    }
    if style_id >= 5:
        style_contract["approval_recorded_on"] = "2026-07-16"
    plan: dict[str, object] = {
        "ok": True,
        "dry_run": True,
        "source": str(svg_path),
        "source_sha256": _sha256_bytes(source_bytes),
        "review_reference_source_sha256": scene_contract.get("source_sha256"),
        "fixture_sha256": scene_contract.get("fixture_sha256"),
        "input_kind": "svg",
        "style_id": style_id,
        "visual_theme": root.get("data-visual-theme"),
        "semantic_profile": root.get("data-semantic-profile"),
        "motion_scene": root.get("data-motion-scene") or resolved_preset,
        "motion_class": "semantic",
        "preset": resolved_preset,
        "motion_grammar_version": MOTION_GRAMMAR_VERSION,
        "motion_grammar": _motion_grammar(duration, fps, frame_count, style_id),
        "scene_signature": SCENE_SIGNATURES[style_id],
        "review_status": timing_revision["status"],
        "style_contract_status": "user-approved",
        "style_contract": style_contract,
        "timing_revision": timing_revision,
        "animation_contract": _draw_on_contract(frame_count, style_id),
        "duration_seconds": duration,
        "fps": fps,
        "frame_count": frame_count,
        "width": resolved_width,
        "height": height,
        "rendered_pixels": rendered_pixels,
        "seamless_loop_contract": (
            "75 frames or fewer: every raster is unique; more than 75 frames: non-adjacent "
            "repeats are allowed within the full-opacity interval, plus the sole intentional "
            "reset-boundary exception at reset_start with opacity 1.00, with at least 75 unique "
            "rasters; seam MAD must not exceed p95 adjacent-frame MAD"
        ),
        "frame_uniqueness_contract": {
            "strict_all_unique_through_frame_count": APPROVED_BASELINE_FRAME_COUNT,
            "minimum_unique_rasters_for_long_timeline": APPROVED_BASELINE_FRAME_COUNT,
            "long_timeline_repeat_scope": [38, frame_count - len(RESET_OPACITY_SAMPLES) - 1],
            "intentional_reset_boundary_repeat_frame": (
                frame_count - len(RESET_OPACITY_SAMPLES)
            ),
            "intentional_reset_boundary_opacity": RESET_OPACITY_SAMPLES[0],
            "adjacent_duplicates_allowed": False,
            "opening_construction_and_reset_tail_globally_distinct": True,
        },
        "semantics": semantics,
        "source_checks": check_results,
    }
    return plan, safe_svg


def probe_motion_runtime() -> dict[str, object]:
    node = shutil.which("node")
    ffmpeg = shutil.which("ffmpeg")
    ffprobe = shutil.which("ffprobe")
    result: dict[str, object] = {
        "ok": False,
        "node": node,
        "ffmpeg": ffmpeg,
        "ffprobe": ffprobe,
        "worker": str(MOTION_WORKER),
    }
    if not node or not MOTION_WORKER.is_file():
        result["error"] = "Node.js or scripts/svg2gif.js is unavailable"
        return result
    try:
        process = subprocess.run(
            [node, str(MOTION_WORKER), "--probe"],
            text=True,
            capture_output=True,
            check=False,
            timeout=MOTION_TIMEOUTS["runtime_probe"],
        )
    except subprocess.TimeoutExpired:
        result["error"] = "MOTION_RUNTIME_TIMEOUT: renderer probe exceeded 15 seconds"
        return result
    try:
        worker = json.loads(process.stdout.strip().splitlines()[-1]) if process.stdout.strip() else {}
    except (IndexError, json.JSONDecodeError):
        worker = {"ok": False, "error": process.stderr.strip() or "motion runtime probe returned invalid JSON"}
    encoders: set[str] = set()
    encoder_error: Optional[str] = None
    if ffmpeg:
        try:
            encoder_process = subprocess.run(
                [ffmpeg, "-nostdin", "-hide_banner", "-encoders"],
                text=True,
                capture_output=True,
                check=False,
                timeout=MOTION_TIMEOUTS["runtime_probe"],
            )
            if encoder_process.returncode == 0:
                for line in encoder_process.stdout.splitlines():
                    match = re.match(r"^\s*[VASFXBD\.]{6}\s+([A-Za-z0-9_][^\s]*)", line)
                    if match:
                        encoders.add(match.group(1))
            else:
                encoder_error = encoder_process.stderr.strip() or "FFmpeg encoder discovery failed"
        except subprocess.TimeoutExpired:
            encoder_error = "MOTION_RUNTIME_TIMEOUT: FFmpeg encoder probe exceeded 15 seconds"
    result["renderer"] = worker
    result["encoders"] = sorted(encoders.intersection({"gif"}))
    result["format"] = {"gif": bool(ffmpeg and "gif" in encoders)}
    if encoder_error:
        result["encoder_probe_error"] = encoder_error
    result["ok"] = bool(
        ffmpeg
        and ffprobe
        and "gif" in encoders
        and process.returncode == 0
        and worker.get("ok")
    )
    if not result["ok"] and "error" not in result:
        result["error"] = (
            worker.get("error")
            or process.stderr.strip()
            or ("FFprobe is unavailable" if not ffprobe else "motion dependencies are incomplete")
        )
    return result


def _last_json_line(output: str, label: str) -> dict[str, object]:
    for line in reversed(output.splitlines()):
        try:
            value = json.loads(line)
        except json.JSONDecodeError:
            continue
        if isinstance(value, dict):
            return value
    raise RuntimeError(f"{label} did not return a JSON result")


def _stage_json(path: Path, value: dict[str, object]) -> Path:
    path.parent.mkdir(parents=True, exist_ok=True)
    if os.path.lexists(path) and path.is_dir():
        raise ValueError("MOTION_REPORT: report target must be a file path")
    temporary = path.with_name(f".{path.name}.{uuid.uuid4().hex}.tmp")
    try:
        temporary.write_text(json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
        return temporary
    except Exception:
        temporary.unlink(missing_ok=True)
        raise


def _commit_artifacts(staged_targets: list[tuple[Path, Path]]) -> None:
    """Install related artifacts together and restore every previous target on failure."""

    for staged, target in staged_targets:
        if not staged.is_file():
            raise RuntimeError(f"MOTION_COMMIT: staged artifact is unavailable: {staged}")
        target.parent.mkdir(parents=True, exist_ok=True)
        if os.path.lexists(target) and target.is_dir():
            raise ValueError(f"MOTION_COMMIT: artifact target must be a file path: {target}")

    backups: dict[Path, Optional[Path]] = {}
    installed: set[Path] = set()
    try:
        for staged, target in staged_targets:
            backup: Optional[Path] = None
            if os.path.lexists(target):
                backup = target.with_name(f".{target.name}.{uuid.uuid4().hex}.rollback")
                backups[target] = backup
                os.replace(target, backup)
            else:
                backups[target] = None
            os.replace(staged, target)
            installed.add(target)
    except Exception as error:
        rollback_errors: list[str] = []
        for _staged, target in reversed(staged_targets):
            backup = backups.get(target)
            try:
                if target in installed and os.path.lexists(target):
                    target.unlink()
                if backup is not None and os.path.lexists(backup):
                    os.replace(backup, target)
            except OSError as rollback_error:
                rollback_errors.append(f"{target}: {rollback_error}")
        if rollback_errors:
            raise RuntimeError(
                "MOTION_COMMIT_ROLLBACK: artifact commit failed and rollback was incomplete: "
                + "; ".join(rollback_errors)
            ) from error
        raise
    else:
        for backup in backups.values():
            if backup is not None:
                backup.unlink(missing_ok=True)
    finally:
        for staged, _target in staged_targets:
            staged.unlink(missing_ok=True)


def _probe_media(path: Path) -> dict[str, object]:
    ffprobe = shutil.which("ffprobe")
    if not ffprobe:
        raise RuntimeError("MOTION_MEDIA_PROBE: FFprobe is required to validate encoded motion")
    try:
        process = subprocess.run(
            [
                ffprobe,
                "-v",
                "error",
                "-count_frames",
                "-select_streams",
                "v:0",
                "-show_entries",
                "stream=codec_name,width,height,duration,nb_frames,nb_read_frames,r_frame_rate,pix_fmt:format=format_name,duration",
                "-of",
                "json",
                "-protocol_whitelist",
                "file,pipe",
                str(path.resolve()),
            ],
            text=True,
            capture_output=True,
            check=False,
            timeout=MOTION_TIMEOUTS["media_probe"],
        )
    except subprocess.TimeoutExpired as error:
        raise RuntimeError("MOTION_MEDIA_PROBE_TIMEOUT: FFprobe exceeded 30 seconds") from error
    if process.returncode != 0:
        return {"error": process.stderr.strip()}
    try:
        payload = json.loads(process.stdout)
        streams = payload.get("streams", [])
    except (AttributeError, json.JSONDecodeError):
        return {"error": "ffprobe returned invalid JSON"}
    if not streams:
        return {"error": "ffprobe found no video stream"}
    result = dict(streams[0])
    if isinstance(payload.get("format"), dict):
        result["container"] = payload["format"]
        if not result.get("duration") and payload["format"].get("duration"):
            result["duration"] = payload["format"]["duration"]
    return result


def _validate_encoded_gif(
    media_probe: dict[str, object],
    expected_width: int,
    expected_height: int,
    expected_frames: int,
    expected_duration: float,
) -> None:
    if media_probe.get("error"):
        raise RuntimeError(f'MOTION_MEDIA_PROBE: {media_probe["error"]}')
    if media_probe.get("codec_name") != "gif":
        raise RuntimeError(
            f'MOTION_CODEC: expected gif, encoded {media_probe.get("codec_name", "unknown")}'
        )
    if int(str(media_probe.get("width", -1))) != expected_width:
        raise RuntimeError("MOTION_DIMENSIONS: encoded media width differs from the motion plan")
    if int(str(media_probe.get("height", -1))) != expected_height:
        raise RuntimeError("MOTION_DIMENSIONS: encoded media height differs from the motion plan")
    encoded_frames = media_probe.get("nb_read_frames") or media_probe.get("nb_frames")
    if encoded_frames is None:
        raise RuntimeError("MOTION_FRAMES: media probe did not report an encoded frame count")
    encoded_frame_count = int(str(encoded_frames))
    if encoded_frame_count != expected_frames:
        raise RuntimeError("MOTION_FRAMES: encoded media frame count differs from the motion plan")
    if media_probe.get("duration") is None:
        raise RuntimeError("MOTION_DURATION: media probe did not report encoded duration")
    encoded_duration = float(str(media_probe["duration"]))
    if abs(encoded_duration - expected_duration) > 0.03:
        raise RuntimeError(
            f"MOTION_DURATION: requested {expected_duration}s, encoder produced {encoded_duration}s"
        )


def _read_gif_loop_count(path: Path) -> Optional[int]:
    """Return the Netscape/ANIMEXTS repeat count embedded in a GIF."""

    data = path.read_bytes()
    if data[:6] not in (b"GIF87a", b"GIF89a"):
        return None
    for application in (b"NETSCAPE2.0", b"ANIMEXTS1.0"):
        marker = b"\x21\xff\x0b" + application + b"\x03\x01"
        offset = data.find(marker)
        if offset < 0:
            continue
        value_offset = offset + len(marker)
        if value_offset + 3 > len(data) or data[value_offset + 2] != 0:
            return None
        return int.from_bytes(data[value_offset : value_offset + 2], "little")
    return None


def _validate_gif_loop(path: Path) -> int:
    loop_count = _read_gif_loop_count(path)
    if loop_count is None:
        raise RuntimeError("MOTION_LOOP: encoded GIF has no readable loop extension")
    if loop_count != 0:
        raise RuntimeError(
            f"MOTION_LOOP: encoded GIF repeats {loop_count} time(s), expected infinite looping"
        )
    return loop_count


def _run_encoder(command: list[str], label: str) -> subprocess.CompletedProcess[str]:
    try:
        process = subprocess.run(
            command,
            text=True,
            capture_output=True,
            check=False,
            timeout=MOTION_TIMEOUTS["encode"],
        )
    except subprocess.TimeoutExpired as error:
        raise RuntimeError(f"MOTION_ENCODE_TIMEOUT: {label} exceeded 120 seconds") from error
    if process.returncode != 0:
        raise RuntimeError(f"MOTION_ENCODE: {process.stderr.strip() or process.stdout.strip()}")
    return process


def _signalstats_values(output: str) -> list[float]:
    return [float(value) for value in re.findall(r"lavfi\.signalstats\.YAVG=([0-9.eE+-]+)", output)]


def _summarize_delta_quality(
    adjacent_yavg: list[float],
    seam_yavg: float,
    changed_area_yavg: float,
) -> dict[str, object]:
    values = sorted(adjacent_yavg)
    if not values or any(not math.isfinite(value) or value < 0 for value in (*values, seam_yavg, changed_area_yavg)):
        raise RuntimeError("MOTION_DELTA: frame-delta metrics are missing or invalid")
    p95 = values[max(0, math.ceil(len(values) * 0.95) - 1)]
    median = values[len(values) // 2]
    seam_within_p95 = seam_yavg <= p95
    result: dict[str, object] = {
        "metric": "normalized-luma-mean-absolute-difference",
        "adjacent_min": values[0] / 255,
        "adjacent_median": median / 255,
        "adjacent_p95": p95 / 255,
        "adjacent_max": values[-1] / 255,
        "seam": seam_yavg / 255,
        "seam_within_adjacent_p95": seam_within_p95,
        "changed_area_ratio": min(1.0, changed_area_yavg / 255),
        "changed_area_method": "accumulated adjacent luma delta greater than 2 percent",
    }
    if not seam_within_p95:
        raise RuntimeError("MOTION_LOOP: loop seam exceeds the p95 adjacent-frame delta")
    return result


def _probe_frame_deltas(ffmpeg: str, frames: Path, fps: int, frame_count: int) -> dict[str, object]:
    pattern = str(frames / "frame-%06d.png")
    adjacent = _run_encoder(
        [
            ffmpeg,
            "-nostdin",
            "-hide_banner",
            "-loglevel",
            "error",
            "-framerate",
            str(fps),
            "-start_number",
            "0",
            "-i",
            pattern,
            "-vf",
            "tblend=all_mode=difference,signalstats,metadata=print:file=-",
            "-f",
            "null",
            "-",
        ],
        "adjacent frame-delta probe",
    )
    adjacent_values = _signalstats_values(adjacent.stdout)
    if len(adjacent_values) != frame_count - 1:
        raise RuntimeError("MOTION_DELTA: adjacent frame-delta probe returned an unexpected sample count")

    seam = _run_encoder(
        [
            ffmpeg,
            "-nostdin",
            "-hide_banner",
            "-loglevel",
            "error",
            "-i",
            str(frames / f"frame-{frame_count - 1:06d}.png"),
            "-i",
            str(frames / "frame-000000.png"),
            "-filter_complex",
            "[0:v][1:v]blend=all_mode=difference,signalstats,metadata=print:file=-",
            "-frames:v",
            "1",
            "-f",
            "null",
            "-",
        ],
        "loop-seam frame-delta probe",
    )
    seam_values = _signalstats_values(seam.stdout)
    if len(seam_values) != 1:
        raise RuntimeError("MOTION_DELTA: loop-seam probe returned an unexpected sample count")

    delta_frames = frame_count - 1
    weights = " ".join("1" for _ in range(delta_frames))
    coverage_filter = (
        "tblend=all_mode=difference,format=gray,"
        f"tmix=frames={delta_frames}:weights='{weights}':scale=1,"
        "lut=y='if(gt(val,5.1),255,0)',signalstats,metadata=print:file=-"
    )
    coverage = _run_encoder(
        [
            ffmpeg,
            "-nostdin",
            "-hide_banner",
            "-loglevel",
            "error",
            "-framerate",
            str(fps),
            "-start_number",
            "0",
            "-i",
            pattern,
            "-vf",
            coverage_filter,
            "-f",
            "null",
            "-",
        ],
        "changed-area probe",
    )
    coverage_values = _signalstats_values(coverage.stdout)
    if len(coverage_values) != delta_frames:
        raise RuntimeError("MOTION_DELTA: changed-area probe returned an unexpected sample count")
    return _summarize_delta_quality(adjacent_values, seam_values[0], coverage_values[-1])


def _summarize_frame_hashes(
    hashes: list[str],
    full_opacity_frames: tuple[int, int],
    *,
    algorithm: str,
    raster_kind: str,
) -> dict[str, object]:
    groups: dict[str, list[int]] = {}
    for index, frame_hash in enumerate(hashes):
        groups.setdefault(frame_hash, []).append(index)
    repeated = [
        {"hash": frame_hash, "frame_indices": indices}
        for frame_hash, indices in groups.items()
        if len(indices) > 1
    ]
    adjacent_duplicates = [
        [index, index + 1]
        for index in range(len(hashes) - 1)
        if hashes[index] == hashes[index + 1]
    ]
    if adjacent_duplicates:
        raise RuntimeError(
            f"MOTION_LOOP: {raster_kind} contains adjacent duplicate frames: {adjacent_duplicates}"
        )

    unique_count = len(groups)
    strict_all_unique = len(hashes) <= APPROVED_BASELINE_FRAME_COUNT
    minimum_unique = len(hashes) if strict_all_unique else APPROVED_BASELINE_FRAME_COUNT
    if strict_all_unique and repeated:
        raise RuntimeError(
            f"MOTION_LOOP: every {raster_kind} frame must be unique for timelines of "
            f"{APPROVED_BASELINE_FRAME_COUNT} frames or fewer"
        )
    if unique_count < minimum_unique:
        raise RuntimeError(
            f"MOTION_LOOP: {raster_kind} requires at least {minimum_unique} unique frames; "
            f"found {unique_count}"
        )

    repeat_scope_start, repeat_scope_end = full_opacity_frames
    reset_boundary_frame = repeat_scope_end + 1
    classified_repeats = []
    for evidence in repeated:
        indices = evidence["frame_indices"]
        if all(repeat_scope_start <= index <= repeat_scope_end for index in indices):
            classification = "steady_state_periodic_repeat"
        elif (
            reset_boundary_frame in indices
            and all(repeat_scope_start <= index <= reset_boundary_frame for index in indices)
        ):
            classification = "intentional_reset_boundary_repeat"
        else:
            classification = "outside_permitted_repeat_scope"
        classified_repeats.append({**evidence, "classification": classification})
    outside_scope = [
        evidence
        for evidence in classified_repeats
        if evidence["classification"] == "outside_permitted_repeat_scope"
    ]
    if outside_scope:
        raise RuntimeError(
            f"MOTION_LOOP: repeated {raster_kind} frames must stay inside full-opacity frames "
            f"{repeat_scope_start}-{repeat_scope_end}, except reset boundary frame "
            f"{reset_boundary_frame} at opacity 1.00: {outside_scope}"
        )

    intentional_reset_boundary_repeats = [
        evidence
        for evidence in classified_repeats
        if evidence["classification"] == "intentional_reset_boundary_repeat"
    ]
    repeats_confined_to_full_opacity = not intentional_reset_boundary_repeats

    return {
        "algorithm": algorithm,
        "frame_count": len(hashes),
        "unique_frame_count": unique_count,
        "minimum_unique_frame_count": minimum_unique,
        "all_frames_unique": not repeated,
        "adjacent_duplicate_count": 0,
        "adjacent_duplicate_pairs": [],
        "repeat_scope": [repeat_scope_start, repeat_scope_end],
        "intentional_reset_boundary_repeat_frame": reset_boundary_frame,
        "intentional_reset_boundary_opacity": RESET_OPACITY_SAMPLES[0],
        "repeat_scope_policy": (
            "all-frames-unique"
            if strict_all_unique
            else "non-adjacent-repeats-inside-full-opacity-plus-reset-start-boundary"
        ),
        "repeated_frame_group_count": len(classified_repeats),
        "repeated_frame_index_evidence": classified_repeats,
        "intentional_reset_boundary_repeat_count": len(intentional_reset_boundary_repeats),
        "intentional_reset_boundary_repeat_evidence": intentional_reset_boundary_repeats,
        "repeats_confined_to_full_opacity": repeats_confined_to_full_opacity,
        "repeats_confined_to_permitted_scope": True,
        "opening_construction_and_reset_tail_globally_distinct": True,
        "first_frame_hash": hashes[0],
        "last_frame_hash": hashes[-1],
    }


def _probe_encoded_frame_hashes(
    ffmpeg: str,
    gif: Path,
    expected_frames: int,
    full_opacity_frames: tuple[int, int],
) -> dict[str, object]:
    process = _run_encoder(
        [
            ffmpeg,
            "-nostdin",
            "-hide_banner",
            "-loglevel",
            "error",
            "-i",
            str(gif),
            "-map",
            "0:v:0",
            "-f",
            "framemd5",
            "-",
        ],
        "encoded GIF frame-hash probe",
    )
    hashes = re.findall(r",\s*([0-9a-f]{32})\s*$", process.stdout, flags=re.MULTILINE)
    if len(hashes) != expected_frames:
        raise RuntimeError("MOTION_FRAMES: encoded frame-hash probe returned an unexpected frame count")
    return _summarize_frame_hashes(
        hashes,
        full_opacity_frames,
        algorithm="framemd5",
        raster_kind="encoded GIF",
    )


def _gif_command(ffmpeg: str, frames: Path, fps: int, output: Path) -> list[str]:
    return [
        ffmpeg,
        "-nostdin",
        "-hide_banner",
        "-loglevel",
        "error",
        "-y",
        "-framerate",
        str(fps),
        "-i",
        str(frames / "frame-%06d.png"),
        "-filter_complex",
        "[0:v]split[a][b];[a]palettegen=stats_mode=full:max_colors=256[p];"
        "[b][p]paletteuse=dither=bayer:bayer_scale=3:diff_mode=rectangle",
        "-loop",
        "0",
        "-map_metadata",
        "-1",
        str(output.resolve()),
    ]


def _encode_gif(
    frames: Path,
    fps: int,
    output: Path,
    runtime: dict[str, object],
) -> str:
    ffmpeg = str(runtime["ffmpeg"])
    _run_encoder(_gif_command(ffmpeg, frames, fps, output), "GIF encoder")
    return "ffmpeg/gif-palette"


def _size_guidance(
    artifact_bytes: int,
) -> dict[str, object]:
    warnings: list[str] = []
    if artifact_bytes > MOTION_SIZE_TARGET_BYTES:
        warnings.append("GIF exceeds 500 KB; reduce width, duration, or frame rate before distribution.")
    return {
        "artifact_target_bytes": MOTION_SIZE_TARGET_BYTES,
        "artifact_within_target": artifact_bytes <= MOTION_SIZE_TARGET_BYTES,
        "warnings": warnings,
    }


def render_motion_gif(
    svg_path: Path,
    output_path: Path,
    report_path: Optional[Path] = None,
    preset: str = "auto",
    duration: float = DEFAULT_MOTION_DURATION,
    fps: int = DEFAULT_MOTION_FPS,
    width: Optional[int] = None,
    dry_run: bool = False,
) -> dict[str, object]:
    """Render one generated semantic SVG to one validated, atomic GIF artifact."""

    source_target = svg_path.resolve()
    gif_target = output_path.resolve()
    if source_target == gif_target:
        raise ValueError("MOTION_OUTPUT: output path must differ from the input path")
    if output_path.suffix.lower() != MOTION_FORMAT["suffix"]:
        raise ValueError("MOTION_OUTPUT: output path must end in .gif")
    if report_path and report_path.resolve() in {source_target, gif_target}:
        raise ValueError("MOTION_REPORT: report path must differ from the input and GIF paths")

    plan, prepared_source = build_motion_plan(
        svg_path,
        preset=preset,
        duration=duration,
        fps=fps,
        width=width,
    )
    encoded_width = int(plan["width"])
    encoded_height = int(plan["height"])
    plan.update(
        {
            "output": str(output_path),
            "report": str(report_path) if report_path else None,
            "output_format": MOTION_FORMAT["name"],
            "mime_type": MOTION_FORMAT["mime"],
            "loop_playback": MOTION_FORMAT["loop_playback"],
            "encoded_dimensions": {"width": encoded_width, "height": encoded_height},
            "required_encoder": "ffmpeg/gif",
        }
    )
    if dry_run:
        return plan

    runtime = probe_motion_runtime()
    if not runtime.get("ok"):
        raise RuntimeError(f'MOTION_RUNTIME: {runtime.get("error", "motion dependencies are unavailable")}')
    available = runtime.get("format")
    if not isinstance(available, dict) or not available.get("gif"):
        raise RuntimeError("MOTION_FORMAT_UNAVAILABLE: GIF encoder is unavailable")

    node = str(runtime["node"])
    output_path.parent.mkdir(parents=True, exist_ok=True)
    temporary_gif = output_path.with_name(f".{output_path.stem}.{uuid.uuid4().hex}.tmp.gif")
    temporary_report: Optional[Path] = None
    source_hash = str(plan["source_sha256"])

    try:
        with tempfile.TemporaryDirectory(prefix="fireworks-motion-") as directory:
            work = Path(directory)
            source = work / "source.svg"
            frames = work / "frames"
            source.write_text(str(prepared_source), encoding="utf-8")
            frames.mkdir()
            try:
                render_process = subprocess.run(
                    [
                        node,
                        str(MOTION_WORKER),
                        "--input",
                        str(source),
                        "--frames-dir",
                        str(frames),
                        "--preset",
                        str(plan["preset"]),
                        "--duration",
                        str(duration),
                        "--fps",
                        str(fps),
                        "--width",
                        str(plan["width"]),
                        "--height",
                        str(plan["height"]),
                    ],
                    text=True,
                    capture_output=True,
                    check=False,
                    timeout=MOTION_TIMEOUTS["render"],
                )
            except subprocess.TimeoutExpired as error:
                raise RuntimeError("MOTION_RENDER_TIMEOUT: frame rendering exceeded 120 seconds") from error
            if render_process.returncode != 0:
                raise RuntimeError(f"MOTION_RENDER: {render_process.stderr.strip() or render_process.stdout.strip()}")
            worker = _last_json_line(render_process.stdout, "motion renderer")
            frame_files = sorted(frames.glob("frame-*.png"))
            if len(frame_files) != plan["frame_count"]:
                raise RuntimeError(f'MOTION_FRAMES: expected {plan["frame_count"]} frames, rendered {len(frame_files)}')
            frame_hashes = [_sha256(frame) for frame in frame_files]
            first_hash = frame_hashes[0]
            last_hash = frame_hashes[-1]
            stream_contract = plan["animation_contract"]["persistent_data_flow"]
            full_opacity_values = stream_contract["full_opacity_frames"]
            full_opacity_frames = (int(full_opacity_values[0]), int(full_opacity_values[1]))
            raw_frame_hashes = _summarize_frame_hashes(
                frame_hashes,
                full_opacity_frames,
                algorithm="sha256",
                raster_kind="raw Chromium raster",
            )
            delta_quality = _probe_frame_deltas(
                str(runtime["ffmpeg"]),
                frames,
                fps,
                int(plan["frame_count"]),
            )

            encoder = _encode_gif(frames, fps, temporary_gif, runtime)
            if not temporary_gif.is_file() or temporary_gif.stat().st_size == 0:
                raise RuntimeError("MOTION_ENCODE: encoder produced an empty GIF artifact")
            media_probe = _probe_media(temporary_gif)
            _validate_encoded_gif(
                media_probe,
                encoded_width,
                encoded_height,
                int(plan["frame_count"]),
                duration,
            )
            loop_count = _validate_gif_loop(temporary_gif)
            encoded_frame_hashes = _probe_encoded_frame_hashes(
                str(runtime["ffmpeg"]),
                temporary_gif,
                int(plan["frame_count"]),
                full_opacity_frames,
            )

        source_hash_after = _sha256(svg_path)
        if source_hash_after != source_hash:
            raise RuntimeError("MOTION_SOURCE_MUTATED: source SVG changed during GIF rendering")
        artifact_bytes = temporary_gif.stat().st_size
        result = dict(plan)
        result.update(
            {
                "dry_run": False,
                "runtime": runtime,
                "renderer": worker,
                "source_integrity": {
                    "sha256": source_hash,
                    "sha256_before": source_hash,
                    "sha256_after": source_hash_after,
                    "unchanged": True,
                },
                "loop": {
                    "first_frame_sha256": first_hash,
                    "last_frame_sha256": last_hash,
                    "unique_frame_count": raw_frame_hashes["unique_frame_count"],
                    "minimum_unique_frame_count": raw_frame_hashes["minimum_unique_frame_count"],
                    "all_frames_unique": raw_frame_hashes["all_frames_unique"],
                    "adjacent_duplicate_count": raw_frame_hashes["adjacent_duplicate_count"],
                    "repeat_scope": raw_frame_hashes["repeat_scope"],
                    "repeat_scope_policy": raw_frame_hashes["repeat_scope_policy"],
                    "repeated_frame_index_evidence": raw_frame_hashes[
                        "repeated_frame_index_evidence"
                    ],
                    "intentional_reset_boundary_repeat_count": raw_frame_hashes[
                        "intentional_reset_boundary_repeat_count"
                    ],
                    "intentional_reset_boundary_repeat_evidence": raw_frame_hashes[
                        "intentional_reset_boundary_repeat_evidence"
                    ],
                    "repeats_confined_to_full_opacity": raw_frame_hashes[
                        "repeats_confined_to_full_opacity"
                    ],
                    "repeats_confined_to_permitted_scope": raw_frame_hashes[
                        "repeats_confined_to_permitted_scope"
                    ],
                    "opening_construction_and_reset_tail_globally_distinct": raw_frame_hashes[
                        "opening_construction_and_reset_tail_globally_distinct"
                    ],
                    "sampling": "uniform-frame-centers",
                    "delta_quality": delta_quality,
                    "raw_frames": raw_frame_hashes,
                    "encoded_frames": encoded_frame_hashes,
                    "playback": MOTION_FORMAT["loop_playback"],
                    "embedded_loop_count": loop_count,
                },
                "artifact": {
                    "path": str(output_path),
                    "format": MOTION_FORMAT["name"],
                    "mime_type": MOTION_FORMAT["mime"],
                    "encoder": encoder,
                    "bytes": artifact_bytes,
                    "sha256": _sha256(temporary_gif),
                    "probe": media_probe,
                    "source_frame_count": plan["frame_count"],
                },
                "size_guidance": _size_guidance(artifact_bytes),
            }
        )
        if report_path:
            temporary_report = _stage_json(report_path, result)
        staged_targets = [(temporary_gif, output_path)]
        if report_path and temporary_report:
            staged_targets.append((temporary_report, report_path))
        _commit_artifacts(staged_targets)
        return result
    finally:
        temporary_gif.unlink(missing_ok=True)
        if temporary_report:
            temporary_report.unlink(missing_ok=True)
scripts/diagram_ir.py
#!/usr/bin/env python3
"""Typed, backwards-compatible input model for Fireworks Tech Graph.

The renderer keeps accepting the historical JSON shape. This module gives
that shape a versioned semantic boundary before any layout code runs.
"""

from __future__ import annotations

import copy
import math
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Mapping, Optional, Sequence


SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
    sys.path.insert(0, str(SCRIPT_DIR))

from semantic_contracts import resolve_style_index, validate_semantic_contract  # noqa: E402


class DiagramValidationError(ValueError):
    """Raised when input cannot be normalized to diagram schema v1."""


def _finite(value: Any, path: str) -> float:
    try:
        number = float(value)
    except (TypeError, ValueError) as error:
        raise DiagramValidationError(f"{path} must be a finite number") from error
    if not math.isfinite(number):
        raise DiagramValidationError(f"{path} must be a finite number")
    return number


@dataclass(frozen=True)
class NodeIR:
    node_id: str
    raw: Mapping[str, Any]


@dataclass(frozen=True)
class EdgeIR:
    edge_id: str
    source: Optional[str]
    target: Optional[str]
    waypoints: tuple[tuple[float, float], ...]
    raw: Mapping[str, Any]


@dataclass(frozen=True)
class DiagramIR:
    schema_version: int
    input_schema: str
    mode: str
    style_index: int
    semantic_report: Mapping[str, Any]
    nodes: tuple[NodeIR, ...]
    edges: tuple[EdgeIR, ...]
    raw: Mapping[str, Any]

    def as_dict(self) -> dict[str, Any]:
        return copy.deepcopy(dict(self.raw))


def _validate_numeric_fields(item: Mapping[str, Any], fields: Sequence[str], path: str) -> None:
    for field in fields:
        if field in item and item[field] is not None:
            _finite(item[field], f"{path}.{field}")


def normalize_diagram(data: Mapping[str, Any], expected_mode: str) -> DiagramIR:
    if not isinstance(data, Mapping):
        raise DiagramValidationError("diagram input must be a JSON object")
    normalized = copy.deepcopy(dict(data))
    input_schema = "v1" if "schema_version" in normalized else "legacy"
    schema_version = normalized.get("schema_version", 1)
    if isinstance(schema_version, bool) or schema_version != 1:
        raise DiagramValidationError(f"unsupported schema_version: {schema_version}")

    mode = str(normalized.get("mode") or normalized.get("template_type") or expected_mode)
    if normalized.get("mode") and mode != expected_mode:
        raise DiagramValidationError(f"mode {mode!r} conflicts with template type {expected_mode!r}")
    normalized["schema_version"] = 1
    normalized["mode"] = mode

    _validate_numeric_fields(normalized, ("width", "height"), "diagram")
    for field in ("width", "height"):
        if field in normalized and _finite(normalized[field], f"diagram.{field}") <= 0:
            raise DiagramValidationError(f"diagram.{field} must be greater than zero")

    raw_containers = normalized.get("containers", [])
    if not isinstance(raw_containers, list):
        raise DiagramValidationError("containers must be an array")
    containers: list[dict[str, Any]] = []
    container_ids: set[str] = set()
    for index, raw_container in enumerate(raw_containers):
        if not isinstance(raw_container, Mapping):
            raise DiagramValidationError(f"containers[{index}] must be an object")
        container = copy.deepcopy(dict(raw_container))
        container_id = str(container.get("id") or "").strip()
        if not container_id:
            raise DiagramValidationError(f"containers[{index}].id must be a non-empty string")
        if container_id in container_ids:
            raise DiagramValidationError(f"duplicate container id: {container_id}")
        container_ids.add(container_id)
        container["id"] = container_id
        _validate_numeric_fields(container, ("x", "y", "width", "height"), f"containers[{index}]")
        containers.append(container)
    normalized["containers"] = containers

    raw_nodes = normalized.get("nodes", [])
    if not isinstance(raw_nodes, list):
        raise DiagramValidationError("nodes must be an array")
    nodes: list[NodeIR] = []
    node_ids: set[str] = set()
    for index, raw_node in enumerate(raw_nodes):
        if not isinstance(raw_node, Mapping):
            raise DiagramValidationError(f"nodes[{index}] must be an object")
        node = copy.deepcopy(dict(raw_node))
        node_id = str(node.get("id") or f"node-{index:03d}")
        if node_id in node_ids:
            raise DiagramValidationError(f"duplicate node id: {node_id}")
        if node_id in container_ids:
            raise DiagramValidationError(f"duplicate diagram id: {node_id}")
        node_ids.add(node_id)
        node["id"] = node_id
        _validate_numeric_fields(node, ("x", "y", "width", "height", "r", "offset_y"), f"nodes[{index}]")
        nodes.append(NodeIR(node_id, node))
    normalized["nodes"] = [copy.deepcopy(dict(node.raw)) for node in nodes]

    if "edges" in normalized and "arrows" not in normalized:
        normalized["arrows"] = normalized.pop("edges")
    raw_edges = normalized.get("arrows", [])
    if not isinstance(raw_edges, list):
        raise DiagramValidationError("arrows must be an array")
    edges: list[EdgeIR] = []
    edge_ids: set[str] = set()
    for index, raw_edge in enumerate(raw_edges):
        if not isinstance(raw_edge, Mapping):
            raise DiagramValidationError(f"arrows[{index}] must be an object")
        edge = copy.deepcopy(dict(raw_edge))
        edge_id = str(edge.get("id") or f"edge-{index:03d}")
        if edge_id in edge_ids:
            raise DiagramValidationError(f"duplicate edge id: {edge_id}")
        if edge_id in container_ids or edge_id in node_ids:
            raise DiagramValidationError(f"duplicate diagram id: {edge_id}")
        edge_ids.add(edge_id)
        edge["id"] = edge_id
        source = str(edge["source"]) if edge.get("source") is not None else None
        target = str(edge["target"]) if edge.get("target") is not None else None
        for endpoint_name, endpoint in (("source", source), ("target", target)):
            if endpoint is not None and endpoint not in node_ids:
                raise DiagramValidationError(f"arrows[{index}].{endpoint_name} references unknown node: {endpoint}")
        _validate_numeric_fields(
            edge,
            ("x1", "y1", "x2", "y2", "label_dx", "label_dy", "routing_padding", "port_clearance"),
            f"arrows[{index}]",
        )
        raw_waypoints = edge.get("route_points", [])
        if not isinstance(raw_waypoints, list):
            raise DiagramValidationError(f"arrows[{index}].route_points must be an array")
        waypoints: list[tuple[float, float]] = []
        for waypoint_index, raw_waypoint in enumerate(raw_waypoints):
            if not isinstance(raw_waypoint, (list, tuple)) or len(raw_waypoint) != 2:
                raise DiagramValidationError(
                    f"arrows[{index}].route_points[{waypoint_index}] must be [x, y]"
                )
            waypoint = (
                _finite(raw_waypoint[0], f"arrows[{index}].route_points[{waypoint_index}][0]"),
                _finite(raw_waypoint[1], f"arrows[{index}].route_points[{waypoint_index}][1]"),
            )
            waypoints.append(waypoint)
        edge["route_points"] = [[x, y] for x, y in waypoints]
        edges.append(EdgeIR(edge_id, source, target, tuple(waypoints), edge))
    normalized["arrows"] = [copy.deepcopy(dict(edge.raw)) for edge in edges]

    style_index = resolve_style_index(normalized)
    semantic_report = validate_semantic_contract(normalized)
    return DiagramIR(
        1,
        input_schema,
        mode,
        style_index,
        semantic_report,
        tuple(nodes),
        tuple(edges),
        normalized,
    )


__all__ = [
    "DiagramIR",
    "DiagramValidationError",
    "EdgeIR",
    "NodeIR",
    "normalize_diagram",
]
sitemap.xml
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url><loc>https://yizhiyanhua-ai.github.io/fireworks-tech-graph/</loc><changefreq>weekly</changefreq><priority>1.0</priority></url>
</urlset>
scripts/validate_svg.py
#!/usr/bin/env python3
"""Structured SVG checks used by validate-svg.sh.

The validator intentionally uses only the Python standard library so it works
inside a freshly installed skill without adding another runtime dependency.
"""

from __future__ import annotations

import argparse
import math
import re
import sys
import xml.etree.ElementTree as ET
from dataclasses import dataclass
from pathlib import Path
from typing import Iterator, Optional, Sequence


SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
    sys.path.insert(0, str(SCRIPT_DIR))

import fireworks_geometry as geometry  # noqa: E402
import composition_quality as quality  # noqa: E402


NUMBER_RE = re.compile(r"[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?")
PATH_TOKEN_RE = re.compile(r"[AaCcHhLlMmQqSsTtVvZz]|[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?")
URL_REF_RE = re.compile(r"url\(\s*#([^\s)]+)\s*\)")
MARKER_ATTRIBUTES = ("marker-start", "marker-mid", "marker-end")
EXCLUDED_ROLES = {"background", "bridge-mask", "container", "decoration", "label", "legend", "reserved"}
IDENTITY = (1.0, 0.0, 0.0, 1.0, 0.0, 0.0)

Point = tuple[float, float]
Matrix = tuple[float, float, float, float, float, float]


@dataclass(frozen=True)
class Bounds:
    left: float
    top: float
    right: float
    bottom: float


@dataclass(frozen=True)
class Collision:
    edge: str
    obstacle: str


@dataclass(frozen=True)
class ElementContext:
    element: ET.Element
    matrix: Matrix
    role: Optional[str]
    in_defs: bool


def local_name(tag: str) -> str:
    return tag.split("}", 1)[-1]


def parse_number(value: Optional[str], default: Optional[float] = 0.0) -> Optional[float]:
    if value is None:
        return default
    match = NUMBER_RE.match(value.strip())
    if not match:
        return default
    return float(match.group(0))


def multiply(left: Matrix, right: Matrix) -> Matrix:
    a1, b1, c1, d1, e1, f1 = left
    a2, b2, c2, d2, e2, f2 = right
    return (
        a1 * a2 + c1 * b2,
        b1 * a2 + d1 * b2,
        a1 * c2 + c1 * d2,
        b1 * c2 + d1 * d2,
        a1 * e2 + c1 * f2 + e1,
        b1 * e2 + d1 * f2 + f1,
    )


def transform_point(matrix: Matrix, point: Point) -> Point:
    a, b, c, d, e, f = matrix
    x, y = point
    return (a * x + c * y + e, b * x + d * y + f)


def parse_transform(value: Optional[str]) -> Matrix:
    result = IDENTITY
    if not value:
        return result
    for name, raw_values in re.findall(r"([A-Za-z]+)\s*\(([^)]*)\)", value):
        values = [float(item) for item in NUMBER_RE.findall(raw_values)]
        name = name.lower()
        current = IDENTITY
        if name == "matrix" and len(values) == 6:
            current = tuple(values)  # type: ignore[assignment]
        elif name == "translate" and values:
            current = (1, 0, 0, 1, values[0], values[1] if len(values) > 1 else 0)
        elif name == "scale" and values:
            current = (values[0], 0, 0, values[1] if len(values) > 1 else values[0], 0, 0)
        elif name == "rotate" and values:
            angle = math.radians(values[0])
            rotation = (math.cos(angle), math.sin(angle), -math.sin(angle), math.cos(angle), 0, 0)
            if len(values) >= 3:
                cx, cy = values[1], values[2]
                current = multiply(
                    multiply((1, 0, 0, 1, cx, cy), rotation),
                    (1, 0, 0, 1, -cx, -cy),
                )
            else:
                current = rotation
        elif name == "skewx" and len(values) == 1:
            current = (1, 0, math.tan(math.radians(values[0])), 1, 0, 0)
        elif name == "skewy" and len(values) == 1:
            current = (1, math.tan(math.radians(values[0])), 0, 1, 0, 0)
        result = multiply(result, current)
    return result


def infer_role(element: ET.Element, inherited: Optional[str]) -> Optional[str]:
    explicit = element.get("data-graph-role")
    if explicit:
        return explicit.strip().lower()
    identity = " ".join(filter(None, (element.get("id"), element.get("class")))).lower()
    if any(token in identity for token in ("legend", "key-box", "key_box")):
        return "legend"
    if local_name(element.tag) == "g":
        text = " ".join("".join(child.itertext()) for child in element if local_name(child.tag) == "text").lower()
        if "legend" in text:
            return "legend"
    return inherited


def walk(element: ET.Element, matrix: Matrix = IDENTITY, role: Optional[str] = None, in_defs: bool = False) -> Iterator[ElementContext]:
    current_matrix = multiply(matrix, parse_transform(element.get("transform")))
    current_role = infer_role(element, role)
    current_in_defs = in_defs or local_name(element.tag) == "defs"
    yield ElementContext(element, current_matrix, current_role, current_in_defs)
    child_role = current_role if current_role in {"background", "bridge-mask", "decoration", "label", "legend", "node", "reserved"} else None
    for child in element:
        yield from walk(child, current_matrix, child_role, current_in_defs)


def canvas_size(root: ET.Element) -> Point:
    values = [float(item) for item in NUMBER_RE.findall(root.get("viewBox", ""))]
    if len(values) == 4:
        return values[2], values[3]
    return (
        float(parse_number(root.get("width"), 0.0) or 0.0),
        float(parse_number(root.get("height"), 0.0) or 0.0),
    )


def metadata_bounds(context: ElementContext) -> Optional[Bounds]:
    values = [float(item) for item in NUMBER_RE.findall(context.element.get("data-graph-bounds", ""))]
    if len(values) != 4:
        return None
    left, top, right, bottom = values
    if not all(math.isfinite(value) for value in values) or right < left or bottom < top:
        return None
    return transformed_bounds(
        context.matrix,
        ((left, top), (right, top), (right, bottom), (left, bottom)),
    )


def bounds_from_points(points: Sequence[Point]) -> Optional[Bounds]:
    if not points:
        return None
    xs = [point[0] for point in points]
    ys = [point[1] for point in points]
    return Bounds(min(xs), min(ys), max(xs), max(ys))


def transformed_bounds(matrix: Matrix, points: Sequence[Point]) -> Optional[Bounds]:
    return bounds_from_points([transform_point(matrix, point) for point in points])


def shape_bounds(context: ElementContext, canvas: Point) -> Optional[Bounds]:
    element = context.element
    tag = local_name(element.tag)
    role = context.role
    if context.in_defs or role in EXCLUDED_ROLES:
        return None

    declared = metadata_bounds(context)
    if declared is not None and role == "node":
        return declared

    if tag == "rect":
        x = float(parse_number(element.get("x"), 0.0) or 0.0)
        y = float(parse_number(element.get("y"), 0.0) or 0.0)
        width = parse_number(element.get("width"), None)
        height = parse_number(element.get("height"), None)
        if width is None or height is None or width <= 0 or height <= 0:
            return None
        canvas_width, canvas_height = canvas
        if role != "node":
            if element.get("x") is None and element.get("y") is None:
                return None
            if height <= 28 or width <= 8:
                return None
            nearly_canvas = canvas_width > 0 and canvas_height > 0 and width >= canvas_width * 0.9 and height >= canvas_height * 0.9
            if nearly_canvas:
                return None
            container_like = bool(element.get("stroke-dasharray")) or element.get("fill", "").strip().lower() == "none"
            if container_like and (
                (canvas_width > 0 and width >= canvas_width * 0.45)
                or (canvas_height > 0 and height >= canvas_height * 0.45)
            ):
                return None
        return transformed_bounds(
            context.matrix,
            ((x, y), (x + width, y), (x + width, y + height), (x, y + height)),
        )

    if tag in {"circle", "ellipse"}:
        cx = float(parse_number(element.get("cx"), 0.0) or 0.0)
        cy = float(parse_number(element.get("cy"), 0.0) or 0.0)
        rx = float(parse_number(element.get("r") or element.get("rx"), 0.0) or 0.0)
        ry = float(parse_number(element.get("r") or element.get("ry"), 0.0) or 0.0)
        if rx < 12 or ry < 12:
            return None
        return transformed_bounds(
            context.matrix,
            ((cx - rx, cy - ry), (cx + rx, cy - ry), (cx + rx, cy + ry), (cx - rx, cy + ry)),
        )

    if tag in {"polygon", "polyline"} and not has_marker(element):
        values = [float(item) for item in NUMBER_RE.findall(element.get("points", ""))]
        points = list(zip(values[::2], values[1::2]))
        if len(points) < 3:
            return None
        return transformed_bounds(context.matrix, points)
    return None


def has_marker(element: ET.Element) -> bool:
    return any(element.get(attribute) for attribute in MARKER_ATTRIBUTES)


def marker_references(root: ET.Element) -> tuple[set[str], set[str]]:
    definitions = {
        element.get("id", "")
        for element in root.iter()
        if local_name(element.tag) == "marker" and element.get("id")
    }
    references: set[str] = set()
    for element in root.iter():
        for attribute in MARKER_ATTRIBUTES:
            value = element.get(attribute, "")
            references.update(URL_REF_RE.findall(value))
    return definitions, references


def sample_quadratic(start: Point, control: Point, end: Point, steps: int = 12) -> list[Point]:
    return [
        (
            (1 - t) ** 2 * start[0] + 2 * (1 - t) * t * control[0] + t**2 * end[0],
            (1 - t) ** 2 * start[1] + 2 * (1 - t) * t * control[1] + t**2 * end[1],
        )
        for t in (index / steps for index in range(1, steps + 1))
    ]


def sample_cubic(start: Point, first: Point, second: Point, end: Point, steps: int = 16) -> list[Point]:
    return [
        (
            (1 - t) ** 3 * start[0] + 3 * (1 - t) ** 2 * t * first[0] + 3 * (1 - t) * t**2 * second[0] + t**3 * end[0],
            (1 - t) ** 3 * start[1] + 3 * (1 - t) ** 2 * t * first[1] + 3 * (1 - t) * t**2 * second[1] + t**3 * end[1],
        )
        for t in (index / steps for index in range(1, steps + 1))
    ]


def sample_arc(
    start: Point,
    rx: float,
    ry: float,
    rotation: float,
    large_arc: bool,
    sweep: bool,
    end: Point,
    steps: int = 20,
) -> list[Point]:
    """Sample an SVG endpoint-parameterized elliptical arc."""

    rx, ry = abs(rx), abs(ry)
    if rx <= 1e-9 or ry <= 1e-9 or start == end:
        return [end]
    phi = math.radians(rotation % 360)
    cos_phi, sin_phi = math.cos(phi), math.sin(phi)
    dx = (start[0] - end[0]) / 2
    dy = (start[1] - end[1]) / 2
    x_prime = cos_phi * dx + sin_phi * dy
    y_prime = -sin_phi * dx + cos_phi * dy
    scale = (x_prime * x_prime) / (rx * rx) + (y_prime * y_prime) / (ry * ry)
    if scale > 1:
        factor = math.sqrt(scale)
        rx *= factor
        ry *= factor
    numerator = max(
        0.0,
        rx * rx * ry * ry - rx * rx * y_prime * y_prime - ry * ry * x_prime * x_prime,
    )
    denominator = rx * rx * y_prime * y_prime + ry * ry * x_prime * x_prime
    coefficient = 0.0 if denominator <= 1e-12 else math.sqrt(numerator / denominator)
    if large_arc == sweep:
        coefficient = -coefficient
    center_x_prime = coefficient * (rx * y_prime / ry)
    center_y_prime = coefficient * (-ry * x_prime / rx)
    center_x = cos_phi * center_x_prime - sin_phi * center_y_prime + (start[0] + end[0]) / 2
    center_y = sin_phi * center_x_prime + cos_phi * center_y_prime + (start[1] + end[1]) / 2

    def angle(vector: Point) -> float:
        return math.atan2(vector[1], vector[0])

    start_angle = angle(((x_prime - center_x_prime) / rx, (y_prime - center_y_prime) / ry))
    end_angle = angle(((-x_prime - center_x_prime) / rx, (-y_prime - center_y_prime) / ry))
    delta = end_angle - start_angle
    if sweep and delta < 0:
        delta += math.tau
    elif not sweep and delta > 0:
        delta -= math.tau
    return [
        (
            center_x + rx * math.cos(start_angle + delta * index / steps) * cos_phi
            - ry * math.sin(start_angle + delta * index / steps) * sin_phi,
            center_y + rx * math.cos(start_angle + delta * index / steps) * sin_phi
            + ry * math.sin(start_angle + delta * index / steps) * cos_phi,
        )
        for index in range(1, steps + 1)
    ]


def path_routes(path_data: str, *, arc_chords: bool = False) -> list[list[Point]]:
    tokens = PATH_TOKEN_RE.findall(path_data or "")
    routes: list[list[Point]] = []
    points: list[Point] = []
    index = 0
    command = ""
    current = (0.0, 0.0)
    start = current
    previous_cubic: Optional[Point] = None
    previous_quadratic: Optional[Point] = None

    def read(count: int) -> Optional[list[float]]:
        nonlocal index
        if index + count > len(tokens) or any(re.fullmatch(r"[A-Za-z]", token) for token in tokens[index : index + count]):
            return None
        values = [float(token) for token in tokens[index : index + count]]
        index += count
        return values

    def absolute(x: float, y: float, relative: bool) -> Point:
        return (current[0] + x, current[1] + y) if relative else (x, y)

    while index < len(tokens):
        if re.fullmatch(r"[A-Za-z]", tokens[index]):
            command = tokens[index]
            index += 1
        if not command:
            return []
        relative = command.islower()
        op = command.upper()
        if op == "Z":
            if current != start:
                points.append(start)
            current = start
            previous_cubic = previous_quadratic = None
            command = ""
            continue
        count = {"M": 2, "L": 2, "H": 1, "V": 1, "C": 6, "S": 4, "Q": 4, "T": 2, "A": 7}.get(op)
        if count is None:
            return []
        values = read(count)
        if values is None:
            return []

        if op == "M":
            if points:
                routes.append(points)
            current = absolute(values[0], values[1], relative)
            start = current
            points = [current]
            command = "l" if relative else "L"
        elif op == "L":
            current = absolute(values[0], values[1], relative)
            points.append(current)
        elif op == "H":
            current = (current[0] + values[0], current[1]) if relative else (values[0], current[1])
            points.append(current)
        elif op == "V":
            current = (current[0], current[1] + values[0]) if relative else (current[0], values[0])
            points.append(current)
        elif op == "C":
            first = absolute(values[0], values[1], relative)
            second = absolute(values[2], values[3], relative)
            end = absolute(values[4], values[5], relative)
            points.extend(sample_cubic(current, first, second, end))
            current, previous_cubic = end, second
            previous_quadratic = None
        elif op == "S":
            first = (2 * current[0] - previous_cubic[0], 2 * current[1] - previous_cubic[1]) if previous_cubic else current
            second = absolute(values[0], values[1], relative)
            end = absolute(values[2], values[3], relative)
            points.extend(sample_cubic(current, first, second, end))
            current, previous_cubic = end, second
            previous_quadratic = None
        elif op == "Q":
            control = absolute(values[0], values[1], relative)
            end = absolute(values[2], values[3], relative)
            points.extend(sample_quadratic(current, control, end))
            current, previous_quadratic = end, control
            previous_cubic = None
        elif op == "T":
            control = (2 * current[0] - previous_quadratic[0], 2 * current[1] - previous_quadratic[1]) if previous_quadratic else current
            end = absolute(values[0], values[1], relative)
            points.extend(sample_quadratic(current, control, end))
            current, previous_quadratic = end, control
            previous_cubic = None
        elif op == "A":
            end = absolute(values[5], values[6], relative)
            if arc_chords:
                points.append(end)
            else:
                points.extend(
                    sample_arc(
                        current,
                        values[0],
                        values[1],
                        values[2],
                        bool(values[3]),
                        bool(values[4]),
                        end,
                    )
                )
            current = end
            previous_cubic = previous_quadratic = None
        if op not in {"C", "S", "Q", "T"}:
            previous_cubic = previous_quadratic = None
    if points:
        routes.append(points)
    return routes


def edge_routes(context: ElementContext) -> list[list[Point]]:
    element = context.element
    tag = local_name(element.tag)
    explicit_edge = context.role == "edge"
    if (
        context.in_defs
        or context.role in {"background", "bridge-mask", "decoration", "label", "legend", "node", "reserved"}
        or (not explicit_edge and not has_marker(element))
    ):
        return []
    if tag == "line":
        routes = [[
            (float(parse_number(element.get("x1"), 0.0) or 0.0), float(parse_number(element.get("y1"), 0.0) or 0.0)),
            (float(parse_number(element.get("x2"), 0.0) or 0.0), float(parse_number(element.get("y2"), 0.0) or 0.0)),
        ]]
    elif tag == "polyline":
        values = [float(item) for item in NUMBER_RE.findall(element.get("points", ""))]
        routes = [list(zip(values[::2], values[1::2]))]
    elif tag == "path":
        routes = path_routes(
            element.get("d", ""),
            arc_chords=bool(element.get("data-bridges")),
        )
    else:
        return []
    return [
        [transform_point(context.matrix, point) for point in route]
        for route in routes
    ]


def segment_hits_bounds(start: Point, end: Point, bounds: Bounds, epsilon: float = 1e-5) -> bool:
    left, right = bounds.left + epsilon, bounds.right - epsilon
    top, bottom = bounds.top + epsilon, bounds.bottom - epsilon
    if left >= right or top >= bottom:
        return False
    x1, y1 = start
    dx, dy = end[0] - x1, end[1] - y1
    low, high = 0.0, 1.0
    for p, q in ((-dx, x1 - left), (dx, right - x1), (-dy, y1 - top), (dy, bottom - y1)):
        if abs(p) < epsilon:
            if q < 0:
                return False
            continue
        ratio = q / p
        if p < 0:
            low = max(low, ratio)
        else:
            high = min(high, ratio)
        if low > high:
            return False
    return high - low > epsilon and high > epsilon and low < 1 - epsilon


def points_within_bounds(points: Sequence[Point], bounds: Bounds, epsilon: float = 1e-5) -> bool:
    return bool(points) and all(
        bounds.left - epsilon <= x <= bounds.right + epsilon
        and bounds.top - epsilon <= y <= bounds.bottom + epsilon
        for x, y in points
    )


def find_collisions(root: ET.Element) -> list[Collision]:
    contexts = list(walk(root))
    obstacles = [
        (context, bounds)
        for context in contexts
        if (bounds := shape_bounds(context, canvas_size(root))) is not None
    ]
    edges = [(context, edge_routes(context)) for context in contexts]
    legend_obstacles = [
        (context, bounds)
        for context in contexts
        if context.role == "legend" and (bounds := role_bounds(context)) is not None
    ]
    obstacles.extend(legend_obstacles)
    collisions: list[Collision] = []
    for edge_context, routes in edges:
        if not any(len(route) >= 2 for route in routes):
            continue
        # A marker path wholly inside a recognized legend is decoration. The
        # legend remains a hard obstacle for every business edge outside it.
        if routes and all(
            any(points_within_bounds(route, bounds) for _, bounds in legend_obstacles)
            for route in routes
        ):
            continue
        edge = edge_context.element
        for obstacle_context, bounds in obstacles:
            obstacle = obstacle_context.element
            if any(
                segment_hits_bounds(first, second, bounds)
                for route in routes
                for first, second in zip(route, route[1:])
            ):
                collisions.append(
                    Collision(
                        describe_element(edge),
                        describe_element(obstacle),
                    )
                )
                break
    return collisions


def describe_element(element: ET.Element) -> str:
    tag = local_name(element.tag)
    if element.get("id"):
        return f"{tag}#{element.get('id')}"
    if tag == "path":
        path_data = re.sub(r"\s+", " ", element.get("d", "")).strip()
        return f"path[d={path_data[:72]}]"
    attributes = []
    for name in ("x", "y", "width", "height", "cx", "cy", "r", "rx", "ry"):
        if element.get(name) is not None:
            attributes.append(f"{name}={element.get(name)}")
    return f"{tag}[{' '.join(attributes)}]" if attributes else tag


def role_bounds(context: ElementContext) -> Optional[Bounds]:
    """Return explicit node/reserved/label bounds for the strict geometry gate."""

    declared = metadata_bounds(context)
    if declared is not None:
        return declared
    element = context.element
    tag = local_name(element.tag)
    if tag == "rect":
        x = float(parse_number(element.get("x"), 0.0) or 0.0)
        y = float(parse_number(element.get("y"), 0.0) or 0.0)
        width = parse_number(element.get("width"), None)
        height = parse_number(element.get("height"), None)
        if width is not None and height is not None and width >= 0 and height >= 0:
            return transformed_bounds(
                context.matrix,
                ((x, y), (x + width, y), (x + width, y + height), (x, y + height)),
            )
    if tag == "text" and context.role == "label":
        x = float(parse_number(element.get("x"), 0.0) or 0.0)
        y = float(parse_number(element.get("y"), 0.0) or 0.0)
        font_size = float(parse_number(element.get("font-size"), 12.0) or 12.0)
        anchor = element.get("text-anchor", "start")
        local = geometry.estimate_text_bounds(x, y, "".join(element.itertext()), font_size=font_size, anchor=anchor)
        return transformed_bounds(
            context.matrix,
            (
                (local[0], local[1]),
                (local[2], local[1]),
                (local[2], local[3]),
                (local[0], local[3]),
            ),
        )
    return None


def bounds_inside_canvas(bounds: Bounds, canvas: Bounds, epsilon: float = 1e-5) -> bool:
    return (
        bounds.left >= canvas.left - epsilon
        and bounds.top >= canvas.top - epsilon
        and bounds.right <= canvas.right + epsilon
        and bounds.bottom <= canvas.bottom + epsilon
    )


def geometry_check(root: ET.Element) -> list[str]:
    """Audit generated business geometry using explicit semantic SVG roles."""

    contexts = list(walk(root))
    paint_order = {id(context.element): index for index, context in enumerate(contexts)}
    width, height = canvas_size(root)
    canvas = Bounds(0.0, 0.0, width, height)
    details: list[str] = []

    obstacles: list[tuple[ElementContext, Bounds]] = []
    labels: list[tuple[ElementContext, Bounds]] = []
    bridge_masks: dict[str, ElementContext] = {}
    edges: list[tuple[ElementContext, list[list[Point]]]] = []
    matched_bridges: dict[str, list[Point]] = {}

    for context in contexts:
        if context.in_defs:
            continue
        if context.role in {"node", "reserved"}:
            if context.role == "node" and context.element.get("data-graph-role") != "node" and metadata_bounds(context) is None:
                continue
            bounds = role_bounds(context)
            if bounds is not None:
                obstacles.append((context, bounds))
        elif context.role == "label":
            if context.element.get("data-graph-role") != "label" and metadata_bounds(context) is None:
                continue
            bounds = role_bounds(context)
            if bounds is not None:
                labels.append((context, bounds))
        elif context.role == "bridge-mask":
            owner = context.element.get("data-owner", "")
            if owner:
                bridge_masks[owner] = context
        elif context.role == "edge":
            routes = edge_routes(context)
            if any(len(route) >= 2 for route in routes):
                edges.append((context, routes))

    for context, bounds in [*obstacles, *labels]:
        if not bounds_inside_canvas(bounds, canvas):
            details.append(f"canvas_clip: {describe_element(context.element)} exceeds viewBox")

    for edge_context, routes in edges:
        edge = edge_context.element
        edge_name = describe_element(edge)
        if not all(
            geometry.route_inside_canvas(route, (canvas.left, canvas.top, canvas.right, canvas.bottom))
            for route in routes
            if len(route) >= 2
        ):
            details.append(f"canvas_clip: {edge_name} exceeds viewBox")
        if root.get("data-generator") == "fireworks-tech-graph" and not all(
            geometry.route_is_orthogonal(route)
            for route in routes
            if len(route) >= 2
        ):
            details.append(f"non_orthogonal: {edge_name}")
        for obstacle_context, bounds in obstacles:
            obstacle_node_id = obstacle_context.element.get("data-node-id", "")
            if obstacle_node_id and obstacle_node_id in {
                edge.get("data-source", ""),
                edge.get("data-target", ""),
            }:
                continue
            if any(
                segment_hits_bounds(first, second, bounds)
                for route in routes
                for first, second in zip(route, route[1:])
            ):
                role = obstacle_context.role or "obstacle"
                details.append(
                    f"edge_{role}: {edge_name} intersects {describe_element(obstacle_context.element)}"
                )

    for first_index, (first_context, first_routes) in enumerate(edges):
        first_element = first_context.element
        first_name = describe_element(first_element)
        for second_context, second_routes in edges[first_index + 1 :]:
            second_element = second_context.element
            second_name = describe_element(second_element)
            interactions = [
                geometry.route_interactions(
                    first_route,
                    [route for route in second_routes if len(route) >= 2],
                )
                for first_route in first_routes
                if len(first_route) >= 2
            ]
            if any(item.overlap_count for item in interactions):
                details.append(f"edge_overlap: {first_name} overlaps {second_name}")
            for point in [point for item in interactions for point in item.crossings]:
                first_bridges = geometry.parse_bridge_points(first_element.get("data-bridges"))
                second_bridges = geometry.parse_bridge_points(second_element.get("data-bridges"))
                first_owner = first_element.get("data-edge-id") or first_element.get("id", "")
                second_owner = second_element.get("data-edge-id") or second_element.get("id", "")
                first_mask = bridge_masks.get(first_owner)
                second_mask = bridge_masks.get(second_owner)
                first_valid = (
                    geometry.bridge_declared(point, first_bridges)
                    and first_mask is not None
                    and paint_order[id(second_element)] < paint_order[id(first_mask.element)] < paint_order[id(first_element)]
                    and first_mask.element.get("d") == first_element.get("d")
                    and first_mask.matrix == first_context.matrix
                )
                second_valid = (
                    geometry.bridge_declared(point, second_bridges)
                    and second_mask is not None
                    and paint_order[id(first_element)] < paint_order[id(second_mask.element)] < paint_order[id(second_element)]
                    and second_mask.element.get("d") == second_element.get("d")
                    and second_mask.matrix == second_context.matrix
                )
                # Backwards compatibility for authored SVGs predating bridge masks.
                if root.get("data-generator") != "fireworks-tech-graph":
                    first_valid = first_valid or geometry.bridge_declared(point, first_bridges)
                    second_valid = second_valid or geometry.bridge_declared(point, second_bridges)
                if not (first_valid or second_valid):
                    declared = (
                        geometry.bridge_declared(point, first_bridges)
                        or geometry.bridge_declared(point, second_bridges)
                    )
                    code = "bridge_paint_order" if declared else "edge_crossing"
                    details.append(
                        f"{code}: {first_name} crosses {second_name} at {point[0]:.2f},{point[1]:.2f}"
                    )
                if first_valid:
                    matched_bridges.setdefault(first_owner, []).append(point)
                if second_valid:
                    matched_bridges.setdefault(second_owner, []).append(point)

    for label_index, (label_context, label_bounds) in enumerate(labels):
        label_name = describe_element(label_context.element)
        owner = label_context.element.get("data-owner", "")
        label_tuple = (label_bounds.left, label_bounds.top, label_bounds.right, label_bounds.bottom)
        for obstacle_context, obstacle_bounds in obstacles:
            obstacle_tuple = (obstacle_bounds.left, obstacle_bounds.top, obstacle_bounds.right, obstacle_bounds.bottom)
            if geometry.bounds_intersect(label_tuple, obstacle_tuple):
                details.append(f"label_obstacle: {label_name} intersects {describe_element(obstacle_context.element)}")
        for edge_context, edge_routes_for_context in edges:
            edge = edge_context.element
            edge_owner = edge.get("data-edge-id") or edge.get("id", "")
            if edge_owner == owner:
                continue
            if any(
                segment_hits_bounds(first, second, label_bounds)
                for route in edge_routes_for_context
                for first, second in zip(route, route[1:])
            ):
                details.append(f"label_edge: {label_name} intersects {describe_element(edge)}")
        for other_context, other_bounds in labels[label_index + 1 :]:
            other_tuple = (other_bounds.left, other_bounds.top, other_bounds.right, other_bounds.bottom)
            if geometry.bounds_intersect(label_tuple, other_tuple):
                details.append(f"label_overlap: {label_name} intersects {describe_element(other_context.element)}")

    for edge_context, _ in edges:
        edge = edge_context.element
        owner = edge.get("data-edge-id") or edge.get("id", "")
        bridges = geometry.parse_bridge_points(edge.get("data-bridges"))
        if bridges and owner not in bridge_masks and root.get("data-generator") == "fireworks-tech-graph":
            details.append(f"bridge_mask_missing: {describe_element(edge)}")
        for bridge in bridges:
            if not geometry.bridge_declared(bridge, matched_bridges.get(owner, [])):
                details.append(
                    f"bridge_without_crossing: {describe_element(edge)} declares {bridge[0]:.2f},{bridge[1]:.2f}"
                )

    return sorted(set(details))


def composition_contract(root: ET.Element) -> quality.CompositionContract:
    """Read the portable quality contract embedded in the SVG root."""

    raw: dict[str, object] = {
        "profile": root.get("data-quality-profile", "standard"),
    }
    attributes = {
        "max_bends_per_edge": "data-max-bends-per-edge",
        "max_total_bends": "data-max-total-bends",
        "max_route_stretch": "data-max-route-stretch",
        "max_bridged_crossings": "data-max-bridged-crossings",
        "min_node_gap": "data-min-node-gap",
        "min_container_gutter": "data-min-container-gutter",
        "min_label_clearance": "data-min-label-clearance",
        "min_segment_length": "data-min-segment-length",
    }
    for key, attribute in attributes.items():
        if root.get(attribute) is not None:
            raw[key] = root.get(attribute, "")
    return quality.resolve_contract(raw)


def composition_check(root: ET.Element) -> list[str]:
    """Enforce visual-composition budgets in addition to collision safety."""

    contexts = list(walk(root))
    contract = composition_contract(root)
    nodes: list[tuple[str, tuple[float, float, float, float]]] = []
    containers: list[tuple[str, tuple[float, float, float, float]]] = []
    labels: list[tuple[ElementContext, Bounds]] = []
    edges: list[tuple[ElementContext, list[list[Point]]]] = []

    for context in contexts:
        if context.in_defs:
            continue
        explicit = context.element.get("data-graph-role")
        bounds = role_bounds(context)
        if context.role == "node" and bounds is not None and (explicit == "node" or metadata_bounds(context) is not None):
            nodes.append(
                (
                    context.element.get("data-node-id") or context.element.get("id") or describe_element(context.element),
                    (bounds.left, bounds.top, bounds.right, bounds.bottom),
                )
            )
        elif context.role == "container" and bounds is not None and explicit == "container":
            containers.append(
                (
                    context.element.get("id") or describe_element(context.element),
                    (bounds.left, bounds.top, bounds.right, bounds.bottom),
                )
            )
        elif context.role == "label" and bounds is not None and (explicit == "label" or metadata_bounds(context) is not None):
            labels.append((context, bounds))
        if context.role == "edge" or (context.role is None and has_marker(context.element)):
            routes = edge_routes(context)
            if any(len(route) >= 2 for route in routes):
                edges.append((context, routes))

    edge_records = []
    for context, routes in edges:
        edge = context.element
        edge_id = edge.get("data-edge-id") or edge.get("id") or describe_element(edge)
        valid_routes = [route for route in routes if len(route) >= 2]
        declared_bridges = geometry.parse_bridge_points(edge.get("data-bridges"))
        for index, route in enumerate(valid_routes):
            edge_records.append(
                {
                    "id": edge_id if len(valid_routes) == 1 else f"{edge_id}:{index + 1}",
                    "route": route,
                    "bends": geometry.bend_count(route),
                    # Bridges are declared once per SVG edge element even when
                    # its path contains multiple independently drawn subpaths.
                    "bridges": declared_bridges if index == 0 else [],
                }
            )
    assessment = quality.assess_composition(
        nodes=nodes,
        containers=containers,
        edges=edge_records,
        contract=contract,
    )
    details = [
        f'composition_{str(item["code"]).lower()}: {item["element"]} '
        f'actual={item["actual"]} limit={item["limit"]}'
        for item in assessment["violations"]
    ]

    clearance = contract.min_label_clearance
    obstacle_bounds = [Bounds(*bounds) for _, bounds in nodes]
    for index, (label_context, label_bounds) in enumerate(labels):
        owner = label_context.element.get("data-owner", "")
        expanded = Bounds(
            label_bounds.left - clearance,
            label_bounds.top - clearance,
            label_bounds.right + clearance,
            label_bounds.bottom + clearance,
        )
        for obstacle in obstacle_bounds:
            if geometry.bounds_intersect(
                (expanded.left, expanded.top, expanded.right, expanded.bottom),
                (obstacle.left, obstacle.top, obstacle.right, obstacle.bottom),
            ):
                details.append(
                    f"composition_label_clearance: {describe_element(label_context.element)} is too close to a node"
                )
                break
        for edge_context, routes in edges:
            edge = edge_context.element
            edge_owner = edge.get("data-edge-id") or edge.get("id", "")
            if edge_owner == owner:
                continue
            if any(
                segment_hits_bounds(first, second, expanded)
                for route in routes
                for first, second in zip(route, route[1:])
            ):
                details.append(
                    f"composition_label_clearance: {describe_element(label_context.element)} is too close to {describe_element(edge)}"
                )
        for other_context, other_bounds in labels[index + 1 :]:
            if geometry.bounds_intersect(
                (expanded.left, expanded.top, expanded.right, expanded.bottom),
                (other_bounds.left, other_bounds.top, other_bounds.right, other_bounds.bottom),
            ):
                details.append(
                    f"composition_label_clearance: {describe_element(label_context.element)} is too close to {describe_element(other_context.element)}"
                )
    return sorted(set(details))


def parse_svg(path: Path) -> ET.Element:
    return ET.parse(path).getroot()


def run_check(path: Path, check: str) -> tuple[bool, list[str]]:
    try:
        root = parse_svg(path)
    except (ET.ParseError, OSError) as error:
        return False, [str(error)]
    if check == "xml":
        return True, []
    if check == "markers":
        definitions, references = marker_references(root)
        missing = sorted(references - definitions)
        return not missing, [f"missing marker: {marker}" for marker in missing]
    if check == "geometry":
        details = geometry_check(root)
        return not details, details
    if check == "composition":
        details = composition_check(root)
        return not details, details
    collisions = find_collisions(root)
    details = [
        f"{item.edge} intersects {item.obstacle}"
        for item in collisions
    ]
    return not collisions, details


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("svg_file", type=Path)
    parser.add_argument("--check", choices=("xml", "markers", "collisions", "geometry", "composition"), required=True)
    return parser.parse_args()


def main() -> int:
    args = parse_args()
    ok, details = run_check(args.svg_file, args.check)
    for detail in details:
        print(detail)
    return 0 if ok else 1


if __name__ == "__main__":
    raise SystemExit(main())
site.webmanifest
{"name":"fireworks-tech-graph","short_name":"tech-graph","description":"Describe your system; get publication-ready SVG+PNG technical diagrams.","start_url":"/fireworks-tech-graph/","scope":"/fireworks-tech-graph/","display":"standalone","background_color":"#0f0f1a","theme_color":"#0f0f1a","icons":[{"src":"agentloop-core.svg","sizes":"any","type":"image/svg+xml"}]}
skills/fireworks-tech-graph/CONTRIBUTING.md
# Contributing

Fireworks Tech Graph accepts fixes, fixtures, style documentation, and renderer improvements.

## Local checks

Requirements: Python 3.9+, Node.js 18+, and either CairoSVG or `rsvg-convert`.

```bash
python3 -m unittest discover -s tests -v
TEST_OUTPUT_DIR="$(mktemp -d)" ./scripts/test-all-styles.sh
python3 tools/check_project_consistency.py
python3 tools/distribution.py --check
./tools/install-canary.sh "$PWD/skills/fireworks-tech-graph"
```

Geometry changes must include a failing fixture or unit test before the fix. Generated SVGs must pass the `geometry` check; declared jumps require a matching bridge mask.

Run `python3 tools/distribution.py --sync` after changing any packaged file. Commit the canonical root files and the refreshed nested mirror together.

Please keep secrets, tokens, cookies, generated caches, and local test output out of commits.
scripts/svg2png.js
#!/usr/bin/env node

const fs = require("fs");
const path = require("path");
const puppeteer = require("puppeteer");

async function main() {
  const directory = path.resolve(process.argv[2] || ".");
  const svgFiles = fs.readdirSync(directory).filter((file) => file.endsWith(".svg"));
  const browser = await puppeteer.launch({
    headless: "new",
    args: ["--no-sandbox", "--disable-setuid-sandbox"],
  });

  try {
    for (const file of svgFiles) {
      const svgPath = path.join(directory, file);
      const pngPath = svgPath.replace(/\.svg$/, ".png");
      const svgContent = fs.readFileSync(svgPath, "utf8");
      const widthMatch = svgContent.match(/width="(\d+)/);
      const heightMatch = svgContent.match(/height="(\d+)/);
      const viewBoxMatch = svgContent.match(/viewBox="[^"]*\s(\d+)\s(\d+)"/);
      const width = widthMatch ? Number(widthMatch[1]) : viewBoxMatch ? Number(viewBoxMatch[1]) : 1200;
      const height = heightMatch ? Number(heightMatch[1]) : viewBoxMatch ? Number(viewBoxMatch[2]) : 800;
      const page = await browser.newPage();

      await page.setViewport({ width, height, deviceScaleFactor: 2 });
      await page.setContent(
        `<html><body style="margin:0;background:transparent"><img src="data:image/svg+xml;base64,${Buffer.from(svgContent).toString("base64")}" width="${width}" height="${height}"></body></html>`,
        { waitUntil: "networkidle0" },
      );
      await page.screenshot({ path: pngPath, type: "png", omitBackground: true });
      await page.close();
      console.log(`Done: ${file} -> ${path.basename(pngPath)} (${width}x${height} @2x)`);
    }
  } finally {
    await browser.close();
  }
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});
skills/fireworks-tech-graph/LICENSE
MIT License

Copyright (c) 2025 fireworks-tech-graph contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
scripts/interactive_html.py
#!/usr/bin/env python3
"""Build a safe, self-contained interactive HTML viewer for an SVG diagram."""

from __future__ import annotations

import argparse
import html
import json
import re
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Mapping, Optional, Sequence


ET.register_namespace("", "http://www.w3.org/2000/svg")
SVG_NAMESPACE = "http://www.w3.org/2000/svg"
XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace"
SAFE_ELEMENTS = {
    "circle",
    "clippath",
    "defs",
    "desc",
    "ellipse",
    "feblend",
    "fecolormatrix",
    "fecomposite",
    "fedropshadow",
    "feflood",
    "fegaussianblur",
    "femerge",
    "femergenode",
    "feoffset",
    "filter",
    "g",
    "line",
    "lineargradient",
    "marker",
    "mask",
    "path",
    "pattern",
    "polygon",
    "polyline",
    "radialgradient",
    "rect",
    "stop",
    "style",
    "svg",
    "text",
    "title",
    "tspan",
}
COMMON_ATTRIBUTES = {
    "aria-describedby",
    "aria-hidden",
    "aria-label",
    "aria-labelledby",
    "class",
    "clip-path",
    "clip-rule",
    "color",
    "color-interpolation",
    "color-interpolation-filters",
    "display",
    "fill",
    "fill-opacity",
    "fill-rule",
    "filter",
    "focusable",
    "id",
    "mask",
    "marker-end",
    "marker-mid",
    "marker-start",
    "opacity",
    "paint-order",
    "role",
    "shape-rendering",
    "stroke",
    "stroke-dasharray",
    "stroke-dashoffset",
    "stroke-linecap",
    "stroke-linejoin",
    "stroke-miterlimit",
    "stroke-opacity",
    "stroke-width",
    "tabindex",
    "text-rendering",
    "transform",
    "vector-effect",
    "visibility",
}
ELEMENT_ATTRIBUTES = {
    "svg": {"height", "preserveaspectratio", "viewbox", "width", "x", "y"},
    "rect": {"height", "rx", "ry", "width", "x", "y"},
    "circle": {"cx", "cy", "r"},
    "ellipse": {"cx", "cy", "rx", "ry"},
    "line": {"x1", "x2", "y1", "y2"},
    "polyline": {"pathlength", "points"},
    "polygon": {"pathlength", "points"},
    "path": {"d", "pathlength"},
    "text": {
        "dominant-baseline",
        "dx",
        "dy",
        "font-family",
        "font-size",
        "font-style",
        "font-weight",
        "letter-spacing",
        "text-anchor",
        "word-spacing",
        "x",
        "y",
    },
    "tspan": {"dominant-baseline", "dx", "dy", "text-anchor", "x", "y"},
    "marker": {
        "markerheight",
        "markerunits",
        "markerwidth",
        "orient",
        "preserveaspectratio",
        "refx",
        "refy",
        "viewbox",
    },
    "lineargradient": {"gradienttransform", "gradientunits", "spreadmethod", "x1", "x2", "y1", "y2"},
    "radialgradient": {"cx", "cy", "fr", "fx", "fy", "gradienttransform", "gradientunits", "r", "spreadmethod"},
    "stop": {"offset", "stop-color", "stop-opacity"},
    "pattern": {
        "height",
        "patterncontentunits",
        "patterntransform",
        "patternunits",
        "preserveaspectratio",
        "viewbox",
        "width",
        "x",
        "y",
    },
    "clippath": {"clippathunits"},
    "mask": {"height", "maskcontentunits", "maskunits", "width", "x", "y"},
    "filter": {"filterunits", "height", "primitiveunits", "width", "x", "y"},
    "fedropshadow": {"dx", "dy", "flood-color", "flood-opacity", "stddeviation"},
    "fegaussianblur": {"in", "result", "stddeviation"},
    "feoffset": {"dx", "dy", "in", "result"},
    "feflood": {"flood-color", "flood-opacity", "result"},
    "fecomposite": {"in", "in2", "k1", "k2", "k3", "k4", "operator", "result"},
    "femerge": {"result"},
    "femergenode": {"in"},
    "fecolormatrix": {"in", "result", "type", "values"},
    "feblend": {"in", "in2", "mode", "result"},
}
LOCAL_URL_ATTRIBUTES = {"clip-path", "fill", "filter", "marker-end", "marker-mid", "marker-start", "mask", "stroke"}
LOCAL_URL_RE = re.compile(r"url\(\s*(['\"]?)#[A-Za-z_][\w:.-]*\1\s*\)\Z", re.IGNORECASE)
ACTIVE_VALUE_RE = re.compile(r"(?:javascript\s*:|vbscript\s*:|data\s*:)", re.IGNORECASE)
UNSAFE_CSS_RE = re.compile(
    r"(?:@|\\|url\s*\(|expression\s*\(|javascript\s*:|vbscript\s*:|data\s*:|https?\s*:|//|behavior\s*:|-moz-binding)",
    re.IGNORECASE,
)


def _local_name(value: str) -> str:
    return value.rsplit("}", 1)[-1].lower()


def _namespace(value: str) -> str:
    return value[1:].split("}", 1)[0] if value.startswith("{") else ""


def sanitize_svg(svg_text: str) -> str:
    """Return safe inline SVG or raise ValueError for active/external content."""

    if len(svg_text.encode("utf-8")) > 20 * 1024 * 1024:
        raise ValueError("SVG exceeds the 20 MiB interactive-export limit")
    if re.search(r"<!\s*(?:DOCTYPE|ENTITY)\b", svg_text, re.IGNORECASE):
        raise ValueError("DTD and entity declarations are not allowed")
    try:
        root = ET.fromstring(svg_text)
    except ET.ParseError as error:
        raise ValueError(f"invalid SVG: {error}") from error
    if _local_name(root.tag) != "svg":
        raise ValueError("input root must be <svg>")

    for element in root.iter():
        tag = _local_name(element.tag)
        if _namespace(element.tag) not in {"", SVG_NAMESPACE}:
            raise ValueError(f"foreign XML namespace is not allowed: {tag}")
        if tag not in SAFE_ELEMENTS:
            raise ValueError(f"unsupported SVG element: {tag}")
        if tag == "style" and UNSAFE_CSS_RE.search(element.text or ""):
            raise ValueError("external or active CSS is not allowed")
        allowed_attributes = COMMON_ATTRIBUTES | ELEMENT_ATTRIBUTES.get(tag, set())
        for raw_name, raw_value in element.attrib.items():
            name = _local_name(raw_name)
            value = raw_value.strip()
            if name.startswith("on"):
                raise ValueError(f"event handler attribute is not allowed: {name}")
            namespace = _namespace(raw_name)
            if namespace and not (namespace == XML_NAMESPACE and name == "space"):
                raise ValueError(f"foreign attribute namespace is not allowed: {name}")
            if name != "space" and name not in allowed_attributes and not name.startswith(("aria-", "data-")):
                raise ValueError(f"unsupported SVG attribute on <{tag}>: {name}")
            if any(ord(character) < 32 and character not in "\t\n\r" for character in value):
                raise ValueError(f"control character is not allowed in attribute: {name}")
            if ACTIVE_VALUE_RE.search(value):
                raise ValueError(f"active reference is not allowed: {name}")
            if "url(" in value.lower():
                if name not in LOCAL_URL_ATTRIBUTES or not LOCAL_URL_RE.fullmatch(value):
                    raise ValueError(f"external reference is not allowed: {value}")
    root.set("role", root.get("role", "img"))
    root.set("focusable", "false")
    return ET.tostring(root, encoding="unicode")


def build_interactive_html(
    svg_text: str,
    title: str,
    metadata: Optional[Mapping[str, object]] = None,
) -> str:
    safe_svg = sanitize_svg(svg_text)
    safe_title = html.escape(title, quote=True)
    metadata_json = json.dumps(dict(metadata or {}), ensure_ascii=False, sort_keys=True).replace("<", "\\u003c")
    return f"""<!doctype html>
<html lang="en" data-theme="dark">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; img-src data: blob:">
  <title>{safe_title}</title>
  <style>
    :root {{ color-scheme: dark; --page:#0f0f1a; --panel:#0f172a; --border:#334155; --text:#e2e8f0; --muted:#94a3b8; --accent:#a855f7; }}
    html[data-theme="light"] {{ color-scheme:light; --page:#f8fafc; --panel:#fff; --border:#cbd5e1; --text:#0f172a; --muted:#475569; --accent:#7c3aed; }}
    * {{ box-sizing:border-box; }}
    html,body {{ width:100%; height:100%; margin:0; overflow:hidden; background:linear-gradient(135deg,var(--page),#1a1a2e); color:var(--text); font-family:'SF Mono','Fira Code',ui-monospace,monospace; }}
    body {{ display:grid; grid-template-rows:auto 1fr; }}
    .toolbar {{ display:flex; flex-wrap:wrap; align-items:center; gap:8px; padding:10px 14px; background:color-mix(in srgb,var(--panel) 92%,transparent); border-bottom:1px solid var(--border); z-index:2; }}
    .title {{ margin-right:auto; font-size:13px; font-weight:700; }}
    button,select {{ min-height:34px; padding:6px 10px; color:var(--text); background:var(--panel); border:1px solid var(--border); border-radius:8px; font:inherit; cursor:pointer; }}
    button:hover,button:focus-visible,select:focus-visible {{ border-color:var(--accent); outline:2px solid color-mix(in srgb,var(--accent) 35%,transparent); outline-offset:1px; }}
    #stage {{ position:relative; overflow:hidden; touch-action:none; cursor:grab; }}
    #stage.dragging {{ cursor:grabbing; }}
    #canvas {{ width:100%; height:100%; display:grid; place-items:center; transform-origin:0 0; will-change:transform; }}
    #canvas svg {{ max-width:calc(100vw - 40px); max-height:calc(100vh - 92px); width:auto; height:auto; filter:drop-shadow(0 22px 60px rgba(0,0,0,.28)); user-select:none; }}
    #status {{ min-width:76px; color:var(--muted); font-size:12px; text-align:center; }}
    .sr-only {{ position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; }}
    @media (prefers-reduced-motion:reduce) {{ * {{ scroll-behavior:auto!important; transition:none!important; }} }}
  </style>
</head>
<body>
  <header class="toolbar" aria-label="Diagram controls">
    <span class="title">{safe_title}</span>
    <button type="button" data-action="zoom-out" aria-label="Zoom out">−</button>
    <button type="button" data-action="reset" aria-label="Reset view">Reset</button>
    <button type="button" data-action="zoom-in" aria-label="Zoom in">+</button>
    <span id="status" aria-live="polite">100%</span>
    <button type="button" data-action="theme" aria-label="Toggle theme">Theme</button>
    <button type="button" data-action="copy" aria-label="Copy SVG source">Copy SVG</button>
    <select id="scale" aria-label="Raster export scale"><option value="1">1×</option><option value="2" selected>2×</option><option value="3">3×</option><option value="4">4×</option></select>
    <select id="format" aria-label="Export format"><option>SVG</option><option>PNG</option><option>JPEG</option><option>WebP</option></select>
    <button type="button" data-action="download">Export</button>
  </header>
  <main id="stage" tabindex="0" aria-label="Interactive diagram. Drag to pan; use plus and minus to zoom.">
    <div id="canvas">{safe_svg}</div>
    <p class="sr-only">Keyboard shortcuts: plus and minus zoom, zero resets, T toggles theme, S exports.</p>
  </main>
  <script>
  (() => {{
    'use strict';
    const metadata = {metadata_json};
    const stage = document.getElementById('stage');
    const canvas = document.getElementById('canvas');
    const svg = canvas.querySelector('svg');
    const status = document.getElementById('status');
    const scaleSelect = document.getElementById('scale');
    const formatSelect = document.getElementById('format');
    let view = {{ x:0, y:0, scale:1 }};
    let drag = null;
    const clamp = value => Math.max(.2, Math.min(8, value));
    const render = () => {{
      canvas.style.transform = `translate(${{view.x}}px,${{view.y}}px) scale(${{view.scale}})`;
      status.textContent = `${{Math.round(view.scale * 100)}}%`;
    }};
    const zoom = (factor, originX=stage.clientWidth/2, originY=stage.clientHeight/2) => {{
      const next = clamp(view.scale * factor);
      const ratio = next / view.scale;
      view.x = originX - (originX - view.x) * ratio;
      view.y = originY - (originY - view.y) * ratio;
      view.scale = next; render();
    }};
    const reset = () => {{ view = {{x:0,y:0,scale:1}}; render(); }};
    stage.addEventListener('wheel', event => {{ event.preventDefault(); const rect=stage.getBoundingClientRect(); zoom(event.deltaY < 0 ? 1.12 : .89, event.clientX-rect.left, event.clientY-rect.top); }}, {{passive:false}});
    stage.addEventListener('pointerdown', event => {{ drag={{id:event.pointerId,x:event.clientX,y:event.clientY,vx:view.x,vy:view.y}}; stage.setPointerCapture(event.pointerId); stage.classList.add('dragging'); }});
    stage.addEventListener('pointermove', event => {{ if(!drag||drag.id!==event.pointerId)return; view.x=drag.vx+event.clientX-drag.x; view.y=drag.vy+event.clientY-drag.y; render(); }});
    const endDrag = () => {{ drag=null; stage.classList.remove('dragging'); }};
    stage.addEventListener('pointerup', endDrag); stage.addEventListener('pointercancel', endDrag);
    const source = () => new XMLSerializer().serializeToString(svg);
    const saveBlob = (blob, extension) => {{ const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download=`${{(metadata.slug||'fireworks-tech-graph')}}.${{extension}}`; a.click(); setTimeout(()=>URL.revokeObjectURL(a.href),1000); }};
    const copySource = async () => {{
      const value=source();
      try {{ await navigator.clipboard.writeText(value); status.textContent='Copied'; return; }} catch {{}}
      const area=document.createElement('textarea'); area.value=value; area.setAttribute('readonly',''); area.style.position='fixed'; area.style.opacity='0'; document.body.appendChild(area); area.select();
      status.textContent=document.execCommand('copy')?'Copied':'Copy failed'; area.remove();
    }};
    const exportDiagram = async () => {{
      const format = formatSelect.value.toLowerCase();
      if(format==='svg') {{ saveBlob(new Blob([source()],{{type:'image/svg+xml;charset=utf-8'}}),'svg'); return; }}
      const scale = Math.max(1,Math.min(4,Number(scaleSelect.value)||2));
      const box = svg.viewBox.baseVal; const width=box.width||svg.clientWidth; const height=box.height||svg.clientHeight;
      const image = new Image(); const url=URL.createObjectURL(new Blob([source()],{{type:'image/svg+xml'}}));
      await new Promise((resolve,reject)=>{{ image.onload=resolve; image.onerror=reject; image.src=url; }});
      const raster=document.createElement('canvas'); raster.width=Math.round(width*scale); raster.height=Math.round(height*scale);
      const ctx=raster.getContext('2d'); if(format==='jpeg'){{ctx.fillStyle='#ffffff';ctx.fillRect(0,0,raster.width,raster.height);}} ctx.drawImage(image,0,0,raster.width,raster.height); URL.revokeObjectURL(url);
      const mime=format==='jpeg'?'image/jpeg':format==='webp'?'image/webp':'image/png';
      const blob=await new Promise(resolve=>raster.toBlob(resolve,mime,.94)); if(blob) saveBlob(blob,format==='jpeg'?'jpg':format);
    }};
    document.querySelector('.toolbar').addEventListener('click', async event => {{
      const action=event.target.closest('[data-action]')?.dataset.action; if(!action)return;
      if(action==='zoom-in')zoom(1.2); else if(action==='zoom-out')zoom(.83); else if(action==='reset')reset();
      else if(action==='theme')document.documentElement.dataset.theme=document.documentElement.dataset.theme==='dark'?'light':'dark';
      else if(action==='download')await exportDiagram();
      else if(action==='copy') await copySource();
    }});
    document.addEventListener('keydown', event => {{
      if(event.target.matches('select'))return;
      if(event.key==='+'||event.key==='=')zoom(1.2); else if(event.key==='-')zoom(.83); else if(event.key==='0'||event.key.toLowerCase()==='r')reset();
      else if(event.key.toLowerCase()==='t')document.querySelector('[data-action=theme]').click(); else if(event.key.toLowerCase()==='s'){{event.preventDefault();exportDiagram();}}
    }});
    render();
  }})();
  </script>
</body>
</html>
"""


def parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("svg_file", type=Path)
    parser.add_argument("output_html", type=Path)
    parser.add_argument("--title")
    parser.add_argument("--slug", default="fireworks-tech-graph")
    return parser.parse_args(argv)


def main(argv: Optional[Sequence[str]] = None) -> int:
    args = parse_args(argv)
    svg_text = args.svg_file.read_text(encoding="utf-8")
    output = build_interactive_html(svg_text, args.title or args.svg_file.stem, {"slug": args.slug})
    args.output_html.parent.mkdir(parents=True, exist_ok=True)
    args.output_html.write_text(output, encoding="utf-8")
    print(f"✓ Interactive HTML: {args.output_html}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
scripts/validate-svg.sh
#!/bin/bash
# SVG Validation Script
# Checks SVG syntax and reports detailed errors

set -euo pipefail

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

if [ $# -eq 0 ]; then
    echo "Usage: $0 <svg-file>"
    exit 1
fi

SVG_FILE="$1"

if [ ! -f "$SVG_FILE" ]; then
    echo -e "${RED}Error: File not found: $SVG_FILE${NC}"
    exit 1
fi

echo "Validating SVG: $SVG_FILE"
echo "----------------------------------------"

FAILURES=0

# Check 0: XML structure and attribute syntax
echo -n "Checking XML structure... "
if XML_ERR=$(python3 "${SCRIPT_DIR}/validate_svg.py" "$SVG_FILE" --check xml 2>&1); then
    echo -e "${GREEN}✓ Pass${NC}"
else
    echo -e "${RED}✗ Fail${NC}"
    echo "$XML_ERR"
    FAILURES=$((FAILURES + 1))
fi

# Check 1: Marker references
echo -n "Checking marker references... "
if MARKER_ERR=$(python3 "${SCRIPT_DIR}/validate_svg.py" "$SVG_FILE" --check markers 2>&1); then
    echo -e "${GREEN}✓ Pass${NC}"
else
    echo -e "${RED}✗ Fail${NC}"
    echo "$MARKER_ERR"
    FAILURES=$((FAILURES + 1))
fi

# Check 2: Arrow-component collision
echo -n "Checking arrow collisions... "
set +e
COLLISION_ERR=$(python3 "${SCRIPT_DIR}/validate_svg.py" "$SVG_FILE" --check collisions 2>&1)
COLLISION_EXIT=$?
set -e

if [ "$COLLISION_EXIT" -eq 0 ]; then
    echo -e "${GREEN}✓ Pass${NC}"
else
    echo -e "${RED}✗ Fail${NC}"
    echo "$COLLISION_ERR" | sed -n '1,8p'
    FAILURES=$((FAILURES + 1))
fi

# Check 3: semantic geometry contract for generated artifacts
echo -n "Checking semantic geometry... "
if GEOMETRY_ERR=$(python3 "${SCRIPT_DIR}/validate_svg.py" "$SVG_FILE" --check geometry 2>&1); then
    echo -e "${GREEN}✓ Pass${NC}"
else
    echo -e "${RED}✗ Fail${NC}"
    echo "$GEOMETRY_ERR" | sed -n '1,12p'
    FAILURES=$((FAILURES + 1))
fi

# Check 4: composition-quality budget
echo -n "Checking composition quality... "
if COMPOSITION_ERR=$(python3 "${SCRIPT_DIR}/validate_svg.py" "$SVG_FILE" --check composition 2>&1); then
    echo -e "${GREEN}✓ Pass${NC}"
else
    echo -e "${RED}✗ Fail${NC}"
    echo "$COMPOSITION_ERR" | sed -n '1,12p'
    FAILURES=$((FAILURES + 1))
fi

# Check 5: render validation (cairosvg preferred, rsvg-convert fallback)
echo -n "Running render validation... "
RENDER_OK=false
RENDER_TOOL=""
RENDER_ERR=""
RENDER_OUTPUT=$(mktemp "${TMPDIR:-/tmp}/fireworks-tech-graph.XXXXXX")
trap 'rm -f "$RENDER_OUTPUT"' EXIT

if python3 -c "import cairosvg" 2>/dev/null; then
    RENDER_TOOL="cairosvg"
    if RENDER_ERR=$(python3 -c "import sys, cairosvg; cairosvg.svg2png(url=sys.argv[1], write_to=sys.argv[2])" "$SVG_FILE" "$RENDER_OUTPUT" 2>&1); then
        RENDER_OK=true
    fi
elif command -v rsvg-convert &> /dev/null; then
    RENDER_TOOL="rsvg-convert"
    if RENDER_ERR=$(rsvg-convert "$SVG_FILE" -o "$RENDER_OUTPUT" 2>&1); then
        RENDER_OK=true
    fi
fi

if [ "$RENDER_OK" = true ]; then
    echo -e "${GREEN}✓ Pass${NC} (via ${RENDER_TOOL})"
    rm -f "$RENDER_OUTPUT"
elif [ -n "$RENDER_TOOL" ]; then
    echo -e "${RED}✗ Fail${NC} (via ${RENDER_TOOL})"
    echo "${RENDER_TOOL} error:"
    echo "$RENDER_ERR"
    FAILURES=$((FAILURES + 1))
else
    echo -e "${RED}✗ Fail${NC} (no renderer found — install with: python3 -m pip install cairosvg)"
    FAILURES=$((FAILURES + 1))
fi

echo "----------------------------------------"
if [ "$FAILURES" -eq 0 ]; then
    echo "Validation complete"
    exit 0
fi

echo -e "${RED}Validation failed (${FAILURES} error(s))${NC}"
exit 1
skills/fireworks-tech-graph/README.md
[English](README.md) | [中文](README.zh.md)

[Release history](docs/releases/README.md) · [Changelog](CHANGELOG.md)

# fireworks-tech-graph

> **Stop drawing diagrams by hand.** Describe your system in English or Chinese — get geometry-safe SVG, PNG, focused SVG-to-GIF motion, and offline interactive technical diagrams.

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![GitHub Release](https://img.shields.io/github/v/release/yizhiyanhua-ai/fireworks-tech-graph)](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/releases)
[![Codex Skill](https://img.shields.io/badge/Codex-Skill-10a37f)](https://learn.chatgpt.com/docs/build-skills)
[![Claude Code Skill](https://img.shields.io/badge/Claude%20Code-Skill-d97757)](https://code.claude.com/docs/en/skills)
[![12 Visual Styles](https://img.shields.io/badge/Styles-12-purple)]()
[![14 Diagram Types](https://img.shields.io/badge/Diagram%20Types-14-green)]()
[![UML Support](https://img.shields.io/badge/UML-Full%20Support-orange)]()

---

## Overview

`fireworks-tech-graph` is one Agent Skill that works unchanged in **Codex and Claude Code**. It turns natural language descriptions into polished, geometry-checked SVG diagrams, high-resolution PNGs, validated SVG-to-GIF semantic motion, and offline interactive HTML. The focused animation path accepts a generated semantic SVG and emits one compact, probed GIF. It ships with **11 generator-backed styles** and **1 AI-authored style (Dark Luxury)**. Four engineering-first styles add executable contracts for C4 reviews, cloud deployments, event streams, and reliability investigations, alongside deep AI/Agent domain patterns and all 14 UML diagram types.

```
User: "Generate a Mem0 memory architecture diagram, dark style"
  → Skill classifies: Memory Architecture Diagram, Style 2
  → Generates SVG with swim lanes, cylinders, semantic arrows
  → Exports 1920px PNG
  → Reports: mem0-architecture.svg / mem0-architecture.png
```

---

## Work With the Builder

This project is also a proof surface for a broader capability: turning vague AI/devtool workflows into constrained, reusable systems with validation, documentation, export paths, and product-facing polish.

If you are building agent infrastructure, AI IDEs, internal copilots, developer tools, technical documentation systems, or applied AI workflow products, I am open to scoped paid sprints, design-partner work, and founding engineer conversations.

- Founder-facing profile: https://bradzhang.dev/en
- Commercial case study: https://bradzhang.dev/en/case-studies/fireworks-tech-graph
- Work with me: https://bradzhang.dev/en/work-with-me

---

## Showcase

> The animated previews use the user-approved 5.75-second settled-flow timeline: routes draw in first, then the final topology keeps live data moving for two additional seconds. Each full-size GIF is 960px wide at 20fps / 115 frames; the 3×4 overview is an optimized 1200px preview. Lossless 1920px PNGs remain in `assets/samples/` as static regression baselines.

![Animated 12-style showcase — one distinct engineering scenario per style](assets/samples/showcase-12-styles.gif)

The v1.2.0 overview above and every full-size animated sample below come from the approved regression set. Each style keeps a distinct scenario while sharing the same geometry, text-fit, wire-routing, and semantic-motion quality gates.

### Style 1 — Flat Icon (default)
*Mem0 Memory Architecture — personal-memory extraction, conflict resolution, storage, and retrieval*
![Style 1 — Flat Icon](assets/samples/sample-style1-flat.gif)

### Style 2 — Dark Terminal
*Tool Call Flow — dark terminal execution, source grounding, retrieval, and answer synthesis*
![Style 2 — Dark Terminal](assets/samples/sample-style2-dark.gif)

### Style 3 — Blueprint
*Microservices Architecture — engineering grid, domain services, data stores, events, and telemetry*
![Style 3 — Blueprint](assets/samples/sample-style3-blueprint.gif)

### Style 4 — Notion Clean
*Agent Memory Types — minimal hierarchy from sensory and working context to durable memory*
![Style 4 — Notion Clean](assets/samples/sample-style4-notion.gif)

### Style 5 — Glassmorphism
*Multi-Agent Collaboration — coordinator, specialists, shared state, review, and synthesis*
![Style 5 — Glassmorphism](assets/samples/sample-style5-glass.gif)

### Style 6 — Claude Official
*System Architecture — warm interface, runtime, safety, memory, tools, and operations layers*
![Style 6 — Claude Official](assets/samples/sample-style6-claude.gif)

### Style 7 — OpenAI Official
*API Integration Flow — clean SDK, prompt, model, tool, delivery, and release stages*
![Style 7 — OpenAI Official](assets/samples/sample-style7-openai.gif)

### Style 8 — Dark Luxury *(AI-authored)*
*Agent Runtime Architecture — control plane, execution and state layers, champagne-gold structure, semantic color buckets*
![Style 8 — Dark Luxury](assets/samples/sample-style8-dark-luxury.gif)

### Style 9 — C4 Review Canvas
*Checkout Container Review — one abstraction level, explicit responsibilities, technologies, and protocols*
![Style 9 — C4 Review Canvas](assets/samples/sample-style9-c4-review-canvas.gif)

### Style 10 — Cloud Fabric
*Active–Active Checkout Deployment — global ingress, regions, VPC ownership, and cross-region replication*
![Style 10 — Cloud Fabric](assets/samples/sample-style10-cloud-fabric.gif)

### Style 11 — Event Transit
*Checkout Event Line — topics as rails, processors as stations, a declared junction, DLQ, and state projection*
![Style 11 — Event Transit](assets/samples/sample-style11-event-transit.gif)

### Style 12 — Ops Pulse
*Checkout Reliability Pulse — golden signals, one critical path, OTel export, and a correlated trace*
![Style 12 — Ops Pulse](assets/samples/sample-style12-ops-pulse.gif)

---

## Stable Prompt Recipe

The public showcase keeps a distinct domain scene for every style. They remain comparable because every fixture passes the same executable composition contract. A same-topology regression set remains internal under `fixtures/quality-baseline/`.

```text
Draw the scenario assigned to style N:
1 Mem0 Memory Architecture; 2 Tool Call Flow; 3 Microservices Architecture;
4 Agent Memory Types; 5 Multi-Agent Collaboration; 6 System Architecture;
7 API Integration Flow; 8 Agent Runtime Architecture; 9 C4 Checkout Review;
10 Active–Active Cloud Deployment; 11 Checkout Event Line; 12 Checkout Reliability Pulse.
Preserve the scenario-specific nodes, sections, and reading direction.
Apply the showcase composition contract: zero crossings, zero bridge jumps, at most two bends per edge,
at most eight bends overall, at least 40px between nodes, at least 20px container gutter,
short orthogonal segments, and labels kept clear of nodes, routes, and section headers.
Preserve the selected style's typography, palette, card material, and brand details.
```

For the four engineering-first styles, use one of these prompt fingerprints so
the router selects the domain contract as well as the visual theme:

```text
Style 9 · C4 review board: show one C4 level, responsibilities, technologies, review state, and relationship protocols.
Style 10 · Multi-region deployment map: show global ingress, Region/VPC ownership, neutral cloud glyphs, deployment mode, and named boundary mechanisms.
Style 11 · Event metro map: show thin topic rails, numbered processor stations, declared junctions, consumer groups, DLQ, and state projections.
Style 12 · Reliability pulse: show one observation window, four golden signals per service, numbered critical hops, telemetry export, and one correlated trace.
```

Replace `N` with `1`–`12`. Style 8 remains AI-authored and loads `references/style-8-dark-luxury.md`; Styles 9–12 also enforce their engineering semantic contract. All styles load `references/composition-quality-contract.md`.

---

## Features

- **12 visual styles** — 11 generator-backed profiles + 1 AI-authored style (Dark Luxury)
- **Engineering semantic contracts** — C4 abstraction levels, deployment ownership, event-rail topology, and exact golden signals fail closed before rendering
- **Executable style system** — style guides are encoded into the generator, not only documented in markdown
- **Shared composition-quality contract** — every official style enforces zero crossings/bridges, ≤2 bends per edge, route-stretch, spacing, gutter, micro-segment, and label-clearance budgets
- **14 diagram types** — Full UML support (Class, Component, Deployment, Package, Composite Structure, Object, Use Case, Activity, State Machine, Sequence, Communication, Timing, Interaction Overview, ER Diagram) plus AI/Agent domain diagrams
- **AI/Agent domain patterns** — RAG, Agentic Search, Mem0, Multi-Agent, Tool Call, and more built-in
- **Semantic shape vocabulary** — LLM = double-border rect, Agent = hexagon, Vector Store = ringed cylinder
- **Semantic arrow system** — color + dash pattern encode meaning (write vs read vs async vs loop)
- **Geometry-safe routing** — deterministic orthogonal routes, exact waypoints, distinct ports, automatic legend relocation, labels kept inside the canvas, and verified bridge jumps for unavoidable crossings
- **Versioned diagram IR** — legacy JSON normalizes to schema v1; duplicate IDs, dangling references, malformed waypoints, and non-finite geometry fail before rendering
- **Structured SVG validation** — XML and marker integrity plus semantic node, reserved-region, label, canvas, edge-overlap, and edge-crossing checks
- **Unified CLI + interactive export** — render, validate, inspect, and export one offline HTML file with pan/zoom, themes, copy, and SVG/PNG/JPEG/WebP output up to 4×
- **Focused semantic GIF motion** — generated SVG in, validated GIF out; connectors begin absent and draw in semantic order. All twelve style contracts are user-approved. The shared `+2s-settled-flow` timing revision is also user-approved, so the default 5.75s/115-frame loop holds full settled flow on frames 38–109, then resets on 110–114
- **Visual review gate** — exported PNGs are inspected for clipping, overlap, label placement, and routing regressions before delivery
- **Product icons** — 40+ products with brand colors: OpenAI, Anthropic, Pinecone, Weaviate, Kafka, PostgreSQL…
- **Swim lane grouping** — automatic layer labeling for complex architectures
- **SVG + PNG output** — SVG for editing, 1920px PNG for embedding
- **Renderer-friendly** — pure inline SVG, no external font fetching; renders cleanly in cairosvg, rsvg-convert, and headless Chrome

---

## Loop Engineering

The first render is treated as a candidate, not an automatic final result. `fireworks-tech-graph` uses an agent-driven, bounded validation feedback loop to move each diagram toward a verified deliverable:

```text
Prompt
  → Diagram Contract
  → Semantic IR
  → Style Spec
  → Route Planner
  → SVG Build
  → Structural Validation
  → PNG Visual Readback
  → Targeted Revision
  → Verified SVG + PNG
```

The loop follows five design principles:

1. **Evaluate, don't assert** — completion is backed by validator and render evidence, not by the model saying the diagram looks correct.
2. **Deterministic checks first** — XML structure, marker integrity, path geometry, arrow-component collisions, and renderability are checked before visual judgment.
3. **Perceptual validation second** — the exported PNG is read back to inspect clipping, label collisions, hierarchy, whitespace, and routing quality that syntax checks cannot see.
4. **Targeted correction** — each pass changes only the diagnosed labels, coordinates, corridors, or spacing, then reruns validation and rendering.
5. **Bounded convergence** — visual review allows at most two focused correction passes by default, preventing an unbounded self-editing loop.

The loop is observable in the final status:

```text
validation: passed
visual_review: passed
```

If the runtime cannot read images, the skill reports `visual_review: skipped (image reader unavailable)` explicitly. The workflow remains bounded and auditable; it does not claim visual verification without image evidence.

---

## Installation

### Recommended: install the complete skill for both runtimes

Use the real nested skill path. The final `/skills/fireworks-tech-graph` segment is required because a bare repository install can select only the root `SKILL.md` in current versions of `skills` CLI.

```bash
npx -y skills@1.5.17 add \
  yizhiyanhua-ai/fireworks-tech-graph/skills/fireworks-tech-graph \
  --agent codex claude-code -g -y --copy
```

This creates complete copies at `~/.agents/skills/fireworks-tech-graph` for Codex and `~/.claude/skills/fireworks-tech-graph` for Claude Code, including scripts, schemas, fixtures, templates, tests, references, and metadata.

### Editable Git checkout for Codex

```bash
mkdir -p ~/.agents/skills
git clone https://github.com/yizhiyanhua-ai/fireworks-tech-graph.git ~/.agents/skills/fireworks-tech-graph
```

Codex discovers personal skills from `~/.agents/skills` and reads the optional `agents/openai.yaml` metadata included in this repository.

### Editable Git checkout for Claude Code

```bash
mkdir -p ~/.claude/skills
git clone https://github.com/yizhiyanhua-ai/fireworks-tech-graph.git ~/.claude/skills/fireworks-tech-graph
```

Claude Code discovers personal skills from `~/.claude/skills` and ignores the Codex-only UI metadata.

### One editable checkout shared by Codex and Claude Code

For a fresh install with Claude Code 2.1.203 or newer, keep one checkout and link both discovery paths to it. Move any existing destinations aside before creating the links.

```bash
mkdir -p ~/.local/share/agent-skills ~/.agents/skills ~/.claude/skills
git clone https://github.com/yizhiyanhua-ai/fireworks-tech-graph.git ~/.local/share/agent-skills/fireworks-tech-graph
ln -s ~/.local/share/agent-skills/fireworks-tech-graph ~/.agents/skills/fireworks-tech-graph
ln -s ~/.local/share/agent-skills/fireworks-tech-graph ~/.claude/skills/fireworks-tech-graph
```

This keeps `SKILL.md`, references, scripts, templates, and future updates identical in both agents. The npm registry is a separate distribution channel and may lag GitHub Releases. For the current Skill version, use the nested GitHub path above; the npm page remains available for package metadata:

```text
https://www.npmjs.com/package/@yizhiyanhua-ai/fireworks-tech-graph
```

## Update

For a `skills` CLI copy, rerun the recommended nested-path command. For Git installations, update whichever checkout you installed:

```bash
git -C ~/.agents/skills/fireworks-tech-graph pull
# or
git -C ~/.claude/skills/fireworks-tech-graph pull
# or, for the shared checkout
git -C ~/.local/share/agent-skills/fireworks-tech-graph pull
```

After the first install, restart Codex and Claude Code so both discover the skill. Later `SKILL.md` edits are detected automatically; restart the runtime after changing bundled scripts or references if the update is not visible.

The shell commands above target macOS, Linux, WSL, and Git Bash. On native Windows, use the equivalent `%USERPROFILE%\.agents\skills` and `%USERPROFILE%\.claude\skills` paths. Python 3.9+ is required; the optional Puppeteer path requires Node.js 18+.

---

## Unified CLI

```bash
SKILL_ROOT="${CLAUDE_SKILL_DIR:-$HOME/.agents/skills/fireworks-tech-graph}"

python3 "$SKILL_ROOT/scripts/fireworks.py" doctor
python3 "$SKILL_ROOT/scripts/fireworks.py" validate architecture "$SKILL_ROOT/fixtures/api-flow-style7.json"
python3 "$SKILL_ROOT/scripts/fireworks.py" render architecture "$SKILL_ROOT/fixtures/api-flow-style7.json" diagram.svg --report layout.json
python3 "$SKILL_ROOT/scripts/fireworks.py" check diagram.svg
python3 "$SKILL_ROOT/scripts/fireworks.py" export-html diagram.svg diagram.html --title "Agent Runtime Architecture"
python3 "$SKILL_ROOT/scripts/fireworks.py" animate diagram.svg diagram.gif
```

The HTML export is one offline file. It sanitizes the SVG, adds pan/zoom/reset, light and dark themes, SVG source copy, and SVG/PNG/JPEG/WebP downloads at 1×–4×.

For motion, say **“Generate a GIF”**, **“Animate this diagram”**, `生成 GIF`, `制作 GIF`, or `让这张图动起来`. The command accepts a generated semantic SVG that carries one of the twelve approved motion contracts. Exact source bytes are not pinned, so validated title and content variants of a supported topology work; missing or changed role/stage/order coverage, route direction, required colors, or geometry fails closed. GIF is the only motion media format, and the default command also writes `<output>.motion.json` as its verification report. The approved default is 960px, 5.75 seconds, 20fps, and 115 frame-center samples. All twelve scenes start connector-free, draw routes on frames 1–36, fade live flow on 36–38, hold full settled flow on 38–109, and reset on 110–114. Their approved identities include the packet heads, terminal evidence trace, Blueprint registration beads, 14×10 Notion memory cards, and the eight scene-specific signatures listed below. Default packages report both the style contract and shared `+2s-settled-flow` timing revision as `user-approved`. Timelines of 75 frames or fewer remain all-unique. Longer timelines allow non-adjacent repeated rasters inside the full-opacity interval; frame 110 is the sole boundary exception because its unchanged reset opacity is exactly 1.00, and such evidence is classified as `intentional_reset_boundary_repeat`. Frames 111–114 remain globally distinct. Long timelines require at least 75 unique rasters and forbid adjacent duplicates. The all-style 75-vs-115 gate counts binary-exact and decoded-RGBA-exact frames separately; compositor-only fallback is accepted only at AE ≤ 128, normalized RMSE ≤ 0.001, with components no thicker than 2px and confined to edge or node borders. DOM and signature geometry remain strict-exact. Explicit 3.75s/75-frame and 2.75s/55-frame calls remain supported. See [Focused SVG-to-GIF Motion](references/motion-effects.md).

| Style | Preset | Live signature |
|---:|---|---|
| 5 | `agent-orchestration` | glass task capsule + coordinator halo |
| 6 | `governed-runtime` | governance thread + policy seal |
| 7 | `token-stream` | API rail + three-cell token train |
| 8 | `golden-circuit` | luxury circuit rail + gem tracer |
| 9 | `review-trace` | review rail + moving review cursor |
| 10 | `cloud-flow` | region chevrons + replication capsule |
| 11 | `event-transit` | event train + exception/projection cars |
| 12 | `ops-pulse` | ECG/export heads + trace reveal + waterfall scanner |

---

## Requirements

The bundled SVG/PNG scripts require **cairosvg** (recommended) or `rsvg-convert`. Optional SVG-to-GIF export requires FFmpeg/FFprobe, Chrome/Chromium, and `puppeteer` or `puppeteer-core`.

```bash
# Recommended: cairosvg (best CSS support)
python3 -m pip install cairosvg

# Fallback: rsvg-convert (system package; may drop CSS / <foreignObject>)
brew install librsvg                   # macOS
sudo apt install librsvg2-bin          # Ubuntu/Debian

# Optional semantic motion export. Install beside every copied Skill because the
# renderer intentionally does not load modules from the caller's directory.
brew install ffmpeg                    # macOS; use your system package manager elsewhere
for SKILL_ROOT in \
  "$HOME/.agents/skills/fireworks-tech-graph" \
  "$HOME/.claude/skills/fireworks-tech-graph"
do
  [ -d "$SKILL_ROOT" ] || continue
  npm install --prefix "$SKILL_ROOT" --ignore-scripts --no-save --package-lock=false puppeteer-core@25.3.0
  python3 "$SKILL_ROOT/scripts/fireworks.py" doctor
done

# Verify either supported script renderer
python3 -c "import cairosvg; print(cairosvg.__version__)"
rsvg-convert --version
```

| Renderer | Quality | Install Cost | Use When |
|----------|---------|--------------|----------|
| **cairosvg** | ✅ Good | Single `python3 -m pip install` | Default — best balance |
| rsvg-convert | ⚠️ Fair | System package | No Python available, simple flat diagrams |
| puppeteer | ✅✅ Best | Node + Chromium | Manual browser-rendering path for D3, Mermaid, or pixel-perfect output |

---

## Why Not Mermaid or draw.io?

| | Mermaid | draw.io | **fireworks-tech-graph** |
|--|---------|---------|--------------------------|
| Natural language input | ✗ | ✗ | ✅ |
| AI/Agent domain patterns | ✗ | ✗ | ✅ |
| Multiple visual styles | ✗ | manual | ✅ 12 built-in |
| High-res PNG export | ✗ | manual | ✅ auto 1920px |
| Semantic arrow colors | ✗ | manual | ✅ auto |
| No online tool needed | ✅ | ✗ | ✅ |

Mermaid is great for quick inline diagrams in markdown. draw.io is great for manual polishing. `fireworks-tech-graph` is optimized for **describing a system and getting a polished diagram immediately**, without writing DSL syntax or clicking around a GUI.

---

## Usage

### Trigger phrases

The skill auto-triggers on:

```
generate diagram / draw diagram / create chart / visualize
architecture diagram / flowchart / sequence diagram / data flow
Generate a GIF / animate this diagram / animate this SVG as a GIF / 生成 GIF / 制作 GIF / 让这张图动起来 / 把刚才的 SVG 转成 GIF
```

### Basic usage

```
Draw a RAG pipeline flowchart
```

```
Generate an Agentic Search architecture diagram
```

### Specify style

```
Draw a microservices architecture diagram, style 2 (dark terminal)
```

```
Draw a multi-agent collaboration diagram --style glassmorphism
```

### Specify output path

```
Generate a Mem0 architecture diagram, output to ~/Desktop/
```

```
Create a tool call flow diagram --output /tmp/diagrams/
```

---

## Example Prompts by Scenario

### AI/Agent Systems

```
Compare Agentic RAG vs standard RAG in a feature matrix, Notion clean style
```
→ Comparison matrix: RAG vs Agentic RAG, covering retrieval strategy, agent loop, tool use

```
Generate a Mem0 memory architecture diagram with vector store, graph DB, KV store, and memory manager
```
→ Memory Architecture with swim lanes: Input → Memory Manager → Storage tiers → Retrieval

```
Draw a Multi-Agent diagram: Orchestrator dispatches 3 SubAgents (search / compute / code execution), results aggregated
```
→ Agent Architecture with hexagons, tool layers, and result aggregation

```
Visualize the Tool Call execution flow: LLM → Tool Selector → Execution → Parser → back to LLM
```
→ Flowchart with decision loop showing tool invocation cycle

```
Draw the 5 agent memory types: Sensory, Working, Episodic, Semantic, Procedural
```
→ Mind map or layered architecture showing memory tiers from sensory to procedural

### Infrastructure & Cloud

```
Draw a microservices architecture: Client → API Gateway → [User Service / Order Service / Payment Service] → PostgreSQL + Redis
```
→ Architecture diagram with horizontal layers, swim lanes per service cluster

```
Generate a data pipeline diagram: Kafka → Spark processing → write to S3 → Athena query
```
→ Data flow diagram with labeled arrows (stream / batch / query)

```
Draw a Kubernetes deployment: Ingress → Service → [Pod × 3] → ConfigMap + PersistentVolume
```
→ Architecture with dashed containers per namespace, solid arrows for traffic flow

### API & Sequence Flows

```
Draw an OAuth2 authorization code flow sequence diagram: User → Client → Auth Server → Resource Server
```
→ Sequence diagram with vertical lifelines and activation boxes

```
Draw the ChatGPT Plugin call sequence diagram
```
→ Sequence: User → ChatGPT → Plugin Manifest → API → Response chain

### Decision & Process Flows

```
Draw a pre-launch QA flowchart for an AI app: Code Review → Security Scan → Performance Test → Manual Approval → Deploy
```
→ Flowchart with diamond decision nodes and parallel branches

```
Generate a feature comparison matrix: RAG vs Fine-tuning vs Prompt Engineering
```
→ Comparison matrix with checked/unchecked cells across cost, latency, accuracy, flexibility

### Concept Maps

```
Visualize the LLM application tech stack: from foundation model to SDK to app framework to deployment
```
→ Layered architecture or mind map from model layer to product layer

```
Draw an AI Agent capability map: Perception / Memory / Reasoning / Action / Learning
```
→ Mind map with central "AI Agent" node and 5 radial branches

---

## 12 Styles

| # | Name | Background | Font | Best For |
|---|------|-----------|------|----------|
| 1 | **Flat Icon** *(default)* | `#ffffff` | Helvetica | Blogs, slides, docs |
| 2 | **Dark Terminal** | `#0f0f1a` | SF Mono / Fira Code | GitHub README, dev articles |
| 3 | **Blueprint** | `#0a1628` | Courier New | Architecture docs, engineering |
| 4 | **Notion Clean** | `#ffffff` | system-ui | Notion, Confluence, wikis |
| 5 | **Glassmorphism** | `#0d1117` gradient | Inter | Product sites, keynotes |
| 6 | **Claude Official** | `#f8f6f3` | system-ui | Anthropic-style diagrams, warm aesthetic |
| 7 | **OpenAI Official** | `#ffffff` | system-ui | OpenAI-style diagrams, clean modern look |
| 8 | **Dark Luxury** *(AI-authored)* | `#0a0a0a` | Georgia + system-ui | Premium docs, README heroes, conference slides |
| 9 | **C4 Review Canvas** | `#f7f2e8` | Avenir / system-ui | C4 design reviews, ADRs, responsibilities |
| 10 | **Cloud Fabric** | `#edf5fb` | Inter / system-ui | Multi-region deployments, VPC/network ownership |
| 11 | **Event Transit** | `#fbf7ee` | Avenir / system-ui | Kafka/event streams, consumer groups, DLQ |
| 12 | **Ops Pulse** | `#07111f` | SF Mono / Fira Code | SRE reviews, golden signals, critical traces |

Each style has a dedicated reference file in `references/` with exact color tokens and SVG patterns. Styles 1–7 and 9–12 are generator-backed; Style 8 uses AI-authored composition plus a static regression fixture.
The generator consumes structure fields such as `containers`, semantic `nodes[].kind`, `arrows[].flow`, and explicit port anchors. Styles 9–12 additionally validate domain-specific fields before layout.

Useful high-leverage fields for style-specific polish:
- `style_overrides` to nudge title alignment or palette tokens without forking a full style
- `containers[].header_prefix` / `containers[].header_text` for blueprint-style numbered section headers such as `01 // EDGE`
- `containers[].side_label` for Claude-style left layer labels
- `window_controls`, `meta_left`, `meta_center`, `meta_right` for terminal / document chrome
- `blueprint_title_block` for engineering title boxes in style 3

### Style Selection Guide

**For UML Diagrams:**
- **Class/Component/Package**: Style 1 (Flat Icon) or Style 4 (Notion Clean) — clear structure, easy to read
- **Sequence/Timing**: Style 2 (Dark Terminal) — monospace fonts help with alignment
- **State Machine/Activity**: Style 3 (Blueprint) — engineering aesthetic fits process flows
- **Use Case/Interview**: Style 1 (Flat Icon) — colorful, accessible

**For AI/Agent Diagrams:**
- **RAG/Agentic Search**: Style 2 (Dark Terminal) or Style 5 (Glassmorphism) — tech-forward aesthetic
- **Memory Architecture**: Style 3 (Blueprint) — emphasizes layered storage tiers
- **Multi-Agent**: Style 5 (Glassmorphism) — frosted cards distinguish agent boundaries

**For Documentation:**
- **Internal docs**: Style 4 (Notion Clean) — minimal, wiki-friendly
- **Blog posts**: Style 1 (Flat Icon) — colorful, engaging
- **GitHub README**: Style 2 (Dark Terminal) — matches dark theme
- **Presentations**: Style 5 (Glassmorphism) or Style 6 (Claude Official) — polished

**For Engineering Reviews:**
- **C4/ADR review**: Style 9 (C4 Review Canvas) — one declared abstraction level with responsibilities and protocols
- **Cloud deployment review**: Style 10 (Cloud Fabric) — explicit region/network ownership and cross-boundary mechanisms
- **Event-driven system review**: Style 11 (Event Transit) — topic rails, processors, consumer groups, state, and DLQ
- **Reliability/incident review**: Style 12 (Ops Pulse) — golden signals, one critical path, and a correlated trace

**Brand-Specific:**
- **Anthropic/Claude projects**: Style 6 (Claude Official) — warm cream background, brand colors
- **OpenAI projects**: Style 7 (OpenAI Official) — clean white, OpenAI palette
- **Premium editorial diagrams**: Style 8 (Dark Luxury) — deep black canvas, champagne-gold hierarchy, semantic color buckets

---

## Diagram Types

| Type | Description | Key Layout Rule |
|------|-------------|-----------------|
| **Architecture** | Services, components, cloud infra | Horizontal layers top→bottom |
| **Data Flow** | What data moves where | Label every arrow with data type |
| **Flowchart** | Decisions, process steps | Diamond = decision, top→bottom |
| **Agent Architecture** | LLM + tools + memory | 5-layer model: Input/Agent/Memory/Tool/Output |
| **Memory Architecture** | Mem0, MemGPT-style | Separate read/write paths, memory tiers |
| **Sequence** | API call chains, time-ordered | Vertical lifelines, horizontal messages |
| **Comparison** | Feature matrix, side-by-side | Column = system, row = attribute |
| **Mind Map** | Concept maps, radial | Central node, bezier branches |

### UML Diagram Support (14 Types)

| UML Type | Description | Best Style |
|----------|-------------|------------|
| **Class Diagram** | Classes, attributes, methods, relationships | Style 1, 4 |
| **Component Diagram** | Software components and dependencies | Style 1, 3 |
| **Deployment Diagram** | Hardware nodes and software deployment | Style 3 |
| **Package Diagram** | Package organization and dependencies | Style 1, 4 |
| **Composite Structure** | Internal structure of classes/components | Style 1, 3 |
| **Object Diagram** | Object instances and relationships | Style 1, 4 |
| **Use Case Diagram** | Actors, use cases, system boundaries | Style 1 |
| **Activity Diagram** | Workflows, parallel processes | Style 3 |
| **State Machine** | State transitions and events | Style 2, 3 |
| **Sequence Diagram** | Message exchanges over time | Style 2 |
| **Communication Diagram** | Object interactions and messages | Style 1, 2 |
| **Timing Diagram** | State changes over time | Style 2 |
| **Interaction Overview** | High-level interaction flow | Style 1, 2 |
| **ER Diagram** | Entity-relationship data models | Style 1, 3 |

---

## AI/Agent Domain Patterns

Built-in pattern knowledge:

```
RAG Pipeline         → Query → Embed → VectorSearch → Retrieve → LLM → Response
Agentic RAG          → adds Agent loop + Tool use
Agentic Search       → Query → Planner → [Search/Calc/Code] → Synthesizer
Mem0 Memory Layer    → Input → Memory Manager → [VectorDB + GraphDB] → Context
Agent Memory Types   → Sensory → Working → Episodic → Semantic → Procedural
Multi-Agent          → Orchestrator → [SubAgent×N] → Aggregator → Output
Tool Call Flow       → LLM → Tool Selector → Execution → Parser → LLM (loop)
```

---

## Shape Vocabulary

Shapes encode semantic meaning consistently across all styles:

| Concept | Shape |
|---------|-------|
| User / Human | Circle + body |
| LLM / Model | Rounded rect, double border, ⚡ |
| Agent / Orchestrator | Hexagon |
| Memory (short-term) | Dashed-border rounded rect |
| Memory (long-term) | Solid cylinder |
| Vector Store | Cylinder with inner rings |
| Graph DB | 3-circle cluster |
| Tool / Function | Rect with ⚙ |
| API / Gateway | Hexagon (single border) |
| Queue / Stream | Horizontal pipe/tube |
| Document / File | Folded-corner rect |
| Browser / UI | Rect with 3-dot titlebar |
| Decision | Diamond |
| External Service | Dashed-border rect |

---

## Arrow Semantics

| Flow Type | Stroke | Dash | Meaning |
|-----------|--------|------|---------|
| Primary data flow | 2px solid | — | Main request/response |
| Control / trigger | 1.5px solid | — | System A triggers B |
| Memory read | 1.5px solid | — | Retrieve from store |
| Memory write | 1.5px | `5,3` | Write/store operation |
| Async / event | 1.5px | `4,2` | Non-blocking |
| Feedback / loop | 1.5px curved | — | Iterative reasoning |

---

## File Structure

```
fireworks-tech-graph/
├── SKILL.md                      # Main skill — diagram types, layout rules, shape vocab
├── README.md                     # This file (English)
├── README.zh.md                  # Chinese version
├── references/
│   ├── style-1-flat-icon.md      # White background, colored accents
│   ├── style-2-dark-terminal.md  # Dark bg, neon accents, monospace
│   ├── style-3-blueprint.md      # Blueprint grid, cyan lines
│   ├── style-4-notion-clean.md   # Minimal, white, single arrow color
│   ├── style-5-glassmorphism.md  # Dark gradient, frosted glass cards
│   ├── style-6-claude-official.md # Warm cream background, Anthropic brand
│   ├── style-7-openai.md         # Clean white, OpenAI brand palette
│   ├── style-8-dark-luxury.md    # Deep black, champagne gold, AI-authored layout
│   ├── style-9-c4-review-canvas.md # C4 review semantics + deterministic rough marks
│   ├── style-10-cloud-fabric.md  # Deployment ownership + neutral cloud glyphs
│   ├── style-11-event-transit.md # Topic rails, stations, junctions, and DLQ
│   ├── style-12-ops-pulse.md     # Golden signals, critical path, and trace waterfall
│   ├── png-export.md             # Renderer selection and manual export paths
│   └── icons.md                  # 40+ product icons + semantic shapes
├── agents/
│   └── openai.yaml              # Optional Codex UI metadata
├── schemas/                      # Versioned diagram JSON Schemas
├── docs/                         # Capability contract and roadmap
├── examples/
│   └── interactive-architecture.html # Offline pan/zoom/export demo
├── fixtures/
│   ├── mem0-style1.json          # Style 1 · Mem0 memory scene
│   ├── tool-call-style2.json     # Style 2 · grounded tool-call scene
│   ├── microservices-style3.json # Style 3 · microservices blueprint
│   ├── agent-memory-types-style4.json # Style 4 · memory hierarchy
│   ├── multi-agent-style5.json   # Style 5 · specialist collaboration
│   ├── system-architecture-style6.json # Style 6 · layered system
│   ├── api-flow-style7.json      # Style 7 · API integration
│   ├── dark-luxury-style8.svg    # Style 8 · AI-authored runtime scene
│   ├── c4-review-canvas-style9.json # Style 9 · Checkout C4 review
│   ├── cloud-fabric-style10.json # Style 10 · Active-active deployment
│   ├── event-transit-style11.json # Style 11 · Checkout event line
│   ├── ops-pulse-style12.json    # Style 12 · Reliability pulse
│   └── quality-baseline/         # Internal same-topology regression set
├── scripts/
│   ├── fireworks.py              # Unified validate/render/check/animate/export CLI
│   ├── diagram_ir.py             # Typed schema-v1 normalization
│   ├── fireworks_geometry.py     # Shared routing and collision semantics
│   ├── interactive_html.py       # Sanitized offline HTML exporter
│   ├── generate-diagram.sh       # Validate SVG + export PNG
│   ├── generate-from-template.py # Create starter SVGs from templates
│   ├── motion.py                 # SVG-to-GIF validation, encoding, and atomic reports
│   ├── svg2gif.js                # Manual-timeline Chromium frame renderer
│   ├── svg2png.js                 # High-fidelity Puppeteer exporter
│   ├── validate-svg.sh           # Validation and render-check entrypoint
│   ├── validate_svg.py           # XML, marker, transform, and path collision checks
│   └── test-all-styles.sh        # Batch test all styles
├── tests/
│   ├── test_geometry_contracts.py # Router and artifact geometry gates
│   └── ...                       # IR, CLI, exporter, installer compatibility
├── tools/                         # Distribution, consistency, install canary
├── skills/fireworks-tech-graph/  # Complete npx-compatible physical mirror
├── assets/
│   └── samples/                  # Showcase diagram PNGs
├── templates/
│   ├── architecture.svg         # Architecture starter template
│   ├── data-flow.svg            # Data-flow starter template
│   └── ...                      # Additional diagram templates
└── agentloop-core.svg           # Included sample SVG
```

---

## Product Icon Coverage

**AI/ML:** OpenAI, Anthropic/Claude, Google Gemini, Meta LLaMA, Mistral, Cohere, Groq, Hugging Face

**AI Frameworks:** Mem0, LangChain, LlamaIndex, LangGraph, CrewAI, AutoGen, DSPy, Haystack

**Vector DBs:** Pinecone, Weaviate, Qdrant, Chroma, Milvus, pgvector, Faiss

**Databases:** PostgreSQL, MySQL, MongoDB, Redis, Elasticsearch, Neo4j, Cassandra

**Messaging:** Kafka, RabbitMQ, NATS, Pulsar

**Cloud:** AWS, GCP, Azure, Cloudflare, Vercel, Docker, Kubernetes

**Observability:** Grafana, Prometheus, Datadog, LangSmith, Langfuse, Arize

---

## Troubleshooting

| Symptom | Cause | Fix |
|---------|-------|-----|
| PNG is blank or all-black | `@import url()` in SVG — neither cairosvg nor rsvg-convert can fetch external fonts | Remove `@import`, use system font stack |
| PNG not generated | No renderer installed | `python3 -m pip install cairosvg` (recommended), or `brew install librsvg` / `apt install librsvg2-bin` |
| Borders or text missing in PNG | Using `rsvg-convert` on SVG with CSS / `<foreignObject>` | Switch to `cairosvg` (`python3 -m pip install cairosvg`) — much better CSS support |
| Diagram cut off at bottom | ViewBox height too short | Increase `height` in `viewBox="0 0 960 <height>"` |
| Text overflowing boxes | Labels too long | Add `text-anchor="middle"` + `<clipPath>` or shorten label |
| Icons not rendering | External CDN URL | Use inline SVG paths from `references/icons.md` |
| Browser-generated SVG renders incorrectly | cairosvg / rsvg can't replay all CSS/JS-injected styles | Use `scripts/svg2png.js` as described in `references/png-export.md` |

---

## License

MIT © 2025 fireworks-tech-graph contributors
skills/fireworks-tech-graph/agents/openai.yaml
interface:
  display_name: "Fireworks Tech Graph"
  short_description: "Generate geometry-safe technical diagrams"
  default_prompt: "Use $fireworks-tech-graph to turn the user's system or workflow description into a geometry-checked SVG, then export PNG, controlled offline HTML, or a validated semantic GIF. Treat direct GIF requests as the approved 5.75s/115-frame SVG-to-GIF default."
skills/fireworks-tech-graph/docs/CAPABILITIES.md
# Capability contract

## Input

- Legacy JSON remains supported and normalizes to schema v1.
- Versioned input uses `schema_version: 1` and a `mode` matching the selected renderer.
- Duplicate IDs, dangling references, non-finite coordinates, malformed waypoints, and unknown schema versions fail before layout.
- Style-specific fields and unknown renderer extensions are preserved.
- `style` and `visual_theme` resolve through one catalog covering Styles 1–12; unknown or conflicting selectors fail closed.

## Engineering semantics

- Style 9 defaults to `c4-review`: one C4 level, typed elements, responsibilities, technologies, and protocols.
- Style 10 defaults to `cloud-fabric`: acyclic deployment boundaries, explicit workload ownership, versioned neutral icons, and named cross-boundary mechanisms.
- Style 11 defaults to `event-transit`: ordered topic rails, declared junctions, consumer groups, state projections, and real DLQ targets.
- Style 12 defaults to `ops-pulse`: exact golden signals, one contiguous business critical path, separate telemetry semantics, and a valid trace tree.
- `semantic_profile: "generic"` keeps any generator-backed visual theme available for internal topology regression without pretending domain semantics are present.

## Geometry

- All generated business edges are rectilinear outside declared bridge arcs.
- `route_points` are exact ordered waypoints; every leg is routed safely.
- Nodes, section headers, legends, title blocks, footers, labels, and the canvas boundary participate in layout checks.
- Distinct ports are allocated deterministically for shared node sides.
- Collinear edge overlap is fatal. Proper crossings receive a visible bridge and background mask.
- Layout output and reports are deterministic for the same input.

## Output

- SVG is the canonical artifact and carries `data-graph-role`, style, diagram-type, semantic-profile, semantic-role, edge-kind, topic, flow, station-order, status, critical-hop, and trace-timing metadata.
- PNG export uses CairoSVG or `rsvg-convert` in the shell workflow.
- Optional motion export has one media contract: a generated semantic SVG carrying one of the 12 approved role/stage/order scene contracts becomes a validated GIF plus its JSON verification report. Exact source bytes are not pinned, but unsupported same-style topologies fail closed. The user-approved 5.75s/115-frame default keeps construction at 1–36, live fade at 36–38, full settled flow at 38–109, and reset at 110–114. Styles 1–12 and the shared `+2s-settled-flow` timing revision retain `user-approved` status. Timelines through 75 frames remain all-unique. Longer timelines require at least 75 unique rasters and zero adjacent duplicates. Repeats stay inside the full-opacity interval except frame 110, the sole `intentional_reset_boundary_repeat` allowed by its unchanged 1.00 reset opacity; frames 111–114 remain globally distinct. The 75-vs-115 compatibility report separates binary-exact, decoded-RGBA-exact, and guarded antialias-equivalent counts. The guarded tier requires AE ≤ 128, normalized RMSE ≤ 0.001, components no thicker than 2px, and edge/node-border-only scope while DOM and signature geometry remain strict-exact. Direction/source-DOM guards, FFprobe validation, infinite looping, and atomic GIF/report installation remain mandatory.
- The single-SVG offline HTML viewer remains independent of motion and supports pan, zoom, reset, themes, copy, and static SVG/PNG/JPEG/WebP export.
- Interactive export rejects active elements, event handlers, external references, `foreignObject`, and external CSS.

## Distribution

- Git clone and npm archives contain the complete skill.
- The committed `skills/fireworks-tech-graph/` mirror supports deterministic `npx skills add` subpath installation for Codex and Claude Code.
- npm `.tgz` and GitHub release `.zip` are built from the same payload and checked by file hash.
skills/fireworks-tech-graph/assets/samples/showcase-gif-manifest.json
{
  "schema_version": 1,
  "approved_at": "2026-07-17",
  "source_contract": "user-approved +2s-settled-flow",
  "approval": {
    "status": "user-approved",
    "style_count": 12,
    "full_size_frame_count": 1380,
    "compatibility_frames_accepted": 852,
    "compatibility_frames_total": 852
  },
  "overview": {
    "layout": "3x4",
    "tile_box": "400x320",
    "preview_fps": "12/1",
    "palette_colors": 160,
    "frame_delays_centiseconds": {
      "8": 46,
      "9": 23
    }
  },
  "assets": [
    {
      "file": "showcase-12-styles.gif",
      "sha256": "361c06f415e14915198e37302beb884c3e6d223f252c163e8d0c4956a8076b08",
      "bytes": 1020419,
      "width": 1200,
      "height": 1280,
      "fps": "12/1",
      "duration_seconds": 5.75,
      "frames": 69
    },
    {
      "file": "sample-style1-flat.gif",
      "sha256": "db22b38f0c54d4d77c464085c66e81558fcca1ece9d352fb17f96ff06244b25e",
      "bytes": 269337,
      "width": 960,
      "height": 680,
      "fps": "20/1",
      "duration_seconds": 5.75,
      "frames": 115
    },
    {
      "file": "sample-style2-dark.gif",
      "sha256": "54ff54b2694ae12b995f59f7bfac658d733298e72ac0238c948fc61e41dec9d4",
      "bytes": 388446,
      "width": 960,
      "height": 720,
      "fps": "20/1",
      "duration_seconds": 5.75,
      "frames": 115
    },
    {
      "file": "sample-style3-blueprint.gif",
      "sha256": "94405804ba16bd6dd1b6b77c0acee576e119ef80909037418cdd9851b66033cb",
      "bytes": 283449,
      "width": 960,
      "height": 720,
      "fps": "20/1",
      "duration_seconds": 5.75,
      "frames": 115
    },
    {
      "file": "sample-style4-notion.gif",
      "sha256": "ef74b2582e24b9aeda41dfc1cac00aa4e45754a2da83949615b5c01719af04f5",
      "bytes": 211403,
      "width": 960,
      "height": 620,
      "fps": "20/1",
      "duration_seconds": 5.75,
      "frames": 115
    },
    {
      "file": "sample-style5-glass.gif",
      "sha256": "c89b0278b3792b179941f4efda300690eb769783b35b2bd055705f73cdda5314",
      "bytes": 376688,
      "width": 960,
      "height": 700,
      "fps": "20/1",
      "duration_seconds": 5.75,
      "frames": 115
    },
    {
      "file": "sample-style6-claude.gif",
      "sha256": "390fda2e36af77f8fe798ed93136bb9874975cffa9eb91842aa2fa430d35e67b",
      "bytes": 246718,
      "width": 960,
      "height": 700,
      "fps": "20/1",
      "duration_seconds": 5.75,
      "frames": 115
    },
    {
      "file": "sample-style7-openai.gif",
      "sha256": "672f9f62d0800f25918db5c411014b1fbfdde64cd5cd700f2fcb16105d4cc97e",
      "bytes": 165225,
      "width": 960,
      "height": 700,
      "fps": "20/1",
      "duration_seconds": 5.75,
      "frames": 115
    },
    {
      "file": "sample-style8-dark-luxury.gif",
      "sha256": "186e28cc8a35ad0434558308562c7a2c743055e3b8bbc2b3865758af39080056",
      "bytes": 234825,
      "width": 960,
      "height": 600,
      "fps": "20/1",
      "duration_seconds": 5.75,
      "frames": 115
    },
    {
      "file": "sample-style9-c4-review-canvas.gif",
      "sha256": "ebc1d337a38d55ba2010191c90e534d94f394dd64205a36d3a205c78771c0b5e",
      "bytes": 205572,
      "width": 960,
      "height": 611,
      "fps": "20/1",
      "duration_seconds": 5.75,
      "frames": 115
    },
    {
      "file": "sample-style10-cloud-fabric.gif",
      "sha256": "d02fa0d67cd4fd621fe94635938de8b6066594b7060e08cf92bbcc008c54e29f",
      "bytes": 340823,
      "width": 960,
      "height": 760,
      "fps": "20/1",
      "duration_seconds": 5.75,
      "frames": 115
    },
    {
      "file": "sample-style11-event-transit.gif",
      "sha256": "f3995d0269732277b6e80f5f24849b26d710f9a6c34642e94c61de000cd08637",
      "bytes": 184537,
      "width": 960,
      "height": 590,
      "fps": "20/1",
      "duration_seconds": 5.75,
      "frames": 115
    },
    {
      "file": "sample-style12-ops-pulse.gif",
      "sha256": "26f5657f4430f337daec744a761bc27f8554f4708bd9a564bff198fc83507800",
      "bytes": 356902,
      "width": 960,
      "height": 860,
      "fps": "20/1",
      "duration_seconds": 5.75,
      "frames": 115
    }
  ]
}
skills/fireworks-tech-graph/CHANGELOG.md
# Changelog

All notable changes use semantic versioning.

Historical `1.0.0` through `1.0.4` entries were reconstructed from the published npm artifacts and their registry-recorded `gitHead` values. See [`docs/releases/`](docs/releases/) for provenance details.

## 1.2.0 — 2026-07-17

- Added one focused motion contract: generated semantic SVG in, validated GIF plus a JSON motion report out. The renderer rejects raster inputs and unsupported media formats.
- Added twelve user-approved style signatures: headed memory packets, terminal evidence traces, blueprint registration beads, Notion memory cards, glass task capsules, governance seals, API token trains, gem tracers, C4 review cursors, active-active cloud chevrons, event trains, and an ops-pulse scanner.
- Changed the default GIF timeline to 5.75s/115 frames at 20fps. Routes construct on frames 1–36, live flow reaches full opacity at frame 38, settled flow continues through frame 109, and reset remains frames 110–114. Explicit 3.75s/75-frame and 2.75s/55-frame timelines remain supported.
- Replaced all 13 README showcase positions with approved GIFs, including a synchronized 3×4 overview below 2 MiB. Added a public SHA-256/metadata manifest and binary GIF parsing for dimensions, cadence, frame count, duration, and infinite-loop readback. Retained regenerated 1920px PNGs as static regression baselines.
- Updated the GitHub Pages product site to showcase the animated overview and all twelve style GIFs, with static social-card media and reduced-motion fallbacks retained.
- Added bounded runtime probes, secure Puppeteer resolution, source-DOM immutability guards, frame uniqueness and periodic-motion gates, FFprobe validation, atomic GIF/report installation, explicit encoder requirements, and a 500KB per-style guidance target.
- Kept valid same-style title/content variants animatable by pinning semantic topology, route roles, order, direction, stage, geometry, and style signatures instead of exact source bytes.
- Added a real-Chromium 12-style 75-vs-115 regression covering 852 comparisons, plus installed-copy all-style motion canaries in CI and the tag-release workflow.
- Fixed transformed SVG bounds to inspect all four corners and corrected multi-subpath bridge counting. Refreshed the Style 6 and Style 7 PNG baselines to match the canonical renderer.
- Updated Codex and Claude Code installation guidance so optional motion dependencies are installed into each copied Skill root.

## 1.1.0 — 2026-07-15

- Added four engineering-first styles: C4 Review Canvas, Cloud Fabric, Event Transit, and Ops Pulse, each with an executable semantic contract, distinct prompt fingerprints, and showcase fixtures.
- Added schema v1 normalization, typed node/edge IR, deterministic layout reports, and a unified CLI.
- Added geometry-safe orthogonal routing, mandatory waypoint validation, port fan-out, label/canvas checks, legend relocation, edge-crossing bridges, and semantic SVG metadata.
- Added strict SVG geometry validation and regression coverage for line crossings, reserved regions, clipping, and bridge masks.
- Added safe offline interactive HTML export with pan, zoom, theme switching, copy, and SVG/PNG/JPEG/WebP export up to 4×.
- Added a complete nested Agent Skill distribution for reliable `npx skills add` installation.
- Added CI, release archives, archive parity checks, project consistency checks, and isolated install canaries.
- Refreshed the official 12-style PNG showcase and product website with one distinct engineering scenario per style.
- Raised the supported Node.js runtime baseline to Node.js 18+.

## 1.0.5 — 2026-07-11

- Added shared Codex and Claude Code support, compatibility metadata, tests, and installation guidance.
- Added Style 8 Dark Luxury and expanded the gallery to eight distinct visual styles.
- Added a bounded generate-validate-repair loop, structural SVG validation, and optional visual self-review.
- Improved connector routing, label placement, overlap prevention, clipping guidance, CJK font fallbacks, and helper-script robustness.
- Switched the default PNG renderer to CairoSVG and documented renderer-specific edge cases.
- Added the first GitHub Pages product showcase.

## 1.0.4 — 2026-04-12

- Added an explicit `npx skills add ... --force -g -y` update workflow to the English README, Chinese README, and Skill instructions.
- Published npm package `1.0.4` with Node.js 14+ metadata.

## 1.0.3 — 2026-04-12

- Clarified that `skills add` installs from the GitHub repository while npm remains the package and distribution page.
- Replaced the legacy install example with `npx skills add yizhiyanhua-ai/fireworks-tech-graph`.
- Published npm package `1.0.3` with the corrected install-source guidance.

## 1.0.2 — 2026-04-12

- Added the style-driven generator, fixtures, reusable SVG templates, and a seven-style diagram matrix.
- Refreshed the seven official showcase images and expanded SVG validation and all-style test scripts.
- Added formal npm package metadata for repository, license, files, runtime, and discovery keywords.
- Expanded the npm package payload to include fixtures and templates.

## 1.0.1 — 2026-04-12

- Added an explicit layout validation checklist covering connector-node collisions, text overflow, endpoint alignment, and label backgrounds.
- Normalized npm repository metadata.

## 1.0.0 — 2026-04-12

- Published the first npm package for the Agent Skill.
- Shipped seven built-in diagram styles, SVG and PNG generation, architecture and UML guidance, validation scripts, and bilingual documentation.
skills/fireworks-tech-graph/docs/ROADMAP.md
# Roadmap

## Shipped in 1.1

- Versioned diagram IR and schemas
- Geometry-safe renderer and artifact audit
- Deterministic layout reports
- Complete Agent Skill installer mirror
- Unified CLI and offline interactive export
- CI, release archives, parity checks, and public install canary

## Next candidates

- Incremental layout for diagrams above 100 edges
- Pluggable text metrics using browser font measurement
- Interactive edge/node inspection panels
- Optional theme token packs without changing diagram semantics
- Visual-diff baselines for browser engines and platform fonts

Roadmap items remain proposals until they have tests, a compatibility plan, and a measured maintenance cost.
scripts/svg2gif.js
#!/usr/bin/env node

"use strict";

const fs = require("fs");
const path = require("path");

const MINIMUM_FRAME_COUNT = 55;
const EMPTY_OPENING_FRAME = 0;
const RESET_OPACITY_SAMPLES = Object.freeze([1.00, 0.7575, 0.515, 0.2725, 0.03]);
const STYLE_1_DRAW_SCHEDULE = Object.freeze([
  Object.freeze({ role: "ingress", order: 0, start: 1, end: 8 }),
  Object.freeze({ role: "reason", order: 0, start: 5, end: 12 }),
  Object.freeze({ role: "extract", order: 0, start: 9, end: 16 }),
  Object.freeze({ role: "transform", order: 0, start: 13, end: 20 }),
  Object.freeze({ role: "resolve", order: 1, start: 17, end: 24 }),
  Object.freeze({ role: "memory-write", order: 0, start: 21, end: 28 }),
  Object.freeze({ role: "memory-read", order: 0, start: 25, end: 32 }),
  Object.freeze({ role: "response-context", order: 0, start: 29, end: 36 }),
]);
const STYLE_2_DRAW_SCHEDULE = Object.freeze([
  Object.freeze({ role: "ingress", stage: 1, order: 0, start: 1, end: 8 }),
  Object.freeze({ role: "delegate", stage: 2, order: 0, start: 5, end: 12 }),
  Object.freeze({ role: "tool-call", stage: 3, order: 0, start: 9, end: 16 }),
  Object.freeze({ role: "inspect", stage: 4, order: 0, start: 13, end: 20 }),
  Object.freeze({ role: "index", stage: 5, order: 0, start: 17, end: 24 }),
  Object.freeze({ role: "grounding", stage: 6, order: 0, start: 21, end: 28 }),
  Object.freeze({ role: "grounding", stage: 6, order: 1, start: 25, end: 32 }),
  Object.freeze({ role: "answer", stage: 7, order: 0, start: 29, end: 36 }),
]);
const STYLE_3_DRAW_SCHEDULE = Object.freeze([
  Object.freeze({ role: "ingress", stage: 1, order: 0, start: 1, end: 6 }),
  Object.freeze({ role: "policy", stage: 2, order: 0, start: 4, end: 9 }),
  Object.freeze({ role: "fanout", stage: 3, order: 0, start: 8, end: 13 }),
  Object.freeze({ role: "fanout", stage: 3, order: 1, start: 11, end: 16 }),
  Object.freeze({ role: "fanout", stage: 3, order: 2, start: 14, end: 19 }),
  Object.freeze({ role: "data-write", stage: 4, order: 0, start: 18, end: 23 }),
  Object.freeze({ role: "data-write", stage: 4, order: 1, start: 21, end: 26 }),
  Object.freeze({ role: "data-write", stage: 4, order: 2, start: 24, end: 29 }),
  Object.freeze({ role: "event", stage: 5, order: 0, start: 28, end: 33 }),
  Object.freeze({ role: "telemetry", stage: 6, order: 0, start: 31, end: 36 }),
]);
const STYLE_4_DRAW_SCHEDULE = Object.freeze([
  Object.freeze({ role: "sample", stage: 1, order: 0, start: 1, end: 4 }),
  Object.freeze({ role: "attend", stage: 2, order: 0, start: 5, end: 8 }),
  Object.freeze({ role: "invoke", stage: 3, order: 0, start: 9, end: 12 }),
  Object.freeze({ role: "remember", stage: 4, order: 0, start: 13, end: 22 }),
  Object.freeze({ role: "consolidate", stage: 5, order: 0, start: 23, end: 26 }),
  Object.freeze({ role: "recall", stage: 6, order: 0, start: 27, end: 36 }),
]);
const STYLE_5_DRAW_SCHEDULE = Object.freeze([
  Object.freeze({ role: "ingress", stage: 1, order: 0, start: 1, end: 6 }),
  Object.freeze({ role: "delegate", stage: 2, order: 0, start: 5, end: 12 }),
  Object.freeze({ role: "delegate", stage: 2, order: 1, start: 8, end: 15 }),
  Object.freeze({ role: "delegate", stage: 2, order: 2, start: 11, end: 18 }),
  Object.freeze({ role: "evidence", stage: 3, order: 0, start: 17, end: 24 }),
  Object.freeze({ role: "artifact", stage: 3, order: 1, start: 20, end: 27 }),
  Object.freeze({ role: "context", stage: 4, order: 0, start: 25, end: 30 }),
  Object.freeze({ role: "deliver", stage: 5, order: 0, start: 29, end: 36 }),
  Object.freeze({ role: "approval", stage: 5, order: 1, start: 29, end: 36 }),
]);
const STYLE_6_DRAW_SCHEDULE = Object.freeze([
  Object.freeze({ role: "ingress", stage: 1, order: 0, start: 1, end: 6 }),
  Object.freeze({ role: "dispatch", stage: 2, order: 0, start: 5, end: 12 }),
  Object.freeze({ role: "runtime-branch", stage: 3, order: 0, start: 10, end: 17 }),
  Object.freeze({ role: "runtime-branch", stage: 3, order: 1, start: 13, end: 20 }),
  Object.freeze({ role: "runtime-branch", stage: 3, order: 2, start: 16, end: 23 }),
  Object.freeze({ role: "foundation", stage: 4, order: 0, start: 21, end: 28 }),
  Object.freeze({ role: "foundation", stage: 4, order: 1, start: 24, end: 31 }),
  Object.freeze({ role: "foundation", stage: 4, order: 2, start: 27, end: 34 }),
  Object.freeze({ role: "promote", stage: 5, order: 0, start: 31, end: 36 }),
]);
const STYLE_7_DRAW_SCHEDULE = Object.freeze([
  Object.freeze({ role: "connect", stage: 1, order: 0, start: 1, end: 6 }),
  Object.freeze({ role: "prepare", stage: 2, order: 0, start: 5, end: 12 }),
  Object.freeze({ role: "invoke", stage: 3, order: 0, start: 10, end: 17 }),
  Object.freeze({ role: "tool-call", stage: 4, order: 0, start: 15, end: 22 }),
  Object.freeze({ role: "token-stream", stage: 4, order: 1, start: 18, end: 27 }),
  Object.freeze({ role: "govern", stage: 5, order: 0, start: 25, end: 32 }),
  Object.freeze({ role: "measure", stage: 5, order: 1, start: 25, end: 32 }),
  Object.freeze({ role: "promote", stage: 6, order: 0, start: 31, end: 36 }),
]);
const STYLE_8_DRAW_SCHEDULE = Object.freeze([
  Object.freeze({ role: "primary", stage: 1, order: 0, start: 1, end: 6 }),
  Object.freeze({ role: "primary", stage: 2, order: 0, start: 5, end: 10 }),
  Object.freeze({ role: "memory-read", stage: 3, order: 0, start: 9, end: 18 }),
  Object.freeze({ role: "tool-call", stage: 3, order: 1, start: 12, end: 21 }),
  Object.freeze({ role: "data", stage: 4, order: 0, start: 20, end: 25 }),
  Object.freeze({ role: "trace", stage: 5, order: 0, start: 24, end: 29 }),
  Object.freeze({ role: "feedback", stage: 6, order: 0, start: 28, end: 36 }),
]);
const STYLE_9_DRAW_SCHEDULE = Object.freeze([
  Object.freeze({ role: "review-entry", stage: 1, order: 0, start: 1, end: 7 }),
  Object.freeze({ role: "review-request", stage: 2, order: 0, start: 7, end: 13 }),
  Object.freeze({ role: "review-async", stage: 3, order: 0, start: 13, end: 22 }),
  Object.freeze({ role: "review-state", stage: 4, order: 0, start: 22, end: 30 }),
  Object.freeze({ role: "review-external", stage: 4, order: 1, start: 28, end: 36 }),
]);
const STYLE_10_DRAW_SCHEDULE = Object.freeze([
  Object.freeze({ role: "global-route", stage: 1, order: 0, start: 1, end: 12 }),
  Object.freeze({ role: "global-route", stage: 1, order: 1, start: 1, end: 12 }),
  Object.freeze({ role: "regional-write", stage: 2, order: 0, start: 13, end: 22 }),
  Object.freeze({ role: "regional-write", stage: 2, order: 1, start: 13, end: 22 }),
  Object.freeze({ role: "cross-region", stage: 3, order: 0, start: 23, end: 36 }),
]);
const STYLE_11_DRAW_SCHEDULE = Object.freeze([
  Object.freeze({ role: "topic-rail", stage: 1, order: 0, start: 1, end: 6 }),
  Object.freeze({ role: "topic-rail", stage: 2, order: 0, start: 7, end: 12 }),
  Object.freeze({ role: "topic-rail", stage: 3, order: 0, start: 13, end: 18 }),
  Object.freeze({ role: "topic-rail", stage: 4, order: 0, start: 19, end: 24 }),
  Object.freeze({ role: "dead-letter", stage: 5, order: 0, start: 25, end: 32 }),
  Object.freeze({ role: "state-project", stage: 5, order: 1, start: 29, end: 36 }),
]);
const STYLE_12_DRAW_SCHEDULE = Object.freeze([
  Object.freeze({ role: "critical-request", stage: 1, order: 0, start: 1, end: 6 }),
  Object.freeze({ role: "critical-request", stage: 2, order: 0, start: 7, end: 12 }),
  Object.freeze({ role: "critical-request", stage: 3, order: 0, start: 13, end: 18 }),
  Object.freeze({ role: "telemetry-export", stage: 4, order: 0, start: 19, end: 26 }),
]);
const STYLE_1_STREAM_CONTRACT = Object.freeze({
  start: 36,
  fadeInFactors: Object.freeze([0.30, 0.65, 1.00]),
  bodyDashPattern: Object.freeze([16, 25]),
  headDashPattern: Object.freeze([6, 35]),
  dashPeriod: 41,
  dashOffsetPerFrame: -6.0,
  headLeadOffset: -10,
  bodyOpacity: 0.90,
  headOpacity: 0.98,
  headStrokeWidth: 2.20,
  bodyPrimitive: "persistent-data-flow-stream",
  headPrimitive: "persistent-data-flow-head",
  bodyColor: "#06b6d4",
  headColor: "#e0f2fe",
  bodyWidthMaximum: 4.0,
  bodyWidthMinimum: 3.0,
  bodyWidthMultiplier: 1.60,
  bodyWidthDescription: "min(4.0, max(3.0, source_stroke * 1.60))",
  sourceStrokeWidth: 2.4,
  resolvedStrokeWidth: 3.84,
  phaseStageMultiplier: 7,
  phasePolicy: "(motionStage * 7 + motionOrder * 3) mod 41",
  expectedStreamPhases: Object.freeze([7, 14, 21, 28, 31, 35, 1, 8]),
  directionSentinels: Object.freeze([
    Object.freeze({ role: "ingress", order: 0, expected: Object.freeze(["right"]) }),
    Object.freeze({ role: "resolve", order: 1, expected: Object.freeze(["left"]) }),
    Object.freeze({ role: "memory-write", order: 0, expected: Object.freeze(["down", "left", "down"]) }),
  ]),
  resetBehavior: "all eight body/head pairs keep advancing while topology, labels, and both flow layers fade together",
});
const STYLE_2_STREAM_CONTRACT = Object.freeze({
  start: 36,
  fadeInFactors: Object.freeze([0.30, 0.65, 1.00]),
  bodyDashPattern: Object.freeze([15, 26]),
  headDashPattern: Object.freeze([5, 36]),
  dashPeriod: 41,
  dashOffsetPerFrame: -6.0,
  headLeadOffset: -10,
  bodyOpacity: 0.94,
  headOpacity: 1.00,
  headStrokeWidth: 2.00,
  bodyPrimitive: "terminal-evidence-stream",
  headPrimitive: "terminal-command-head",
  bodyColor: "inherit-source-stroke",
  headColor: "#f8fafc",
  bodyWidthMaximum: 3.8,
  bodyWidthMinimum: 3.0,
  bodyWidthMultiplier: 1.50,
  bodyWidthPrecision: 2,
  bodyWidthDescription: "min(3.8, max(3.0, source_stroke * 1.50))",
  sourceStrokeWidth: 2.3,
  resolvedStrokeWidth: 3.45,
  phaseStageMultiplier: 6,
  phasePolicy: "(motionStage * 6 + motionOrder * 3) mod 41",
  expectedStreamPhases: Object.freeze([6, 12, 18, 24, 30, 36, 39, 1]),
  expectedSourceColors: Object.freeze([
    "#a855f7", "#a855f7", "#38bdf8", "#38bdf8",
    "#22c55e", "#fb7185", "#fb7185", "#f97316",
  ]),
  directionSentinels: Object.freeze([
    Object.freeze({ role: "ingress", order: 0, expected: Object.freeze(["right"]) }),
    Object.freeze({ role: "delegate", order: 0, expected: Object.freeze(["down", "left", "down"]) }),
    Object.freeze({ role: "tool-call", order: 0, expected: Object.freeze(["right"]) }),
    Object.freeze({ role: "inspect", order: 0, expected: Object.freeze(["down"]) }),
    Object.freeze({ role: "index", order: 0, expected: Object.freeze(["right", "up"]) }),
    Object.freeze({ role: "grounding", order: 0, expected: Object.freeze(["up", "left", "up"]) }),
    Object.freeze({ role: "grounding", order: 1, expected: Object.freeze(["right"]) }),
    Object.freeze({ role: "answer", order: 0, expected: Object.freeze(["right"]) }),
  ]),
  resetBehavior: "all eight body/head pairs keep advancing while topology, labels, cursor, and both flow layers fade together",
});
const STYLE_3_STREAM_CONTRACT = Object.freeze({
  start: 36,
  fadeInFactors: Object.freeze([0.30, 0.65, 1.00]),
  bodyDashPattern: Object.freeze([12, 31]),
  dashPeriod: 43,
  dashOffsetPerFrame: -6.0,
  beadAdvancePerFrame: 6.0,
  bodyOpacity: 0.92,
  beadOpacity: 0.98,
  beadRadius: 3.0,
  beadFill: "#e0f2fe",
  beadStrokeWidth: 1.2,
  bodyPrimitive: "blueprint-distribution-wave",
  beadPrimitive: "blueprint-registration-bead",
  bodyColor: "inherit-source-stroke",
  bodyWidthMaximum: 3.4,
  bodyWidthMinimum: 2.8,
  bodyWidthMultiplier: 1.40,
  bodyWidthPrecision: 2,
  bodyWidthDescription: "min(3.4, max(2.8, source_stroke * 1.40))",
  sourceStrokeWidth: 2.1,
  resolvedStrokeWidth: 2.94,
  phaseStageMultiplier: 7,
  phaseOrderMultiplier: 0,
  phasePolicy: "(motionStage * 7 + motionOrder * 0) mod 43",
  expectedStreamPhases: Object.freeze([7, 14, 21, 21, 21, 28, 28, 28, 35, 42]),
  expectedSourceColors: Object.freeze([
    "#38bdf8", "#67e8f9", "#38bdf8", "#38bdf8", "#38bdf8",
    "#fde047", "#fde047", "#fde047", "#fb7185", "#fb7185",
  ]),
  directionSentinels: Object.freeze([
    Object.freeze({ role: "ingress", order: 0, expected: Object.freeze(["right"]) }),
    Object.freeze({ role: "policy", order: 0, expected: Object.freeze(["right"]) }),
    Object.freeze({ role: "fanout", order: 0, expected: Object.freeze(["down", "left", "down"]) }),
    Object.freeze({ role: "fanout", order: 1, expected: Object.freeze(["down"]) }),
    Object.freeze({ role: "fanout", order: 2, expected: Object.freeze(["down", "right", "down"]) }),
    Object.freeze({ role: "data-write", order: 0, expected: Object.freeze(["down"]) }),
    Object.freeze({ role: "data-write", order: 1, expected: Object.freeze(["down"]) }),
    Object.freeze({ role: "data-write", order: 2, expected: Object.freeze(["down"]) }),
    Object.freeze({ role: "event", order: 0, expected: Object.freeze(["right"]) }),
    Object.freeze({ role: "telemetry", order: 0, expected: Object.freeze(["down"]) }),
  ]),
  resetBehavior: "all ten bodies and registration beads keep advancing while topology, labels, and both Blueprint flow layers fade together",
});
const STYLE_4_STREAM_CONTRACT = Object.freeze({
  start: 36,
  fadeInFactors: Object.freeze([0.30, 0.65, 1.00]),
  bodyDashPattern: Object.freeze([12, 35]),
  dashPeriod: 47,
  dashOffsetPerFrame: -6.0,
  cardAdvancePerFrame: 6.0,
  bodyOpacity: 0.88,
  cardOpacity: 0.98,
  bodyPrimitive: "notion-memory-rail",
  cardPrimitive: "notion-memory-card",
  bodyColor: "semantic-memory-destination",
  bodyWidthMaximum: 3.0,
  bodyWidthMinimum: 2.4,
  bodyWidthMultiplier: 1.50,
  bodyWidthPrecision: 2,
  bodyWidthDescription: "min(3.0, max(2.4, source_stroke * 1.50))",
  sourceStrokeWidth: 1.8,
  resolvedStrokeWidth: 2.70,
  phaseStageMultiplier: 7,
  phaseOrderMultiplier: 0,
  phasePolicy: "(motionStage * 7 + motionOrder * 0) mod 47",
  expectedStreamPhases: Object.freeze([7, 14, 21, 28, 35, 42]),
  expectedSourceColors: Object.freeze([
    "#3b82f6", "#3b82f6", "#3b82f6", "#3b82f6", "#3b82f6", "#3b82f6",
  ]),
  semanticColors: Object.freeze([
    "#3b82f6", "#3b82f6", "#7c3aed", "#059669", "#ea580c", "#ea580c",
  ]),
  initialNormalizedProgress: Object.freeze([0.08, 0.22, 0.36, 0.50, 0.64, 0.78]),
  endpointClearance: 8,
  outerRect: Object.freeze({ x: -7, y: -5, width: 14, height: 10, rx: 2 }),
  cardFill: "#ffffff",
  cardStrokeWidth: 1.4,
  inkStrokeWidth: 2.0,
  inkLinecap: "butt",
  inkShapeRendering: "crispEdges",
  inkLines: Object.freeze([
    Object.freeze({ x1: -4.5, y1: -2, x2: 4, y2: -2 }),
    Object.freeze({ x1: -4.5, y1: 2, x2: 0.5, y2: 2 }),
  ]),
  directionSentinels: Object.freeze([
    Object.freeze({ role: "sample", order: 0, expected: Object.freeze(["right"]), tangentRotation: 0 }),
    Object.freeze({ role: "attend", order: 0, expected: Object.freeze(["right"]), tangentRotation: 0 }),
    Object.freeze({ role: "invoke", order: 0, expected: Object.freeze(["right"]), tangentRotation: 0 }),
    Object.freeze({ role: "remember", order: 0, expected: Object.freeze(["down"]), tangentRotation: 90 }),
    Object.freeze({ role: "consolidate", order: 0, expected: Object.freeze(["right"]), tangentRotation: 0 }),
    Object.freeze({ role: "recall", order: 0, expected: Object.freeze(["up"]), tangentRotation: -90 }),
  ]),
  resetBehavior: "all six rails and six memory cards keep advancing while topology, labels, rails, and cards fade together",
});
const SPECIALIZED_STREAM_CONTRACTS = Object.freeze({
  5: Object.freeze({
    start: 36, fadeInFactors: Object.freeze([0.30, 0.65, 1.00]),
    bodyDashPattern: Object.freeze([13, 30]), dashPeriod: 43, dashOffsetPerFrame: -6,
    advancePerFrame: 6, bodyOpacity: 0.88, bodyPrimitive: "glass-handoff-rail",
    signaturePrimitive: "glass-task-capsule", signatureKind: "glass-task-capsule",
    bodyWidthMaximum: 2.2, bodyWidthDescription: "min(2.2, source_stroke)", endpointClearance: 8,
    phaseStageMultiplier: 7, phaseOrderMultiplier: 3,
    phasePolicy: "(motionStage * 7 + motionOrder * 3) mod 43",
    expectedStreamPhases: Object.freeze([7, 14, 17, 20, 21, 24, 28, 35, 38]),
    directionSentinels: Object.freeze([
      Object.freeze({ role: "ingress", order: 0, expected: Object.freeze(["right"]) }),
      Object.freeze({ role: "delegate", order: 0, expected: Object.freeze(["down", "left", "down"]) }),
      Object.freeze({ role: "delegate", order: 2, expected: Object.freeze(["down", "right", "down"]) }),
      Object.freeze({ role: "evidence", order: 0, expected: Object.freeze(["down"]) }),
      Object.freeze({ role: "context", order: 0, expected: Object.freeze(["right"]) }),
    ]),
    auxiliary: Object.freeze({ kind: "node-halo", primitive: "coordinator-halo", nodeId: "coordinator", periodFrames: 16, minimumOpacity: 0.12, maximumOpacity: 0.32 }),
  }),
  6: Object.freeze({
    start: 36, fadeInFactors: Object.freeze([0.30, 0.65, 1.00]),
    bodyDashPattern: Object.freeze([11, 36]), dashPeriod: 47, dashOffsetPerFrame: -6,
    advancePerFrame: 6, bodyOpacity: 0.82, bodyPrimitive: "governance-thread",
    signaturePrimitive: "policy-seal", signatureKind: "policy-seal",
    bodyWidthMaximum: 2.8, bodyWidthDescription: "min(2.8, source_stroke)", endpointClearance: 8,
    phaseStageMultiplier: 7, phaseOrderMultiplier: 3,
    phasePolicy: "(motionStage * 7 + motionOrder * 3) mod 47",
    expectedStreamPhases: Object.freeze([7, 14, 21, 24, 27, 28, 31, 34, 35]),
    directionSentinels: Object.freeze([
      Object.freeze({ role: "ingress", order: 0, expected: Object.freeze(["right"]) }),
      Object.freeze({ role: "dispatch", order: 0, expected: Object.freeze(["down"]) }),
      Object.freeze({ role: "runtime-branch", order: 0, expected: Object.freeze(["left"]) }),
      Object.freeze({ role: "runtime-branch", order: 1, expected: Object.freeze(["right"]) }),
      Object.freeze({ role: "foundation", order: 0, expected: Object.freeze(["down"]) }),
      Object.freeze({ role: "promote", order: 0, expected: Object.freeze(["right"]) }),
    ]),
  }),
  7: Object.freeze({
    start: 36, fadeInFactors: Object.freeze([0.30, 0.65, 1.00]),
    bodyDashPattern: Object.freeze([10, 33]), dashPeriod: 43, dashOffsetPerFrame: -6,
    advancePerFrame: 6, bodyOpacity: 0.86, bodyPrimitive: "api-token-rail",
    signaturePrimitive: "token-train", signatureKind: "token-train",
    bodyWidthMaximum: 2.5, bodyWidthDescription: "min(2.5, source_stroke)", endpointClearance: 10,
    phaseStageMultiplier: 7, phaseOrderMultiplier: 3,
    phasePolicy: "(motionStage * 7 + motionOrder * 3) mod 43",
    expectedStreamPhases: Object.freeze([7, 14, 21, 28, 31, 35, 38, 42]),
    directionSentinels: Object.freeze([
      Object.freeze({ role: "connect", order: 0, expected: Object.freeze(["right"]) }),
      Object.freeze({ role: "prepare", order: 0, expected: Object.freeze(["down", "left", "down"]) }),
      Object.freeze({ role: "tool-call", order: 0, expected: Object.freeze(["right"]) }),
      Object.freeze({ role: "token-stream", order: 1, expected: Object.freeze(["down", "left", "down"]) }),
      Object.freeze({ role: "govern", order: 0, expected: Object.freeze(["down"]) }),
      Object.freeze({ role: "promote", order: 0, expected: Object.freeze(["right"]) }),
    ]),
  }),
  8: Object.freeze({
    start: 36, fadeInFactors: Object.freeze([0.30, 0.65, 1.00]),
    bodyDashPattern: Object.freeze([14, 33]), dashPeriod: 47, dashOffsetPerFrame: -6,
    advancePerFrame: 6, bodyOpacity: 0.86, bodyPrimitive: "luxury-circuit-rail",
    signaturePrimitive: "gem-tracer", signatureKind: "gem-tracer",
    bodyWidthMaximum: 2.8, bodyWidthDescription: "min(2.8, source_stroke)", endpointClearance: 8,
    phaseStageMultiplier: 7, phaseOrderMultiplier: 3,
    phasePolicy: "(motionStage * 7 + motionOrder * 3) mod 47",
    expectedStreamPhases: Object.freeze([7, 14, 21, 24, 28, 35, 42]),
    directionSentinels: Object.freeze([
      Object.freeze({ role: "primary", stage: 1, order: 0, expected: Object.freeze(["right"]) }),
      Object.freeze({ role: "primary", stage: 2, order: 0, expected: Object.freeze(["right"]) }),
      Object.freeze({ role: "memory-read", stage: 3, order: 0, expected: Object.freeze(["down", "left", "down"]) }),
      Object.freeze({ role: "feedback", stage: 6, order: 0, expected: Object.freeze(["up", "left", "up"]) }),
    ]),
  }),
  9: Object.freeze({
    start: 36, fadeInFactors: Object.freeze([0.30, 0.65, 1.00]),
    bodyDashPattern: Object.freeze([8, 33]), dashPeriod: 41, dashOffsetPerFrame: -5,
    advancePerFrame: 5, bodyOpacity: 0.82, bodyPrimitive: "review-trace-rail",
    signaturePrimitive: "review-cursor", signatureKind: "review-cursor",
    bodyWidthMaximum: 2.6, bodyWidthDescription: "min(2.6, source_stroke)", endpointClearance: 9,
    phaseStageMultiplier: 7, phaseOrderMultiplier: 3,
    phasePolicy: "(motionStage * 7 + motionOrder * 3) mod 41",
    expectedStreamPhases: Object.freeze([7, 14, 21, 28, 31]),
    directionSentinels: Object.freeze([
      Object.freeze({ role: "review-entry", order: 0, expected: Object.freeze(["right"]) }),
      Object.freeze({ role: "review-request", order: 0, expected: Object.freeze(["right"]) }),
      Object.freeze({ role: "review-async", order: 0, expected: Object.freeze(["down"]) }),
      Object.freeze({ role: "review-state", order: 0, expected: Object.freeze(["left"]) }),
      Object.freeze({ role: "review-external", order: 1, expected: Object.freeze(["right"]) }),
    ]),
  }),
  10: Object.freeze({
    start: 36, fadeInFactors: Object.freeze([0.30, 0.65, 1.00]),
    bodyDashPattern: Object.freeze([12, 31]), dashPeriod: 43, dashOffsetPerFrame: -6,
    advancePerFrame: 6, bodyOpacity: 0.82, bodyPrimitive: "cloud-flow-rail",
    signaturePrimitive: "region-chevron-pair-or-replication-capsule", signatureKind: "cloud-flow",
    bodyWidthMaximum: 2.7, bodyWidthDescription: "min(2.7, source_stroke)", endpointClearance: 8,
    phaseStageMultiplier: 7, phaseOrderMultiplier: 0,
    phasePolicy: "motionStage * 7 mod 43; A/B orders are phase-locked",
    expectedStreamPhases: Object.freeze([7, 7, 14, 14, 21]),
    directionSentinels: Object.freeze([
      Object.freeze({ role: "global-route", order: 0, expected: Object.freeze(["down"]) }),
      Object.freeze({ role: "global-route", order: 1, expected: Object.freeze(["down"]) }),
      Object.freeze({ role: "regional-write", order: 0, expected: Object.freeze(["down"]) }),
      Object.freeze({ role: "regional-write", order: 1, expected: Object.freeze(["down"]) }),
      Object.freeze({ role: "cross-region", order: 0, expected: Object.freeze(["right"]) }),
    ]),
    auxiliary: Object.freeze({ kind: "container-pair-pulse", primitive: "availability-pulse", containerIds: Object.freeze(["region-a", "region-b"]), periodFrames: 16, minimumOpacity: 0.10, maximumOpacity: 0.26 }),
  }),
  11: Object.freeze({
    start: 36, fadeInFactors: Object.freeze([0.30, 0.65, 1.00]),
    bodyDashPattern: Object.freeze([8, 33]), dashPeriod: 41, dashOffsetPerFrame: -5,
    advancePerFrame: 5, bodyOpacity: 0.78, bodyPrimitive: "event-transit-rail",
    signaturePrimitive: "event-train-or-branch-car", signatureKind: "event-transit",
    bodyWidthMaximum: 2.2, bodyWidthDescription: "min(2.2, source_stroke)", endpointClearance: 7,
    phaseStageMultiplier: 5, phaseOrderMultiplier: 3,
    phasePolicy: "(motionStage * 5 + motionOrder * 3) mod 41",
    expectedStreamPhases: Object.freeze([5, 10, 15, 20, 25, 28]),
    directionSentinels: Object.freeze([
      Object.freeze({ role: "topic-rail", stage: 1, order: 0, expected: Object.freeze(["right"]) }),
      Object.freeze({ role: "topic-rail", stage: 2, order: 0, expected: Object.freeze(["right"]) }),
      Object.freeze({ role: "topic-rail", stage: 3, order: 0, expected: Object.freeze(["right"]) }),
      Object.freeze({ role: "topic-rail", stage: 4, order: 0, expected: Object.freeze(["right"]) }),
      Object.freeze({ role: "dead-letter", stage: 5, order: 0, expected: Object.freeze(["down"]) }),
      Object.freeze({ role: "state-project", stage: 5, order: 1, expected: Object.freeze(["down"]) }),
    ]),
    auxiliary: Object.freeze({ kind: "station-dwell-rings", primitive: "station-dwell-ring", periodFrames: 10 }),
  }),
  12: Object.freeze({
    start: 36, fadeInFactors: Object.freeze([0.30, 0.65, 1.00]),
    bodyDashPattern: Object.freeze([12, 31]), dashPeriod: 43, dashOffsetPerFrame: -5,
    advancePerFrame: 5, bodyOpacity: 0.84, bodyPrimitive: "incident-pulse-rail-or-telemetry-export-rail",
    signaturePrimitive: "ecg-head-or-telemetry-export-packet", signatureKind: "ops-pulse",
    bodyWidthMaximum: 2.2, bodyWidthDescription: "min(2.2, source_stroke)", endpointClearance: 8,
    phaseStageMultiplier: 5, phaseOrderMultiplier: 0,
    phasePolicy: "motionStage * 5 mod 43",
    expectedStreamPhases: Object.freeze([5, 10, 15, 20]),
    directionSentinels: Object.freeze([
      Object.freeze({ role: "critical-request", stage: 1, order: 0, expected: Object.freeze(["right"]) }),
      Object.freeze({ role: "critical-request", stage: 2, order: 0, expected: Object.freeze(["right"]) }),
      Object.freeze({ role: "critical-request", stage: 3, order: 0, expected: Object.freeze(["right"]) }),
      Object.freeze({ role: "telemetry-export", stage: 4, order: 0, expected: Object.freeze(["down"]) }),
    ]),
    auxiliary: Object.freeze({ kind: "ops-pulse", primitive: "checkout-degraded-halo", nodeId: "checkout-service", periodFrames: 18, minimumOpacity: 0.10, maximumOpacity: 0.28 }),
  }),
});
const TERMINAL_CURSOR_CONTRACT = Object.freeze({
  primitive: "terminal-prompt-cursor",
  nodeId: "terminal",
  sourceText: "_",
  start: 16,
  periodFrames: 16,
  brightFrames: 8,
  absentFrames: 8,
  brightOpacity: 0.95,
  height: 2.2,
  fill: "#a7f3d0",
});
const SCENE_CONTRACTS = Object.freeze({
  "memory-weave": Object.freeze({
    styleId: 1,
    name: "Memory Weave",
    drawSchedule: STYLE_1_DRAW_SCHEDULE,
    stream: STYLE_1_STREAM_CONTRACT,
    signature: null,
    legacyStyleOneReport: true,
  }),
  "tool-grounding": Object.freeze({
    styleId: 2,
    name: "Dark Terminal",
    drawSchedule: STYLE_2_DRAW_SCHEDULE,
    stream: STYLE_2_STREAM_CONTRACT,
    signature: TERMINAL_CURSOR_CONTRACT,
    legacyStyleOneReport: false,
  }),
  "service-blueprint": Object.freeze({
    styleId: 3,
    name: "Blueprint",
    drawSchedule: STYLE_3_DRAW_SCHEDULE,
    stream: STYLE_3_STREAM_CONTRACT,
    signature: null,
    legacyStyleOneReport: false,
    streamMode: "blueprint-registration-bead",
    expectedRouteLabelCount: 7,
    requiresLabelPerEdge: false,
    requiredMaximumConcurrentDraws: 2,
  }),
  "memory-lifecycle": Object.freeze({
    styleId: 4,
    name: "Notion Clean",
    drawSchedule: STYLE_4_DRAW_SCHEDULE,
    stream: STYLE_4_STREAM_CONTRACT,
    signature: null,
    legacyStyleOneReport: false,
    streamMode: "notion-memory-card-handoff",
    expectedRouteLabelCount: 2,
    requiresLabelPerEdge: false,
    requiredMaximumConcurrentDraws: 1,
  }),
  "agent-orchestration": Object.freeze({
    styleId: 5, name: "Glassmorphism", drawSchedule: STYLE_5_DRAW_SCHEDULE,
    stream: SPECIALIZED_STREAM_CONTRACTS[5], signature: null, legacyStyleOneReport: false,
    streamMode: "specialized-live-signature", expectedRouteLabelCount: 9,
    requiresLabelPerEdge: true, requiredMaximumConcurrentDraws: 2,
  }),
  "governed-runtime": Object.freeze({
    styleId: 6, name: "Claude Official", drawSchedule: STYLE_6_DRAW_SCHEDULE,
    stream: SPECIALIZED_STREAM_CONTRACTS[6], signature: null, legacyStyleOneReport: false,
    streamMode: "specialized-live-signature", expectedRouteLabelCount: 9,
    requiresLabelPerEdge: true, requiredMaximumConcurrentDraws: 2,
  }),
  "token-stream": Object.freeze({
    styleId: 7, name: "OpenAI Official", drawSchedule: STYLE_7_DRAW_SCHEDULE,
    stream: SPECIALIZED_STREAM_CONTRACTS[7], signature: null, legacyStyleOneReport: false,
    streamMode: "specialized-live-signature", expectedRouteLabelCount: 8,
    requiresLabelPerEdge: true, requiredMaximumConcurrentDraws: 3,
  }),
  "golden-circuit": Object.freeze({
    styleId: 8, name: "Dark Luxury", drawSchedule: STYLE_8_DRAW_SCHEDULE,
    stream: SPECIALIZED_STREAM_CONTRACTS[8], signature: null, legacyStyleOneReport: false,
    streamMode: "specialized-live-signature", expectedRouteLabelCount: 7,
    requiresLabelPerEdge: true, requiredMaximumConcurrentDraws: 2, stageAwareRouteKey: true,
  }),
  "review-trace": Object.freeze({
    styleId: 9, name: "C4 Review Canvas", drawSchedule: STYLE_9_DRAW_SCHEDULE,
    stream: SPECIALIZED_STREAM_CONTRACTS[9], signature: null, legacyStyleOneReport: false,
    streamMode: "specialized-live-signature", expectedRouteLabelCount: 5,
    requiresLabelPerEdge: true, requiredMaximumConcurrentDraws: 2,
  }),
  "cloud-flow": Object.freeze({
    styleId: 10, name: "Cloud Fabric", drawSchedule: STYLE_10_DRAW_SCHEDULE,
    stream: SPECIALIZED_STREAM_CONTRACTS[10], signature: null, legacyStyleOneReport: false,
    streamMode: "specialized-live-signature", expectedRouteLabelCount: 3,
    requiresLabelPerEdge: false, requiredMaximumConcurrentDraws: 2,
  }),
  "event-transit": Object.freeze({
    styleId: 11, name: "Event Transit", drawSchedule: STYLE_11_DRAW_SCHEDULE,
    stream: SPECIALIZED_STREAM_CONTRACTS[11], signature: null, legacyStyleOneReport: false,
    streamMode: "specialized-live-signature", expectedRouteLabelCount: 0,
    requiresLabelPerEdge: false, requiredMaximumConcurrentDraws: 2, stageAwareRouteKey: true,
  }),
  "ops-pulse": Object.freeze({
    styleId: 12, name: "Ops Pulse", drawSchedule: STYLE_12_DRAW_SCHEDULE,
    stream: SPECIALIZED_STREAM_CONTRACTS[12], signature: null, legacyStyleOneReport: false,
    streamMode: "specialized-live-signature", expectedRouteLabelCount: 0,
    requiresLabelPerEdge: false, requiredMaximumConcurrentDraws: 1, stageAwareRouteKey: true,
  }),
});
const PRESETS = new Set(Object.keys(SCENE_CONTRACTS));

function parseArguments(argv) {
  const values = {};
  for (let index = 0; index < argv.length; index += 1) {
    const argument = argv[index];
    if (argument === "--probe") {
      values.probe = true;
      continue;
    }
    if (!argument.startsWith("--") || index + 1 >= argv.length) {
      throw new Error(`Invalid argument: ${argument}`);
    }
    values[argument.slice(2)] = argv[index + 1];
    index += 1;
  }
  return values;
}

function loadRenderer() {
  const attempts = [];
  const loaders = [
    {
      label: "puppeteer",
      load: () => require("puppeteer"),
      resolve: () => require.resolve("puppeteer"),
      version: () => require("puppeteer/package.json").version,
    },
    {
      label: "puppeteer-core",
      load: () => require("puppeteer-core"),
      resolve: () => require.resolve("puppeteer-core"),
      version: () => require("puppeteer-core/package.json").version,
    },
  ];
  if (process.env.FIREWORKS_PUPPETEER_PATH) {
    const explicitPath = path.resolve(process.env.FIREWORKS_PUPPETEER_PATH);
    loaders.unshift({
      label: "FIREWORKS_PUPPETEER_PATH",
      load: () => require(explicitPath),
      resolve: () => require.resolve(explicitPath),
      version: () => {
        const packagePath = fs.statSync(explicitPath).isDirectory()
          ? path.join(explicitPath, "package.json")
          : path.join(path.dirname(explicitPath), "package.json");
        return JSON.parse(fs.readFileSync(packagePath, "utf8")).version;
      },
    });
  }
  for (const candidate of loaders) {
    try {
      return {
        api: candidate.load(),
        module: candidate.label,
        resolvedModule: candidate.resolve(),
        moduleVersion: candidate.version(),
      };
    } catch (error) {
      attempts.push(`${candidate.label}:${error.code || error.message}`);
    }
  }
  throw new Error(
    `Puppeteer is unavailable. Install puppeteer-core or set FIREWORKS_PUPPETEER_PATH. ${attempts.join("; ")}`,
  );
}

function chromeExecutable(renderer) {
  const candidates = [
    process.env.FIREWORKS_CHROME_PATH,
    "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
    "/Applications/Chromium.app/Contents/MacOS/Chromium",
    "/usr/bin/google-chrome",
    "/usr/bin/google-chrome-stable",
    "/usr/bin/chromium",
    "/usr/bin/chromium-browser",
    "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
    "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
  ].filter(Boolean);
  for (const candidate of candidates) {
    if (fs.existsSync(candidate)) {
      return candidate;
    }
  }
  if (typeof renderer.executablePath === "function") {
    try {
      const bundled = renderer.executablePath();
      if (bundled && fs.existsSync(bundled)) {
        return bundled;
      }
    } catch (error) {
      // puppeteer-core intentionally has no bundled browser.
    }
  }
  return null;
}

function probe() {
  const loaded = loadRenderer();
  const executable = chromeExecutable(loaded.api);
  if (!executable) {
    throw new Error("No compatible Chrome or Chromium executable was found");
  }
  return {
    ok: true,
    module: loaded.module,
    resolved_module: loaded.resolvedModule,
    module_version: loaded.moduleVersion,
    chrome: executable,
    deterministic_timeline: true,
    sandbox_default: "enabled",
    input: "semantic-svg",
    motion_model: "connector-draw-on-with-persistent-data-flow",
    presets: [...PRESETS],
  };
}

async function installMotionRuntime(page, preset, fps, frameCount) {
  const sceneContract = SCENE_CONTRACTS[preset];
  if (!sceneContract) {
    throw new Error(`Preset ${preset} is awaiting style review`);
  }
  return page.evaluate(
    ({
      selectedPreset,
      selectedFps,
      selectedFrameCount,
      selectedSceneContract,
      minimumFrameCount,
      emptyOpeningFrame,
      resetOpacitySamples,
    }) => {
      const SVG_NS = "http://www.w3.org/2000/svg";
      const root = document.querySelector("svg");
      if (!root) {
        throw new Error("SVG root is unavailable");
      }
      const drawSchedule = selectedSceneContract.drawSchedule;
      const persistentStreamContract = selectedSceneContract.stream;
      const signatureContract = selectedSceneContract.signature;
      const expectedStreamPhases = persistentStreamContract.expectedStreamPhases;
      if (Number(root.dataset.styleId) !== selectedSceneContract.styleId) {
        throw new Error(
          `Preset ${selectedPreset} belongs to Style ${selectedSceneContract.styleId}, input is Style ${root.dataset.styleId || "unknown"}`,
        );
      }
      if (selectedFrameCount < minimumFrameCount) {
        throw new Error(`${selectedSceneContract.name} requires at least ${minimumFrameCount} rendered frames`);
      }
      const renderedFrameMax = selectedFrameCount - 1;
      const resetRange = [selectedFrameCount - resetOpacitySamples.length, renderedFrameMax];
      const fullOpacityEnd = resetRange[0] - 1;

      const attributeSignature = (element) => Array.from(element.attributes)
        .map((attribute) => `${attribute.name}=${attribute.value}`)
        .sort()
        .join("\u001f");
      const directTextSignature = (element) => Array.from(element.childNodes)
        .filter((node) => node.nodeType === Node.TEXT_NODE)
        .map((node) => node.data)
        .join("\u001f");
      const staticDomSnapshot = [root, ...root.querySelectorAll("*")].map((element) => ({
        element,
        parent: element.parentNode,
        attributes: attributeSignature(element),
        directText: directTextSignature(element),
      }));
      const assertStaticDomUnchanged = () => {
        for (const entry of staticDomSnapshot) {
          if (
            !entry.element.isConnected ||
            entry.element.parentNode !== entry.parent ||
            attributeSignature(entry.element) !== entry.attributes ||
            directTextSignature(entry.element) !== entry.directText
          ) {
            const identity = entry.element.id
              || entry.element.dataset?.edgeId
              || entry.element.dataset?.nodeId
              || entry.element.tagName;
            throw new Error(`Motion mutated source SVG element: ${identity}`);
          }
        }
      };

      const edges = Array.from(root.querySelectorAll('[data-graph-role="edge"]'));
      const nodes = Array.from(root.querySelectorAll('[data-graph-role="node"]'));
      const routeLabels = Array.from(root.querySelectorAll('[data-graph-role="label"][data-owner]'));
      const routeOwnerDecorations = Array.from(
        root.querySelectorAll('[data-graph-role="decoration"][data-owner]'),
      );
      if (!edges.length || !nodes.length) {
        throw new Error("Semantic edges and nodes are required");
      }
      const routeKey = (role, order, stage) => selectedSceneContract.stageAwareRouteKey
        ? `${role}/${stage}/${order}`
        : `${role}/${order}`;
      const edgesByRouteKey = new Map();
      edges.forEach((edge) => {
        const role = edge.dataset.motionRole || "";
        const order = Number(edge.dataset.motionOrder);
        const stage = Number(edge.dataset.motionStage);
        if (!role || !Number.isInteger(order) || !Number.isInteger(stage)) {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} has invalid motion metadata`);
        }
        const key = routeKey(role, order, stage);
        const matches = edgesByRouteKey.get(key) || [];
        matches.push(edge);
        edgesByRouteKey.set(key, matches);
      });
      const expectedRouteKeys = drawSchedule.map((entry) => routeKey(entry.role, entry.order, entry.stage));
      const missingRouteKeys = expectedRouteKeys.filter((key) => !edgesByRouteKey.has(key));
      const unexpectedRouteKeys = [...edgesByRouteKey.keys()].filter((key) => !expectedRouteKeys.includes(key));
      const duplicateRouteKeys = [...edgesByRouteKey.entries()]
        .filter(([, matches]) => matches.length !== 1)
        .map(([key]) => key);
      if (
        edges.length !== drawSchedule.length
        || missingRouteKeys.length
        || unexpectedRouteKeys.length
        || duplicateRouteKeys.length
      ) {
        throw new Error(
          `${selectedSceneContract.name} requires exact (motion role, motion order) coverage; `
          + `missing=${missingRouteKeys.join(",")}; unexpected=${unexpectedRouteKeys.join(",")}; duplicate=${duplicateRouteKeys.join(",")}`,
        );
      }
      const edgeForKey = (role, order, stage) => edgesByRouteKey.get(routeKey(role, order, stage))[0];
      const edgeForEntry = (entry) => edgeForKey(entry.role, entry.order, entry.stage);
      drawSchedule.forEach((entry) => {
        if (entry.stage !== undefined && Number(edgeForEntry(entry).dataset.motionStage) !== entry.stage) {
          throw new Error(`${selectedSceneContract.name} route ${routeKey(entry.role, entry.order, entry.stage)} has an invalid stage`);
        }
      });
      if (persistentStreamContract.expectedSourceColors) {
        const sourceColors = drawSchedule.map((entry) => edgeForEntry(entry).getAttribute("stroke"));
        if (sourceColors.some((color, index) => color !== persistentStreamContract.expectedSourceColors[index])) {
          throw new Error(
            `${selectedSceneContract.name} semantic route colors changed: expected `
            + `${persistentStreamContract.expectedSourceColors.join(",")}, got ${sourceColors.join(",")}`,
          );
        }
      }
      const labelCounts = new Map();
      routeLabels.forEach((label) => {
        const owner = label.dataset.owner || "";
        labelCounts.set(owner, (labelCounts.get(owner) || 0) + 1);
      });
      const edgeIds = new Set(edges.map((edge) => edge.dataset.edgeId || ""));
      const duplicateLabelOwners = [...labelCounts.entries()]
        .filter(([, count]) => count !== 1)
        .map(([owner]) => owner);
      const unknownLabelOwners = [...labelCounts.keys()].filter((owner) => !edgeIds.has(owner));
      const missingLabelOwners = edges
        .map((edge) => edge.dataset.edgeId || "")
        .filter((edgeId) => !labelCounts.has(edgeId));
      if (
        duplicateLabelOwners.length
        || unknownLabelOwners.length
        || (selectedSceneContract.requiresLabelPerEdge !== false && missingLabelOwners.length)
        || (
          selectedSceneContract.expectedRouteLabelCount !== undefined
          && routeLabels.length !== selectedSceneContract.expectedRouteLabelCount
        )
      ) {
        throw new Error(
          `${selectedSceneContract.name} route-owned label contract changed; `
          + `missing=${missingLabelOwners.join(",")}; unknown=${unknownLabelOwners.join(",")}; duplicate=${duplicateLabelOwners.join(",")}`,
        );
      }

      const concurrency = [];
      for (let frame = 0; frame <= renderedFrameMax; frame += 1) {
        concurrency.push(drawSchedule.filter((entry) => (
          selectedSceneContract.styleId >= 5
            ? frame > entry.start && frame < entry.end
            : frame >= entry.start && frame <= entry.end
        )).length);
      }
      const maximumConcurrentDraws = Math.max(...concurrency);
      if (maximumConcurrentDraws > selectedSceneContract.requiredMaximumConcurrentDraws) {
        throw new Error(
          `Draw-on schedule exceeds the ${selectedSceneContract.requiredMaximumConcurrentDraws}-connector concurrency limit`,
        );
      }
      if (
        selectedSceneContract.requiredMaximumConcurrentDraws !== undefined
        && maximumConcurrentDraws !== selectedSceneContract.requiredMaximumConcurrentDraws
      ) {
        throw new Error(`${selectedSceneContract.name} draw-on concurrency changed`);
      }

      const motionLayer = document.createElementNS(SVG_NS, "g");
      motionLayer.setAttribute("data-graph-role", "decoration");
      motionLayer.setAttribute("data-motion-layer", "connector-draw-on-with-persistent-data-flow");
      motionLayer.setAttribute("aria-hidden", "true");
      motionLayer.setAttribute("pointer-events", "none");
      const edgeHidingStyle = document.createElementNS(SVG_NS, "style");
      edgeHidingStyle.setAttribute("data-motion-source-edge-hider", "true");
      const sourceHidingSelectors = [
        '[data-graph-role="edge"]',
        '[data-graph-role="label"][data-owner]',
      ];
      if (selectedSceneContract.styleId === 11 || selectedSceneContract.styleId === 12) {
        sourceHidingSelectors.push(
          '[data-graph-role="decoration"][data-owner]:not([data-motion-primitive])',
        );
      }
      edgeHidingStyle.textContent = `${sourceHidingSelectors.join(",")}{visibility:hidden!important}`;
      motionLayer.append(edgeHidingStyle);
      const labelLayer = document.createElementNS(SVG_NS, "g");
      labelLayer.setAttribute("data-graph-role", "decoration");
      labelLayer.setAttribute("data-motion-layer", "route-label-arrivals");
      labelLayer.setAttribute("aria-hidden", "true");
      labelLayer.setAttribute("pointer-events", "none");
      const firstNode = nodes[0].parentNode === root ? nodes[0] : null;
      root.insertBefore(motionLayer, firstNode);

      const effects = [];
      const drawReports = [];
      const persistentStreamReports = [];
      const persistentPacketHeadReports = [];
      const registrationBeadReports = [];
      const notionMemoryCardReports = [];
      const specializedSignatureReports = [];
      const auxiliaryReports = [];
      const traceSpanRevealReports = [];
      const settledOwnerDecorationReports = [];
      const settledOwnerDecorationClones = [];
      const transientTraceSpanSources = [];
      let settledOwnerDirectionLayer = null;
      let waterfallScannerReport = null;
      let terminalPromptCursorReport = null;
      const clamped = (value, minimum = 0, maximum = 1) => Math.min(maximum, Math.max(minimum, value));
      const renderedFrameAtTime = (time) => clamped(
        time * selectedFps - 0.5,
        0,
        renderedFrameMax,
      );
      const inclusiveProgress = (frame, start, end) => clamped((frame - start + 1) / (end - start + 1));
      const resetOpacity = (frame) => {
        if (frame <= resetRange[0]) {
          return resetOpacitySamples[0];
        }
        const position = clamped(frame - resetRange[0], 0, resetOpacitySamples.length - 1);
        const lowerIndex = Math.floor(position);
        const upperIndex = Math.min(resetOpacitySamples.length - 1, Math.ceil(position));
        const fraction = position - lowerIndex;
        return resetOpacitySamples[lowerIndex]
          + (resetOpacitySamples[upperIndex] - resetOpacitySamples[lowerIndex]) * fraction;
      };
      const streamFadeIn = (frame) => {
        const position = clamped(
          frame - persistentStreamContract.start,
          0,
          persistentStreamContract.fadeInFactors.length - 1,
        );
        const lowerIndex = Math.floor(position);
        const upperIndex = Math.min(persistentStreamContract.fadeInFactors.length - 1, Math.ceil(position));
        const fraction = position - lowerIndex;
        return persistentStreamContract.fadeInFactors[lowerIndex]
          + (
            persistentStreamContract.fadeInFactors[upperIndex]
            - persistentStreamContract.fadeInFactors[lowerIndex]
          ) * fraction;
      };
      const removeCloneIdentity = (clone) => {
        for (const attribute of Array.from(clone.attributes)) {
          if (attribute.name === "id" || attribute.name.startsWith("data-")) {
            clone.removeAttribute(attribute.name);
          }
        }
      };
      const prepareDecoration = (edge, primitive, preserveMarkers) => {
        const clone = edge.cloneNode(false);
        removeCloneIdentity(clone);
        if (!preserveMarkers) {
          clone.removeAttribute("marker-start");
          clone.removeAttribute("marker-mid");
          clone.removeAttribute("marker-end");
        }
        clone.removeAttribute("filter");
        clone.setAttribute("data-graph-role", "decoration");
        clone.setAttribute("data-motion-primitive", primitive);
        clone.setAttribute("data-owner", edge.dataset.edgeId || "");
        clone.setAttribute("aria-hidden", "true");
        clone.setAttribute("pointer-events", "none");
        clone.setAttribute("display", "none");
        clone.setAttribute("opacity", "0");
        return clone;
      };

      function addDrawOnRoute(edge, entry) {
        if (typeof edge.getTotalLength !== "function") {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} does not support path drawing`);
        }
        const length = edge.getTotalLength();
        if (!Number.isFinite(length) || length <= 0) {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} has invalid path length`);
        }
        const drawPath = prepareDecoration(edge, "connector-draw-on", false);
        drawPath.setAttribute("stroke-dasharray", `${length} ${length}`);
        drawPath.setAttribute("stroke-dashoffset", String(length));
        const settledPath = prepareDecoration(edge, "settled-connector", true);
        settledPath.removeAttribute("stroke-dasharray");
        settledPath.removeAttribute("stroke-dashoffset");
        motionLayer.append(drawPath, settledPath);

        effects.push((time) => {
          const frame = renderedFrameAtTime(time);
          const opacity = resetOpacity(frame);
          if (frame < entry.start) {
            drawPath.setAttribute("display", "none");
            drawPath.setAttribute("opacity", "0");
            settledPath.setAttribute("display", "none");
            settledPath.setAttribute("opacity", "0");
            return;
          }
          if (frame < entry.end) {
            const startsIdleBatch = frame === entry.start && !drawSchedule.some((candidate) => (
              candidate !== entry && frame > candidate.start && frame < candidate.end
            ));
            const raw = selectedSceneContract.styleId >= 5
              ? clamped(
                (frame - entry.start + (startsIdleBatch ? 1 : 0))
                  / (entry.end - entry.start + (startsIdleBatch ? 1 : 0)),
              )
              : inclusiveProgress(frame, entry.start, entry.end);
            const progress = raw;
            drawPath.setAttribute("display", "inline");
            drawPath.setAttribute("opacity", String(opacity));
            drawPath.setAttribute("stroke-dashoffset", String(length * (1 - progress)));
            settledPath.setAttribute("display", "none");
            settledPath.setAttribute("opacity", "0");
            return;
          }
          drawPath.setAttribute("display", "none");
          drawPath.setAttribute("opacity", "0");
          settledPath.setAttribute("display", "inline");
          settledPath.setAttribute("opacity", String(opacity));
        });
        const drawReport = {
          edge_id: edge.dataset.edgeId || "",
          role: entry.role,
          rendered_frames: [entry.start, entry.end],
          easing: "linear",
          marker_during_draw: false,
          marker_after_arrival: Boolean(edge.getAttribute("marker-end")),
        };
        if (!selectedSceneContract.legacyStyleOneReport) {
          drawReport.stage = Number(edge.dataset.motionStage);
          drawReport.order = Number(edge.dataset.motionOrder);
          drawReport.schedule_key = routeKey(entry.role, entry.order, entry.stage);
        }
        drawReports.push(drawReport);
      }

      function addPersistentDataFlow(edge, entry) {
        const sourceWidth = Number.parseFloat(edge.getAttribute("stroke-width") || "1.5");
        const rawBodyStrokeWidth = Math.min(
          persistentStreamContract.bodyWidthMaximum,
          Math.max(
            persistentStreamContract.bodyWidthMinimum,
            sourceWidth * persistentStreamContract.bodyWidthMultiplier,
          ),
        );
        const bodyStrokeWidth = persistentStreamContract.bodyWidthPrecision === undefined
          ? rawBodyStrokeWidth
          : Number(rawBodyStrokeWidth.toFixed(persistentStreamContract.bodyWidthPrecision));
        const motionStage = Number(edge.dataset.motionStage);
        const motionOrder = Number(edge.dataset.motionOrder);
        if (!Number.isFinite(motionStage) || !Number.isFinite(motionOrder)) {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} has invalid stream phase metadata`);
        }
        const initialPhase = (
          (
            motionStage * persistentStreamContract.phaseStageMultiplier
            + motionOrder * 3
          ) % persistentStreamContract.dashPeriod
          + persistentStreamContract.dashPeriod
        ) % persistentStreamContract.dashPeriod;
        const streamColor = persistentStreamContract.bodyColor === "inherit-source-stroke"
          ? edge.getAttribute("stroke")
          : persistentStreamContract.bodyColor;
        const packetHeadColor = persistentStreamContract.headColor;
        const stream = prepareDecoration(edge, persistentStreamContract.bodyPrimitive, false);
        stream.setAttribute("fill", "none");
        stream.setAttribute("stroke", streamColor);
        stream.setAttribute("stroke-width", String(bodyStrokeWidth));
        stream.setAttribute("stroke-linecap", "round");
        stream.setAttribute("stroke-linejoin", "round");
        stream.setAttribute("stroke-dasharray", persistentStreamContract.bodyDashPattern.join(" "));

        const packetHead = prepareDecoration(edge, persistentStreamContract.headPrimitive, false);
        packetHead.setAttribute("fill", "none");
        packetHead.setAttribute("stroke", packetHeadColor);
        packetHead.setAttribute("stroke-width", String(persistentStreamContract.headStrokeWidth));
        packetHead.setAttribute("stroke-linecap", "round");
        packetHead.setAttribute("stroke-linejoin", "round");
        packetHead.setAttribute("stroke-dasharray", persistentStreamContract.headDashPattern.join(" "));
        motionLayer.append(stream, packetHead);

        const streamDecorations = [stream, packetHead];
        const markerFree = streamDecorations.every((decoration) => (
          !decoration.hasAttribute("marker-start")
          && !decoration.hasAttribute("marker-mid")
          && !decoration.hasAttribute("marker-end")
        ));
        const filterFree = streamDecorations.every((decoration) => !decoration.hasAttribute("filter"));
        if (!markerFree || !filterFree) {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} stream pair must be marker-free and filter-free`);
        }

        effects.push((time) => {
          const frame = renderedFrameAtTime(time);
          if (frame < persistentStreamContract.start || frame > renderedFrameMax) {
            stream.setAttribute("display", "none");
            stream.setAttribute("opacity", "0");
            packetHead.setAttribute("display", "none");
            packetHead.setAttribute("opacity", "0");
            return;
          }
          const dashOffset = initialPhase
            + (frame - persistentStreamContract.start) * persistentStreamContract.dashOffsetPerFrame;
          const fade = streamFadeIn(frame) * resetOpacity(frame);
          stream.setAttribute("display", "inline");
          stream.setAttribute("opacity", String(persistentStreamContract.bodyOpacity * fade));
          stream.setAttribute("stroke-dashoffset", String(dashOffset));
          packetHead.setAttribute("display", "inline");
          packetHead.setAttribute("opacity", String(persistentStreamContract.headOpacity * fade));
          packetHead.setAttribute(
            "stroke-dashoffset",
            String(dashOffset + persistentStreamContract.headLeadOffset),
          );
        });
        const streamReport = {
          edge_id: edge.dataset.edgeId || "",
          role: entry.role,
          primitive: persistentStreamContract.bodyPrimitive,
          rendered_frames: [persistentStreamContract.start, renderedFrameMax],
          fade_in_frames: [persistentStreamContract.start, persistentStreamContract.start + 2],
          full_opacity_frames: [persistentStreamContract.start + 2, fullOpacityEnd],
          stroke_width: bodyStrokeWidth,
          color: streamColor,
          dash_pattern: persistentStreamContract.bodyDashPattern,
          dash_period: persistentStreamContract.dashPeriod,
          dash_offset_per_rendered_frame: persistentStreamContract.dashOffsetPerFrame,
          initial_phase: initialPhase,
          phase_policy: persistentStreamContract.phasePolicy,
          motion_stage: motionStage,
          motion_order: motionOrder,
          direction: "source-to-target",
          opacity: persistentStreamContract.bodyOpacity,
          travel_easing: "linear",
          marker_free: markerFree,
          filter_free: filterFree,
        };
        const packetHeadReport = {
          edge_id: edge.dataset.edgeId || "",
          role: entry.role,
          primitive: persistentStreamContract.headPrimitive,
          rendered_frames: [persistentStreamContract.start, renderedFrameMax],
          fade_in_frames: [persistentStreamContract.start, persistentStreamContract.start + 2],
          full_opacity_frames: [persistentStreamContract.start + 2, fullOpacityEnd],
          stroke_width: persistentStreamContract.headStrokeWidth,
          color: packetHeadColor,
          dash_pattern: persistentStreamContract.headDashPattern,
          dash_period: persistentStreamContract.dashPeriod,
          dash_offset_per_rendered_frame: persistentStreamContract.dashOffsetPerFrame,
          body_initial_phase: initialPhase,
          dash_offset_from_body: persistentStreamContract.headLeadOffset,
          initial_dash_offset: initialPhase + persistentStreamContract.headLeadOffset,
          direction: "source-to-target",
          opacity: persistentStreamContract.headOpacity,
          travel_easing: "linear",
          marker_free: markerFree,
          filter_free: filterFree,
        };
        if (!selectedSceneContract.legacyStyleOneReport) {
          streamReport.schedule_key = routeKey(entry.role, entry.order, entry.stage);
          streamReport.source_stroke = edge.getAttribute("stroke");
          packetHeadReport.schedule_key = routeKey(entry.role, entry.order, entry.stage);
          packetHeadReport.motion_stage = motionStage;
          packetHeadReport.motion_order = motionOrder;
        }
        persistentStreamReports.push(streamReport);
        persistentPacketHeadReports.push(packetHeadReport);
      }

      function addBlueprintDistributionWave(edge, entry) {
        if (typeof edge.getTotalLength !== "function" || typeof edge.getPointAtLength !== "function") {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} does not support registration-bead travel`);
        }
        const pathLength = edge.getTotalLength();
        if (!Number.isFinite(pathLength) || pathLength <= 0) {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} has invalid registration-bead path length`);
        }
        const sourceWidth = Number.parseFloat(edge.getAttribute("stroke-width") || "1.5");
        if (sourceWidth !== persistentStreamContract.sourceStrokeWidth) {
          throw new Error(
            `Edge ${edge.dataset.edgeId || "unknown"} source stroke width changed: `
            + `expected ${persistentStreamContract.sourceStrokeWidth}, got ${sourceWidth}`,
          );
        }
        const rawBodyStrokeWidth = Math.min(
          persistentStreamContract.bodyWidthMaximum,
          Math.max(
            persistentStreamContract.bodyWidthMinimum,
            sourceWidth * persistentStreamContract.bodyWidthMultiplier,
          ),
        );
        const bodyStrokeWidth = Number(rawBodyStrokeWidth.toFixed(persistentStreamContract.bodyWidthPrecision));
        if (bodyStrokeWidth !== persistentStreamContract.resolvedStrokeWidth) {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} Blueprint wave width changed`);
        }
        const motionStage = Number(edge.dataset.motionStage);
        const motionOrder = Number(edge.dataset.motionOrder);
        if (!Number.isFinite(motionStage) || !Number.isFinite(motionOrder)) {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} has invalid Blueprint phase metadata`);
        }
        const initialPhase = (
          (
            motionStage * persistentStreamContract.phaseStageMultiplier
            + motionOrder * persistentStreamContract.phaseOrderMultiplier
          ) % persistentStreamContract.dashPeriod
          + persistentStreamContract.dashPeriod
        ) % persistentStreamContract.dashPeriod;
        const sourceColor = edge.getAttribute("stroke");
        const stream = prepareDecoration(edge, persistentStreamContract.bodyPrimitive, false);
        stream.setAttribute("fill", "none");
        stream.setAttribute("stroke", sourceColor);
        stream.setAttribute("stroke-width", String(bodyStrokeWidth));
        stream.setAttribute("stroke-linecap", "round");
        stream.setAttribute("stroke-linejoin", "round");
        stream.setAttribute("stroke-dasharray", persistentStreamContract.bodyDashPattern.join(" "));

        const bead = document.createElementNS(SVG_NS, "circle");
        const initialPoint = edge.getPointAtLength(initialPhase % pathLength);
        bead.setAttribute("data-graph-role", "decoration");
        bead.setAttribute("data-motion-primitive", persistentStreamContract.beadPrimitive);
        bead.setAttribute("data-owner", edge.dataset.edgeId || "");
        bead.setAttribute("cx", String(initialPoint.x));
        bead.setAttribute("cy", String(initialPoint.y));
        bead.setAttribute("r", String(persistentStreamContract.beadRadius));
        bead.setAttribute("fill", persistentStreamContract.beadFill);
        bead.setAttribute("stroke", sourceColor);
        bead.setAttribute("stroke-width", String(persistentStreamContract.beadStrokeWidth));
        bead.setAttribute("opacity", "0");
        bead.setAttribute("aria-hidden", "true");
        bead.setAttribute("pointer-events", "none");
        motionLayer.append(stream, bead);

        const decorations = [stream, bead];
        const markerFree = decorations.every((decoration) => (
          !decoration.hasAttribute("marker-start")
          && !decoration.hasAttribute("marker-mid")
          && !decoration.hasAttribute("marker-end")
        ));
        const filterFree = decorations.every((decoration) => !decoration.hasAttribute("filter"));
        if (!markerFree || !filterFree) {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} Blueprint wave must be marker-free and filter-free`);
        }

        effects.push((time) => {
          const frame = renderedFrameAtTime(time);
          if (frame < persistentStreamContract.start || frame > renderedFrameMax) {
            stream.setAttribute("display", "none");
            stream.setAttribute("opacity", "0");
            bead.setAttribute("opacity", "0");
            return;
          }
          const liveFrame = frame - persistentStreamContract.start;
          const dashOffset = initialPhase + liveFrame * persistentStreamContract.dashOffsetPerFrame;
          const beadDistance = (
            (
              initialPhase + liveFrame * persistentStreamContract.beadAdvancePerFrame
            ) % pathLength
            + pathLength
          ) % pathLength;
          const point = edge.getPointAtLength(beadDistance);
          const fade = streamFadeIn(frame) * resetOpacity(frame);
          stream.setAttribute("display", "inline");
          stream.setAttribute("opacity", String(persistentStreamContract.bodyOpacity * fade));
          stream.setAttribute("stroke-dashoffset", String(dashOffset));
          bead.setAttribute("cx", String(point.x));
          bead.setAttribute("cy", String(point.y));
          bead.setAttribute("opacity", String(persistentStreamContract.beadOpacity * fade));
        });
        persistentStreamReports.push({
          edge_id: edge.dataset.edgeId || "",
          role: entry.role,
          schedule_key: routeKey(entry.role, entry.order, entry.stage),
          primitive: persistentStreamContract.bodyPrimitive,
          rendered_frames: [persistentStreamContract.start, renderedFrameMax],
          fade_in_frames: [persistentStreamContract.start, persistentStreamContract.start + 2],
          full_opacity_frames: [persistentStreamContract.start + 2, fullOpacityEnd],
          stroke_width: bodyStrokeWidth,
          color: sourceColor,
          source_stroke: sourceColor,
          dash_pattern: persistentStreamContract.bodyDashPattern,
          dash_period: persistentStreamContract.dashPeriod,
          dash_offset_per_rendered_frame: persistentStreamContract.dashOffsetPerFrame,
          initial_phase: initialPhase,
          phase_policy: persistentStreamContract.phasePolicy,
          motion_stage: motionStage,
          motion_order: motionOrder,
          direction: "source-to-target",
          opacity: persistentStreamContract.bodyOpacity,
          travel_easing: "linear",
          marker_free: markerFree,
          filter_free: filterFree,
        });
        registrationBeadReports.push({
          edge_id: edge.dataset.edgeId || "",
          role: entry.role,
          schedule_key: routeKey(entry.role, entry.order, entry.stage),
          primitive: persistentStreamContract.beadPrimitive,
          shape: "circle",
          rendered_frames: [persistentStreamContract.start, renderedFrameMax],
          fade_in_frames: [persistentStreamContract.start, persistentStreamContract.start + 2],
          full_opacity_frames: [persistentStreamContract.start + 2, fullOpacityEnd],
          radius: persistentStreamContract.beadRadius,
          fill: persistentStreamContract.beadFill,
          stroke: sourceColor,
          stroke_width: persistentStreamContract.beadStrokeWidth,
          opacity: persistentStreamContract.beadOpacity,
          initial_path_distance: initialPhase,
          initial_point: { x: initialPoint.x, y: initialPoint.y },
          path_length: pathLength,
          path_advance_per_rendered_frame: persistentStreamContract.beadAdvancePerFrame,
          direction: "source-to-target",
          wrap: "target-end-to-source-start",
          animated_attributes: ["cx", "cy", "opacity"],
          motion_stage: motionStage,
          motion_order: motionOrder,
          marker_free: markerFree,
          filter_free: filterFree,
        });
      }

      function addNotionMemoryCardHandoff(edge, entry) {
        if (typeof edge.getTotalLength !== "function" || typeof edge.getPointAtLength !== "function") {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} does not support Notion memory-card travel`);
        }
        const pathLength = edge.getTotalLength();
        const endpointClearance = persistentStreamContract.endpointClearance;
        if (!Number.isFinite(pathLength) || pathLength <= endpointClearance * 2) {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} has invalid Notion memory-card path length`);
        }
        const scheduleIndex = drawSchedule.findIndex((candidate) => (
          candidate.role === entry.role && candidate.order === entry.order
        ));
        if (scheduleIndex < 0) {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} is absent from the Notion schedule`);
        }
        const sourceWidth = Number.parseFloat(edge.getAttribute("stroke-width") || "1.5");
        if (sourceWidth !== persistentStreamContract.sourceStrokeWidth) {
          throw new Error(
            `Edge ${edge.dataset.edgeId || "unknown"} source stroke width changed: `
            + `expected ${persistentStreamContract.sourceStrokeWidth}, got ${sourceWidth}`,
          );
        }
        const rawBodyStrokeWidth = Math.min(
          persistentStreamContract.bodyWidthMaximum,
          Math.max(
            persistentStreamContract.bodyWidthMinimum,
            sourceWidth * persistentStreamContract.bodyWidthMultiplier,
          ),
        );
        const bodyStrokeWidth = Number(rawBodyStrokeWidth.toFixed(persistentStreamContract.bodyWidthPrecision));
        if (bodyStrokeWidth !== persistentStreamContract.resolvedStrokeWidth) {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} Notion rail width changed`);
        }
        const motionStage = Number(edge.dataset.motionStage);
        const motionOrder = Number(edge.dataset.motionOrder);
        if (!Number.isFinite(motionStage) || !Number.isFinite(motionOrder)) {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} has invalid Notion phase metadata`);
        }
        const initialPhase = (
          (
            motionStage * persistentStreamContract.phaseStageMultiplier
            + motionOrder * persistentStreamContract.phaseOrderMultiplier
          ) % persistentStreamContract.dashPeriod
          + persistentStreamContract.dashPeriod
        ) % persistentStreamContract.dashPeriod;
        const semanticColor = persistentStreamContract.semanticColors[scheduleIndex];
        const initialNormalizedProgress = persistentStreamContract.initialNormalizedProgress[scheduleIndex];
        const availableTravel = pathLength - endpointClearance * 2;
        const initialPathDistance = endpointClearance + initialNormalizedProgress * availableTravel;

        const normalizeRotation = (rotation) => {
          const normalized = ((rotation + 180) % 360 + 360) % 360 - 180;
          return Math.abs(normalized) < 1e-9 ? 0 : normalized;
        };
        const tangentRotationAtDistance = (distance) => {
          const delta = Math.min(0.25, availableTravel / 4);
          const before = edge.getPointAtLength(Math.max(0, distance - delta));
          const after = edge.getPointAtLength(Math.min(pathLength, distance + delta));
          return normalizeRotation(Math.atan2(after.y - before.y, after.x - before.x) * 180 / Math.PI);
        };
        const initialPoint = edge.getPointAtLength(initialPathDistance);
        const initialTangentRotation = tangentRotationAtDistance(initialPathDistance);
        const sentinel = persistentStreamContract.directionSentinels[scheduleIndex];
        if (
          !sentinel
          || sentinel.role !== entry.role
          || sentinel.order !== entry.order
          || Math.abs(initialTangentRotation - sentinel.tangentRotation) > 0.001
        ) {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} Notion tangent rotation changed`);
        }

        const stream = prepareDecoration(edge, persistentStreamContract.bodyPrimitive, false);
        stream.setAttribute("fill", "none");
        stream.setAttribute("stroke", semanticColor);
        stream.setAttribute("stroke-width", String(bodyStrokeWidth));
        stream.setAttribute("stroke-linecap", "round");
        stream.setAttribute("stroke-linejoin", "round");
        stream.setAttribute("stroke-dasharray", persistentStreamContract.bodyDashPattern.join(" "));

        const card = document.createElementNS(SVG_NS, "g");
        card.setAttribute("data-graph-role", "decoration");
        card.setAttribute("data-motion-primitive", persistentStreamContract.cardPrimitive);
        card.setAttribute("data-owner", edge.dataset.edgeId || "");
        card.setAttribute(
          "transform",
          `translate(${initialPoint.x} ${initialPoint.y}) rotate(${initialTangentRotation})`,
        );
        card.setAttribute("opacity", "0");
        card.setAttribute("aria-hidden", "true");
        card.setAttribute("pointer-events", "none");

        const outer = document.createElementNS(SVG_NS, "rect");
        outer.setAttribute("x", String(persistentStreamContract.outerRect.x));
        outer.setAttribute("y", String(persistentStreamContract.outerRect.y));
        outer.setAttribute("width", String(persistentStreamContract.outerRect.width));
        outer.setAttribute("height", String(persistentStreamContract.outerRect.height));
        outer.setAttribute("rx", String(persistentStreamContract.outerRect.rx));
        outer.setAttribute("fill", persistentStreamContract.cardFill);
        outer.setAttribute("stroke", semanticColor);
        outer.setAttribute("stroke-width", String(persistentStreamContract.cardStrokeWidth));

        const inkLines = persistentStreamContract.inkLines.map((geometry) => {
          const line = document.createElementNS(SVG_NS, "line");
          Object.entries(geometry).forEach(([name, value]) => line.setAttribute(name, String(value)));
          line.setAttribute("stroke", semanticColor);
          line.setAttribute("stroke-width", String(persistentStreamContract.inkStrokeWidth));
          line.setAttribute("stroke-linecap", persistentStreamContract.inkLinecap);
          line.setAttribute("shape-rendering", persistentStreamContract.inkShapeRendering);
          return line;
        });
        card.append(outer, ...inkLines);
        motionLayer.append(stream, card);

        const decorations = [stream, card, outer, ...inkLines];
        const markerFree = decorations.every((decoration) => (
          !decoration.hasAttribute("marker-start")
          && !decoration.hasAttribute("marker-mid")
          && !decoration.hasAttribute("marker-end")
        ));
        const filterFree = decorations.every((decoration) => !decoration.hasAttribute("filter"));
        const shadowFree = decorations.every((decoration) => (
          !decoration.hasAttribute("filter") && !decoration.hasAttribute("style")
        ));
        if (!markerFree || !filterFree || !shadowFree) {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} Notion handoff must be marker/filter/shadow-free`);
        }

        effects.push((time) => {
          const frame = renderedFrameAtTime(time);
          if (frame < persistentStreamContract.start || frame > renderedFrameMax) {
            stream.setAttribute("display", "none");
            stream.setAttribute("opacity", "0");
            card.setAttribute("opacity", "0");
            return;
          }
          const liveFrame = frame - persistentStreamContract.start;
          const dashOffset = initialPhase + liveFrame * persistentStreamContract.dashOffsetPerFrame;
          const cardDistance = endpointClearance + (
            (
              initialPathDistance - endpointClearance
              + liveFrame * persistentStreamContract.cardAdvancePerFrame
            ) % availableTravel
            + availableTravel
          ) % availableTravel;
          const point = edge.getPointAtLength(cardDistance);
          const tangentRotation = tangentRotationAtDistance(cardDistance);
          if (Math.abs(tangentRotation - sentinel.tangentRotation) > 0.001) {
            throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} Notion card left its declared tangent`);
          }
          const fade = streamFadeIn(frame) * resetOpacity(frame);
          stream.setAttribute("display", "inline");
          stream.setAttribute("opacity", String(persistentStreamContract.bodyOpacity * fade));
          stream.setAttribute("stroke-dashoffset", String(dashOffset));
          card.setAttribute("transform", `translate(${point.x} ${point.y}) rotate(${tangentRotation})`);
          card.setAttribute("opacity", String(persistentStreamContract.cardOpacity * fade));
        });

        persistentStreamReports.push({
          edge_id: edge.dataset.edgeId || "",
          role: entry.role,
          schedule_key: routeKey(entry.role, entry.order, entry.stage),
          primitive: persistentStreamContract.bodyPrimitive,
          rendered_frames: [persistentStreamContract.start, renderedFrameMax],
          fade_in_frames: [persistentStreamContract.start, persistentStreamContract.start + 2],
          full_opacity_frames: [persistentStreamContract.start + 2, fullOpacityEnd],
          stroke_width: bodyStrokeWidth,
          color: semanticColor,
          source_stroke: edge.getAttribute("stroke"),
          dash_pattern: persistentStreamContract.bodyDashPattern,
          dash_period: persistentStreamContract.dashPeriod,
          dash_offset_per_rendered_frame: persistentStreamContract.dashOffsetPerFrame,
          initial_phase: initialPhase,
          phase_policy: persistentStreamContract.phasePolicy,
          motion_stage: motionStage,
          motion_order: motionOrder,
          direction: "source-to-target",
          opacity: persistentStreamContract.bodyOpacity,
          travel_easing: "linear",
          marker_free: markerFree,
          filter_free: filterFree,
        });
        notionMemoryCardReports.push({
          edge_id: edge.dataset.edgeId || "",
          role: entry.role,
          schedule_key: routeKey(entry.role, entry.order, entry.stage),
          primitive: persistentStreamContract.cardPrimitive,
          shape: "group",
          rendered_frames: [persistentStreamContract.start, renderedFrameMax],
          fade_in_frames: [persistentStreamContract.start, persistentStreamContract.start + 2],
          full_opacity_frames: [persistentStreamContract.start + 2, fullOpacityEnd],
          outer_rect: {
            ...persistentStreamContract.outerRect,
            fill: persistentStreamContract.cardFill,
            stroke: semanticColor,
            stroke_width: persistentStreamContract.cardStrokeWidth,
          },
          ink_lines: persistentStreamContract.inkLines,
          ink_stroke: semanticColor,
          ink_stroke_width: persistentStreamContract.inkStrokeWidth,
          ink_linecap: persistentStreamContract.inkLinecap,
          ink_shape_rendering: persistentStreamContract.inkShapeRendering,
          opacity: persistentStreamContract.cardOpacity,
          semantic_color: semanticColor,
          path_length: pathLength,
          endpoint_clearance: endpointClearance,
          initial_normalized_progress: initialNormalizedProgress,
          initial_path_distance: initialPathDistance,
          initial_point: { x: initialPoint.x, y: initialPoint.y },
          tangent_rotation: initialTangentRotation,
          path_advance_per_rendered_frame: persistentStreamContract.cardAdvancePerFrame,
          direction: "source-to-target",
          wrap: "target-clearance-to-source-clearance",
          animated_attributes: ["transform", "opacity"],
          motion_stage: motionStage,
          motion_order: motionOrder,
          marker_free: markerFree,
          filter_free: filterFree,
          shadow_free: shadowFree,
        });
      }

      const parseGraphBounds = (element) => {
        const values = (element.dataset.graphBounds || "").split(",").map(Number);
        if (values.length !== 4 || values.some((value) => !Number.isFinite(value))) {
          throw new Error(`Element ${element.id || element.dataset.nodeId || element.dataset.containerId || "unknown"} has invalid graph bounds`);
        }
        return { x: values[0], y: values[1], width: values[2] - values[0], height: values[3] - values[1] };
      };
      const appendMotionAttributes = (element, primitive, owner) => {
        element.setAttribute("data-graph-role", "decoration");
        element.setAttribute("data-motion-primitive", primitive);
        element.setAttribute("data-owner", owner);
        element.setAttribute("aria-hidden", "true");
        element.setAttribute("pointer-events", "none");
      };
      const tangentRotationAt = (edge, distance, pathLength) => {
        const delta = Math.min(0.75, Math.max(0.2, pathLength / 200));
        const before = edge.getPointAtLength(Math.max(0, distance - delta));
        const after = edge.getPointAtLength(Math.min(pathLength, distance + delta));
        return Math.atan2(after.y - before.y, after.x - before.x) * 180 / Math.PI;
      };
      const ensureGemFilter = () => {
        const filterId = "fireworks-motion-gem-halo";
        if (!motionLayer.querySelector(`#${filterId}`)) {
          const defs = document.createElementNS(SVG_NS, "defs");
          const filter = document.createElementNS(SVG_NS, "filter");
          filter.setAttribute("id", filterId);
          filter.setAttribute("x", "-80%");
          filter.setAttribute("y", "-80%");
          filter.setAttribute("width", "260%");
          filter.setAttribute("height", "260%");
          const blur = document.createElementNS(SVG_NS, "feGaussianBlur");
          blur.setAttribute("stdDeviation", "1.4");
          filter.append(blur);
          defs.append(filter);
          motionLayer.prepend(defs);
        }
        return filterId;
      };
      const buildSpecializedSignature = (edge, entry, color) => {
        const owner = edge.dataset.edgeId || "";
        const group = document.createElementNS(SVG_NS, "g");
        let primitive = persistentStreamContract.signaturePrimitive;
        let geometry = {};
        let rearExtent = 0;
        let forwardExtent = 0;
        let filteredElementCount = 0;

        if (persistentStreamContract.signatureKind === "glass-task-capsule") {
          primitive = "glass-task-capsule";
          const plate = document.createElementNS(SVG_NS, "rect");
          plate.setAttribute("x", "-7"); plate.setAttribute("y", "-4.5");
          plate.setAttribute("width", "14"); plate.setAttribute("height", "9"); plate.setAttribute("rx", "3");
          plate.setAttribute("fill", color); plate.setAttribute("fill-opacity", "0.30");
          plate.setAttribute("stroke", "#ffffff"); plate.setAttribute("stroke-opacity", "0.78"); plate.setAttribute("stroke-width", "1");
          plate.setAttribute("data-motion-component", "translucent-plate");
          const highlight = document.createElementNS(SVG_NS, "line");
          highlight.setAttribute("x1", "-4"); highlight.setAttribute("y1", "-2.7");
          highlight.setAttribute("x2", "4"); highlight.setAttribute("y2", "-2.7");
          highlight.setAttribute("stroke", "#ffffff"); highlight.setAttribute("stroke-width", "1"); highlight.setAttribute("stroke-opacity", "0.85");
          highlight.setAttribute("data-motion-component", "white-highlight");
          const dots = [-2.5, 2.5].map((x) => {
            const dot = document.createElementNS(SVG_NS, "circle");
            dot.setAttribute("cx", String(x)); dot.setAttribute("cy", "1.2"); dot.setAttribute("r", "2");
            dot.setAttribute("fill", color); dot.setAttribute("stroke", "#ffffff"); dot.setAttribute("stroke-width", "0.6");
            dot.setAttribute("data-motion-component", "work-item-dot");
            return dot;
          });
          group.append(plate, highlight, ...dots);
          geometry = { shape: "rounded-translucent-plate", width: 14, height: 9, rx: 3, highlight_stroke_width: 1, work_item_dot_radius: 2, work_item_dot_count: 2 };
          rearExtent = 7; forwardExtent = 7;
        } else if (persistentStreamContract.signatureKind === "policy-seal") {
          primitive = "policy-seal";
          const hexagon = document.createElementNS(SVG_NS, "polygon");
          hexagon.setAttribute("points", "0,-6 5.2,-3 5.2,3 0,6 -5.2,3 -5.2,-3");
          hexagon.setAttribute("fill", "#fffaf0"); hexagon.setAttribute("fill-opacity", "0.30");
          hexagon.setAttribute("stroke", "#fffaf0"); hexagon.setAttribute("stroke-width", "1.2");
          hexagon.setAttribute("data-motion-component", "seal-hexagon");
          const dot = document.createElementNS(SVG_NS, "circle");
          dot.setAttribute("cx", "0"); dot.setAttribute("cy", "-1.2"); dot.setAttribute("r", "1.5"); dot.setAttribute("fill", color);
          dot.setAttribute("data-motion-component", "seal-center-dot");
          const bar = document.createElementNS(SVG_NS, "line");
          bar.setAttribute("x1", "-2"); bar.setAttribute("y1", "3"); bar.setAttribute("x2", "2"); bar.setAttribute("y2", "3");
          bar.setAttribute("stroke", color); bar.setAttribute("stroke-width", "1.4"); bar.setAttribute("stroke-linecap", "round");
          bar.setAttribute("data-motion-component", "approval-bar");
          group.append(hexagon, dot, bar);
          geometry = { shape: "warm-white-hexagonal-outline", width: 12, height: 12, center_dot_diameter: 3, approval_bar_width: 4, shadow: false, glow: false };
          rearExtent = 6; forwardExtent = 6;
        } else if (persistentStreamContract.signatureKind === "token-train") {
          primitive = "token-train";
          const opacities = [1, 0.72, 0.44];
          [-8, -2, 4].forEach((x, index) => {
            const cell = document.createElementNS(SVG_NS, "rect");
            cell.setAttribute("x", String(x)); cell.setAttribute("y", "-2"); cell.setAttribute("width", "4"); cell.setAttribute("height", "4"); cell.setAttribute("rx", "1");
            cell.setAttribute("fill", color); cell.setAttribute("fill-opacity", String(opacities[index]));
            cell.setAttribute("data-motion-component", `token-cell-${index + 1}`);
            group.append(cell);
          });
          geometry = { shape: "three-cell-token-train", group_width: 18, group_height: 8, cell_width: 4, cell_height: 4, cell_gap: 2, cell_opacities: opacities };
          rearExtent = 8; forwardExtent = 8;
        } else if (persistentStreamContract.signatureKind === "gem-tracer") {
          primitive = "gem-tracer";
          const tail = document.createElementNS(SVG_NS, "polygon");
          tail.setAttribute("points", "-4,0 -16,-2 -16,2"); tail.setAttribute("fill", color); tail.setAttribute("fill-opacity", "0.55");
          tail.setAttribute("data-motion-component", "tapered-tail");
          const halo = document.createElementNS(SVG_NS, "rect");
          halo.setAttribute("x", "-3.5"); halo.setAttribute("y", "-3.5"); halo.setAttribute("width", "7"); halo.setAttribute("height", "7");
          halo.setAttribute("transform", "rotate(45)"); halo.setAttribute("fill", color); halo.setAttribute("fill-opacity", "0.34");
          halo.setAttribute("filter", `url(#${ensureGemFilter()})`); halo.setAttribute("data-motion-component", "diamond-halo");
          const diamond = document.createElementNS(SVG_NS, "rect");
          diamond.setAttribute("x", "-3.5"); diamond.setAttribute("y", "-3.5"); diamond.setAttribute("width", "7"); diamond.setAttribute("height", "7");
          diamond.setAttribute("transform", "rotate(45)"); diamond.setAttribute("fill", color); diamond.setAttribute("stroke", "#fde68a"); diamond.setAttribute("stroke-width", "0.8");
          diamond.setAttribute("data-motion-component", "gem-diamond");
          const specular = document.createElementNS(SVG_NS, "circle");
          specular.setAttribute("cx", "1"); specular.setAttribute("cy", "-1"); specular.setAttribute("r", "1"); specular.setAttribute("fill", "#ffffff");
          specular.setAttribute("data-motion-component", "specular-point");
          group.append(tail, halo, diamond, specular);
          geometry = { shape: "diamond-with-tapered-tail", diamond_width: 7, diamond_height: 7, diamond_rotation: 45, specular_diameter: 2, tail_length: 12 };
          rearExtent = 16; forwardExtent = 5; filteredElementCount = 1;
        } else if (persistentStreamContract.signatureKind === "review-cursor") {
          primitive = "review-cursor";
          const circle = document.createElementNS(SVG_NS, "circle");
          circle.setAttribute("cx", "0"); circle.setAttribute("cy", "0"); circle.setAttribute("r", "5.5"); circle.setAttribute("fill", "none");
          circle.setAttribute("stroke", color); circle.setAttribute("stroke-width", "1.4"); circle.setAttribute("data-motion-component", "cursor-circle");
          const handle = document.createElementNS(SVG_NS, "line");
          handle.setAttribute("x1", "3.9"); handle.setAttribute("y1", "3.9"); handle.setAttribute("x2", "7.44"); handle.setAttribute("y2", "7.44");
          handle.setAttribute("stroke", color); handle.setAttribute("stroke-width", "1.4"); handle.setAttribute("stroke-linecap", "round"); handle.setAttribute("data-motion-component", "cursor-handle");
          const check = document.createElementNS(SVG_NS, "polyline");
          check.setAttribute("points", "-2,0 -0.5,1.5 2,-1.5"); check.setAttribute("fill", "none"); check.setAttribute("stroke", color); check.setAttribute("stroke-width", "1.2");
          check.setAttribute("stroke-linecap", "round"); check.setAttribute("stroke-linejoin", "round"); check.setAttribute("data-motion-component", "cursor-check");
          group.append(circle, handle, check);
          geometry = { shape: "review-mark", outline_circle_diameter: 11, diagonal_handle_length: 5, internal_check_extent: 3 };
          rearExtent = 6; forwardExtent = 8;
        } else if (persistentStreamContract.signatureKind === "cloud-flow") {
          if (entry.role === "cross-region") {
            primitive = "replication-capsule";
            const capsule = document.createElementNS(SVG_NS, "rect");
            capsule.setAttribute("x", "-7"); capsule.setAttribute("y", "-3.5"); capsule.setAttribute("width", "14"); capsule.setAttribute("height", "7"); capsule.setAttribute("rx", "3.5");
            capsule.setAttribute("fill", "#ffffff"); capsule.setAttribute("fill-opacity", "0.92"); capsule.setAttribute("stroke", color); capsule.setAttribute("stroke-width", "1.2");
            capsule.setAttribute("data-motion-component", "replication-outline");
            [-2.5, 2.5].forEach((x) => {
              const cell = document.createElementNS(SVG_NS, "circle");
              cell.setAttribute("cx", String(x)); cell.setAttribute("cy", "0"); cell.setAttribute("r", "1.5"); cell.setAttribute("fill", "#7c3aed");
              cell.setAttribute("data-motion-component", "replication-data-cell"); group.append(cell);
            });
            group.prepend(capsule);
            geometry = { shape: "replication-capsule", width: 14, height: 7, data_cell_count: 2, direction: "left-to-right" };
            rearExtent = 7; forwardExtent = 7;
          } else {
            primitive = "region-chevron-pair";
            [-2.5, 2.5].forEach((center) => {
              const chevron = document.createElementNS(SVG_NS, "path");
              chevron.setAttribute("d", `M ${center - 3},-2.5 L ${center},0 L ${center - 3},2.5`);
              chevron.setAttribute("fill", "none"); chevron.setAttribute("stroke", color); chevron.setAttribute("stroke-width", "1.6");
              chevron.setAttribute("stroke-linecap", "round"); chevron.setAttribute("stroke-linejoin", "round"); chevron.setAttribute("data-motion-component", "region-chevron");
              group.append(chevron);
            });
            geometry = { shape: "region-chevron-pair", chevron_width: 6, chevron_height: 5, separation: 5 };
            rearExtent = 6; forwardExtent = 3;
          }
        } else if (persistentStreamContract.signatureKind === "event-transit") {
          if (entry.role === "topic-rail") {
            primitive = "event-train";
            [-8, 0, 8].forEach((x) => {
              const car = document.createElementNS(SVG_NS, "circle");
              car.setAttribute("cx", String(x)); car.setAttribute("cy", "0"); car.setAttribute("r", "2.5"); car.setAttribute("fill", color);
              car.setAttribute("stroke", "#ffffff"); car.setAttribute("stroke-width", "0.6"); car.setAttribute("data-motion-component", "event-car"); group.append(car);
            });
            geometry = { shape: "three-car-event-train", car_diameter: 5, car_gap: 3, car_count: 3 };
            rearExtent = 10.5; forwardExtent = 10.5;
          } else if (entry.role === "dead-letter") {
            primitive = "exception-car";
            const exception = document.createElementNS(SVG_NS, "circle");
            exception.setAttribute("cx", "0"); exception.setAttribute("cy", "0"); exception.setAttribute("r", "4"); exception.setAttribute("fill", "#fff7f7");
            exception.setAttribute("stroke", "#c62828"); exception.setAttribute("stroke-width", "1.4"); exception.setAttribute("data-motion-component", "exception-outline");
            const mark = document.createElementNS(SVG_NS, "path");
            mark.setAttribute("d", "M 0,-2 L 0,1 M 0,2.5 L 0,2.6"); mark.setAttribute("stroke", "#c62828"); mark.setAttribute("stroke-width", "1.2"); mark.setAttribute("stroke-linecap", "round");
            mark.setAttribute("data-motion-component", "exception-mark"); group.append(exception, mark);
            geometry = { shape: "red-outlined-exception-car", diameter: 8 };
            rearExtent = 4; forwardExtent = 4;
          } else {
            primitive = "projection-car";
            [-6.5, 1.5].forEach((x) => {
              const cell = document.createElementNS(SVG_NS, "rect");
              cell.setAttribute("x", String(x)); cell.setAttribute("y", "-2.5"); cell.setAttribute("width", "5"); cell.setAttribute("height", "5"); cell.setAttribute("rx", "1.2");
              cell.setAttribute("fill", "#00897b"); cell.setAttribute("stroke", "#ffffff"); cell.setAttribute("stroke-width", "0.6"); cell.setAttribute("data-motion-component", "projection-cell"); group.append(cell);
            });
            geometry = { shape: "teal-two-cell-projection-car", cell_count: 2, cell_width: 5, cell_gap: 3 };
            rearExtent = 6.5; forwardExtent = 6.5;
          }
        } else if (persistentStreamContract.signatureKind === "ops-pulse") {
          if (entry.role === "critical-request") {
            primitive = "ecg-head";
            const ecg = document.createElementNS(SVG_NS, "polyline");
            ecg.setAttribute("points", "-10,0 -6,0 -4,-3.5 -1,3.5 2,-4.5 5,0 10,0"); ecg.setAttribute("fill", "none");
            ecg.setAttribute("stroke", "#fde68a"); ecg.setAttribute("stroke-width", "1.6"); ecg.setAttribute("stroke-linecap", "round"); ecg.setAttribute("stroke-linejoin", "round");
            ecg.setAttribute("data-motion-component", "ecg-waveform"); group.append(ecg);
            geometry = { shape: "compact-ecg-head", width: 20, stroke_width: 1.6 };
            rearExtent = 10; forwardExtent = 10;
          } else {
            primitive = "telemetry-export-packet";
            [-6, 0, 6].forEach((x, index) => {
              const dot = document.createElementNS(SVG_NS, "circle");
              dot.setAttribute("cx", String(x)); dot.setAttribute("cy", "0"); dot.setAttribute("r", "2"); dot.setAttribute("fill", "#22d3ee");
              dot.setAttribute("fill-opacity", String([0.48, 0.72, 1][index])); dot.setAttribute("data-motion-component", "telemetry-dot"); group.append(dot);
            });
            geometry = { shape: "cyan-three-dot-export-packet", dot_count: 3, dot_diameter: 4 };
            rearExtent = 8; forwardExtent = 8;
          }
        } else {
          throw new Error(`${selectedSceneContract.name} has no specialized signature builder`);
        }

        appendMotionAttributes(group, primitive, owner);
        group.setAttribute("opacity", "0");
        return { group, primitive, geometry, rearExtent, forwardExtent, filteredElementCount };
      };

      function addSpecializedLiveSignature(edge, entry) {
        if (typeof edge.getTotalLength !== "function" || typeof edge.getPointAtLength !== "function") {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} does not support specialized live travel`);
        }
        const pathLength = edge.getTotalLength();
        const sourceWidth = Number.parseFloat(edge.getAttribute("stroke-width") || "1.5");
        const bodyStrokeWidth = Number(Math.min(persistentStreamContract.bodyWidthMaximum, sourceWidth).toFixed(2));
        const motionStage = Number(edge.dataset.motionStage);
        const motionOrder = Number(edge.dataset.motionOrder);
        const initialPhase = (
          motionStage * persistentStreamContract.phaseStageMultiplier
          + motionOrder * persistentStreamContract.phaseOrderMultiplier
        ) % persistentStreamContract.dashPeriod;
        const sourceColor = edge.getAttribute("stroke") || "#64748b";
        const bodyPrimitive = selectedSceneContract.styleId === 12
          ? (entry.role === "critical-request" ? "incident-pulse-rail" : "telemetry-export-rail")
          : persistentStreamContract.bodyPrimitive;
        const stream = prepareDecoration(edge, bodyPrimitive, false);
        stream.setAttribute("fill", "none"); stream.setAttribute("stroke", sourceColor); stream.setAttribute("stroke-width", String(bodyStrokeWidth));
        stream.setAttribute("stroke-linecap", "round"); stream.setAttribute("stroke-linejoin", "round");
        stream.setAttribute("stroke-dasharray", persistentStreamContract.bodyDashPattern.join(" "));
        const signature = buildSpecializedSignature(edge, entry, sourceColor);
        const startDistance = persistentStreamContract.endpointClearance + signature.rearExtent;
        const endDistance = pathLength - persistentStreamContract.endpointClearance - signature.forwardExtent;
        const availableTravel = endDistance - startDistance;
        if (!Number.isFinite(pathLength) || availableTravel <= 0) {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} is too short for ${signature.primitive} clearance`);
        }
        const initialDistance = startDistance + (initialPhase % availableTravel);
        const initialPoint = edge.getPointAtLength(initialDistance);
        const initialRotation = tangentRotationAt(edge, initialDistance, pathLength);
        signature.group.setAttribute("transform", `translate(${initialPoint.x} ${initialPoint.y}) rotate(${initialRotation})`);
        motionLayer.append(stream, signature.group);

        effects.push((time) => {
          const frame = renderedFrameAtTime(time);
          if (frame < persistentStreamContract.start || frame > renderedFrameMax) {
            stream.setAttribute("display", "none"); stream.setAttribute("opacity", "0"); signature.group.setAttribute("opacity", "0");
            return;
          }
          const liveFrame = frame - persistentStreamContract.start;
          const dashOffset = initialPhase + liveFrame * persistentStreamContract.dashOffsetPerFrame;
          const distance = startDistance + (
            (initialDistance - startDistance + liveFrame * persistentStreamContract.advancePerFrame) % availableTravel
            + availableTravel
          ) % availableTravel;
          const point = edge.getPointAtLength(distance);
          const rotation = tangentRotationAt(edge, distance, pathLength);
          const fade = streamFadeIn(frame) * resetOpacity(frame);
          stream.setAttribute("display", "inline"); stream.setAttribute("opacity", String(persistentStreamContract.bodyOpacity * fade));
          stream.setAttribute("stroke-dashoffset", String(dashOffset));
          signature.group.setAttribute("transform", `translate(${point.x} ${point.y}) rotate(${rotation})`);
          signature.group.setAttribute("opacity", String(0.98 * fade));
        });

        const scheduleKey = routeKey(entry.role, entry.order, entry.stage);
        persistentStreamReports.push({
          edge_id: edge.dataset.edgeId || "", role: entry.role, schedule_key: scheduleKey,
          primitive: bodyPrimitive, rendered_frames: [persistentStreamContract.start, renderedFrameMax],
          fade_in_frames: [persistentStreamContract.start, persistentStreamContract.start + 2],
          full_opacity_frames: [persistentStreamContract.start + 2, fullOpacityEnd],
          stroke_width: bodyStrokeWidth, maximum_live_width: persistentStreamContract.bodyWidthMaximum,
          source_stroke_width: sourceWidth, dynamic_not_thicker_than_source: bodyStrokeWidth <= sourceWidth,
          color: sourceColor, dash_pattern: persistentStreamContract.bodyDashPattern,
          dash_period: persistentStreamContract.dashPeriod, dash_offset_per_rendered_frame: persistentStreamContract.dashOffsetPerFrame,
          initial_phase: initialPhase, phase_policy: persistentStreamContract.phasePolicy,
          motion_stage: motionStage, motion_order: motionOrder, direction: "source-to-target",
          opacity: persistentStreamContract.bodyOpacity, travel_easing: "linear", marker_free: true, filter_free: true,
        });
        specializedSignatureReports.push({
          edge_id: edge.dataset.edgeId || "", role: entry.role, schedule_key: scheduleKey,
          primitive: signature.primitive, signature_family: persistentStreamContract.signatureKind,
          geometry: signature.geometry, rendered_frames: [persistentStreamContract.start, renderedFrameMax],
          fade_in_frames: [persistentStreamContract.start, persistentStreamContract.start + 2],
          full_opacity_frames: [persistentStreamContract.start + 2, fullOpacityEnd],
          path_length: pathLength, endpoint_clearance: persistentStreamContract.endpointClearance,
          geometry_rear_extent: signature.rearExtent, geometry_forward_extent: signature.forwardExtent,
          center_travel_range: [startDistance, endDistance], initial_path_distance: initialDistance,
          initial_point: { x: initialPoint.x, y: initialPoint.y }, initial_tangent_rotation: initialRotation,
          path_advance_per_rendered_frame: persistentStreamContract.advancePerFrame,
          travel_pixels_per_rendered_frame_at_50_percent: persistentStreamContract.advancePerFrame / 2,
          direction: "source-to-target", wrap: "target-clearance-to-source-clearance",
          animated_attributes: ["transform", "opacity"], source_color: sourceColor,
          marker_free: true, filtered_element_count: signature.filteredElementCount,
          filter_boundary_valid: signature.filteredElementCount <= 1,
          appended_below_labels_and_nodes: true,
        });
      }

      const fixedPulseOpacity = (frame, period, minimum, maximum) => {
        const phase = ((frame - persistentStreamContract.start) % period + period) % period;
        const wave = 0.5 - 0.5 * Math.cos(2 * Math.PI * phase / period);
        return minimum + (maximum - minimum) * wave;
      };
      const addFixedNodeHalo = (config) => {
        const matches = nodes.filter((node) => node.dataset.nodeId === config.nodeId);
        if (matches.length !== 1) {
          throw new Error(`${selectedSceneContract.name} requires node ${config.nodeId} for its halo`);
        }
        const bounds = parseGraphBounds(matches[0]);
        const halo = document.createElementNS(SVG_NS, "rect");
        appendMotionAttributes(halo, config.primitive, config.nodeId);
        halo.setAttribute("x", String(bounds.x - 4)); halo.setAttribute("y", String(bounds.y - 4));
        halo.setAttribute("width", String(bounds.width + 8)); halo.setAttribute("height", String(bounds.height + 8)); halo.setAttribute("rx", "10");
        halo.setAttribute("fill", "none"); halo.setAttribute("stroke", selectedSceneContract.styleId === 12 ? "#f59e0b" : "#ffffff");
        halo.setAttribute("stroke-width", "2"); halo.setAttribute("opacity", "0");
        motionLayer.append(halo);
        effects.push((time) => {
          const frame = renderedFrameAtTime(time);
          if (frame < persistentStreamContract.start) { halo.setAttribute("opacity", "0"); return; }
          const opacity = fixedPulseOpacity(frame, config.periodFrames, config.minimumOpacity, config.maximumOpacity)
            * streamFadeIn(frame) * resetOpacity(frame);
          halo.setAttribute("opacity", String(opacity));
        });
        auxiliaryReports.push({
          primitive: config.primitive, node_id: config.nodeId, count: 1,
          movement: "opacity-only", animated_attributes: ["opacity"], period_frames: config.periodFrames,
          opacity_range: [config.minimumOpacity, config.maximumOpacity], geometry: { x: bounds.x - 4, y: bounds.y - 4, width: bounds.width + 8, height: bounds.height + 8 },
          below_node: true, source_geometry_mutated: false,
        });
      };
      const addContainerPairPulse = (config) => {
        const reports = [];
        config.containerIds.forEach((containerId) => {
          const container = root.querySelector(`[data-graph-role="container"][data-container-id="${containerId}"]`);
          if (!container) throw new Error(`${selectedSceneContract.name} requires container ${containerId}`);
          const sourceBoundary = container.querySelector("rect");
          if (!sourceBoundary) throw new Error(`${selectedSceneContract.name} container ${containerId} has no boundary`);
          const pulse = sourceBoundary.cloneNode(false);
          removeCloneIdentity(pulse); appendMotionAttributes(pulse, config.primitive, containerId);
          pulse.setAttribute("fill", "none"); pulse.setAttribute("opacity", "0"); pulse.removeAttribute("filter");
          motionLayer.append(pulse);
          effects.push((time) => {
            const frame = renderedFrameAtTime(time);
            if (frame < persistentStreamContract.start) { pulse.setAttribute("opacity", "0"); return; }
            const opacity = fixedPulseOpacity(frame, config.periodFrames, config.minimumOpacity, config.maximumOpacity)
              * streamFadeIn(frame) * resetOpacity(frame);
            pulse.setAttribute("opacity", String(opacity));
          });
          reports.push({
            container_id: containerId, geometry_attributes: ["x", "y", "width", "height", "rx"].reduce((result, name) => ({ ...result, [name]: pulse.getAttribute(name) }), {}),
            stroke_width: pulse.getAttribute("stroke-width"), source_stroke_width: sourceBoundary.getAttribute("stroke-width"), geometry_and_stroke_unchanged: pulse.getAttribute("stroke-width") === sourceBoundary.getAttribute("stroke-width"),
          });
        });
        auxiliaryReports.push({ primitive: config.primitive, count: reports.length, movement: "opacity-only", animated_attributes: ["opacity"], phase_locked: true, period_frames: config.periodFrames, opacity_range: [config.minimumOpacity, config.maximumOpacity], containers: reports, source_geometry_mutated: false });
      };
      const addStationDwellRings = (config) => {
        const mainEntries = drawSchedule.filter((entry) => entry.role === "topic-rail");
        const reports = [];
        mainEntries.forEach((entry) => {
          const edge = edgeForEntry(entry);
          const target = nodes.find((node) => node.dataset.nodeId === edge.dataset.target);
          if (!target) throw new Error(`Transit target ${edge.dataset.target || "unknown"} is unavailable`);
          const bounds = parseGraphBounds(target);
          const ring = document.createElementNS(SVG_NS, "rect");
          appendMotionAttributes(ring, config.primitive, edge.dataset.target || "");
          ring.setAttribute("x", String(bounds.x - 4)); ring.setAttribute("y", String(bounds.y - 4));
          ring.setAttribute("width", String(bounds.width + 8)); ring.setAttribute("height", String(bounds.height + 8)); ring.setAttribute("rx", "10");
          ring.setAttribute("fill", "none"); ring.setAttribute("stroke", edge.getAttribute("stroke") || "#e4475b"); ring.setAttribute("stroke-width", "1.2"); ring.setAttribute("opacity", "0");
          motionLayer.append(ring);
          effects.push((time) => {
            const frame = renderedFrameAtTime(time);
            const elapsed = frame - entry.end;
            const opacity = elapsed >= 0 && elapsed < config.periodFrames
              ? Math.sin(Math.PI * (elapsed + 1) / (config.periodFrames + 1)) * 0.32 * resetOpacity(frame)
              : 0;
            ring.setAttribute("opacity", String(opacity));
          });
          reports.push({ edge_id: edge.dataset.edgeId || "", target_node_id: edge.dataset.target || "", arrival_frame: entry.end, period_frames: config.periodFrames, movement: "opacity-only", geometry_expansion_per_frame: 0, outside_node_border: true, fixed_geometry: { x: bounds.x - 4, y: bounds.y - 4, width: bounds.width + 8, height: bounds.height + 8 } });
        });
        auxiliaryReports.push({ primitive: config.primitive, count: reports.length, rings: reports, source_geometry_mutated: false });
      };
      const addOpsScannerAndTraceReveal = () => {
        const spansById = new Map(nodes.filter((node) => node.dataset.spanId).map((node) => [node.dataset.spanId, node]));
        const schedule = [
          { id: "span-root", start: 24, end: 27 }, { id: "span-api", start: 27, end: 30 },
          { id: "span-checkout", start: 30, end: 33 }, { id: "span-payment", start: 33, end: 36 },
        ];
        if (schedule.some((entry) => !spansById.has(entry.id))) throw new Error("Ops Pulse trace-span source set changed");
        edgeHidingStyle.textContent += '[data-graph-role="node"][data-span-id]:not([data-span-id=""]){visibility:hidden!important}';
        transientTraceSpanSources.push(...schedule.map((entry) => spansById.get(entry.id)));
        const spanBounds = schedule.map((entry) => parseGraphBounds(spansById.get(entry.id)));
        const plot = {
          x: Math.min(...spanBounds.map((bounds) => bounds.x)),
          y: Math.min(...spanBounds.map((bounds) => bounds.y)),
          x2: Math.max(...spanBounds.map((bounds) => bounds.x + bounds.width)),
          y2: Math.max(...spanBounds.map((bounds) => bounds.y + bounds.height)),
        };
        const defs = document.createElementNS(SVG_NS, "defs");
        const scannerClip = document.createElementNS(SVG_NS, "clipPath");
        scannerClip.setAttribute("id", "fireworks-ops-scanner-clip");
        const scannerClipRect = document.createElementNS(SVG_NS, "rect");
        scannerClipRect.setAttribute("x", String(plot.x)); scannerClipRect.setAttribute("y", String(plot.y));
        scannerClipRect.setAttribute("width", String(plot.x2 - plot.x)); scannerClipRect.setAttribute("height", String(plot.y2 - plot.y)); scannerClip.append(scannerClipRect); defs.append(scannerClip);
        motionLayer.append(defs);
        const scanner = document.createElementNS(SVG_NS, "g");
        appendMotionAttributes(scanner, "waterfall-scanner", "trace-waterfall"); scanner.setAttribute("clip-path", "url(#fireworks-ops-scanner-clip)"); scanner.setAttribute("opacity", "0");
        const tail = document.createElementNS(SVG_NS, "rect");
        tail.setAttribute("x", "-12"); tail.setAttribute("y", String(plot.y)); tail.setAttribute("width", "12"); tail.setAttribute("height", String(plot.y2 - plot.y)); tail.setAttribute("fill", "#22d3ee"); tail.setAttribute("fill-opacity", "0.14"); tail.setAttribute("data-motion-component", "scanner-tail");
        const line = document.createElementNS(SVG_NS, "line");
        line.setAttribute("x1", "0"); line.setAttribute("x2", "0"); line.setAttribute("y1", String(plot.y)); line.setAttribute("y2", String(plot.y2)); line.setAttribute("stroke", "#67e8f9"); line.setAttribute("stroke-width", "2"); line.setAttribute("data-motion-component", "scanner-line");
        scanner.append(tail, line); motionLayer.append(scanner);
        effects.push((time) => {
          const frame = renderedFrameAtTime(time);
          if (frame < persistentStreamContract.start) { scanner.setAttribute("opacity", "0"); return; }
          const liveFrame = frame - persistentStreamContract.start;
          const progress = ((liveFrame % 34) + 34) % 34 / 33;
          const x = plot.x + progress * (plot.x2 - plot.x);
          scanner.setAttribute("transform", `translate(${x} 0)`);
          scanner.setAttribute("opacity", String(streamFadeIn(frame) * resetOpacity(frame)));
        });
        waterfallScannerReport = { primitive: "waterfall-scanner", width: 2, tail_width: 12, period_frames: 34, plot_bounds: [plot.x, plot.y, plot.x2, plot.y2], contained_by: "trace-waterfall", below_span_labels: true, movement: "horizontal-within-trace-plot", animated_attributes: ["transform", "opacity"] };

        schedule.forEach((entry, index) => {
          const source = spansById.get(entry.id);
          const bounds = spanBounds[index];
          const clip = document.createElementNS(SVG_NS, "clipPath");
          const clipId = `fireworks-trace-reveal-${index}`; clip.setAttribute("id", clipId);
          const clipRect = document.createElementNS(SVG_NS, "rect");
          clipRect.setAttribute("x", String(bounds.x)); clipRect.setAttribute("y", String(bounds.y)); clipRect.setAttribute("width", "0"); clipRect.setAttribute("height", String(bounds.height)); clip.append(clipRect); defs.append(clip);
          const clone = source.cloneNode(true);
          for (const element of [clone, ...clone.querySelectorAll("*")]) removeCloneIdentity(element);
          appendMotionAttributes(clone, "trace-span-reveal", entry.id); clone.setAttribute("clip-path", `url(#${clipId})`); clone.setAttribute("display", "none"); clone.setAttribute("opacity", "0"); motionLayer.append(clone);
          effects.push((time) => {
            const frame = renderedFrameAtTime(time);
            if (frame < entry.start) { clone.setAttribute("display", "none"); clone.setAttribute("opacity", "0"); clipRect.setAttribute("width", "0"); return; }
            const progress = frame >= entry.end ? 1 : clamped((frame - entry.start) / (entry.end - entry.start));
            clone.setAttribute("display", "inline"); clone.setAttribute("opacity", String(resetOpacity(frame))); clipRect.setAttribute("width", String(bounds.width * progress));
          });
          traceSpanRevealReports.push({ span_id: entry.id, parent_span: source.dataset.parentSpan || null, rendered_frames: [entry.start, entry.end], source_geometry: bounds, reveal: "left-to-right-clip", source_transiently_hidden: true, source_geometry_mutated: false, clone_geometry_immutable: true });
        });
      };
      const addSpecializedAuxiliaries = () => {
        if (selectedSceneContract.styleId === 12) {
          addOpsScannerAndTraceReveal();
        }
        const config = persistentStreamContract.auxiliary;
        if (!config) return;
        if (config.kind === "node-halo" || config.kind === "ops-pulse") addFixedNodeHalo(config);
        else if (config.kind === "container-pair-pulse") addContainerPairPulse(config);
        else if (config.kind === "station-dwell-rings") addStationDwellRings(config);
      };

      const addSettledOwnerDecorations = () => {
        if (![11, 12].includes(selectedSceneContract.styleId)) return;
        const casingLayer = document.createElementNS(SVG_NS, "g");
        casingLayer.setAttribute("data-graph-role", "decoration");
        casingLayer.setAttribute("data-motion-layer", "settled-route-casings");
        casingLayer.setAttribute("aria-hidden", "true");
        casingLayer.setAttribute("pointer-events", "none");
        settledOwnerDirectionLayer = document.createElementNS(SVG_NS, "g");
        settledOwnerDirectionLayer.setAttribute("data-graph-role", "decoration");
        settledOwnerDirectionLayer.setAttribute("data-motion-layer", "settled-route-directions");
        settledOwnerDirectionLayer.setAttribute("aria-hidden", "true");
        settledOwnerDirectionLayer.setAttribute("pointer-events", "none");
        const ownerRole = selectedSceneContract.styleId === 11 ? "topic-rail" : "critical-request";
        const expectedSourceCount = selectedSceneContract.styleId === 11 ? 8 : 9;
        const expectedPerOwner = selectedSceneContract.styleId === 11 ? 2 : 3;
        const mainEntries = drawSchedule.filter((entry) => entry.role === ownerRole);
        const expectedOwners = new Set(mainEntries.map((entry) => edgeForEntry(entry).dataset.edgeId || ""));
        const decorationsByOwner = new Map();
        routeOwnerDecorations.forEach((decoration) => {
          const owner = decoration.dataset.owner || "";
          const matches = decorationsByOwner.get(owner) || [];
          matches.push(decoration);
          decorationsByOwner.set(owner, matches);
        });
        if (
          routeOwnerDecorations.length !== expectedSourceCount
          || decorationsByOwner.size !== expectedOwners.size
          || [...decorationsByOwner.keys()].some((owner) => !expectedOwners.has(owner))
          || [...expectedOwners].some((owner) => (
            decorationsByOwner.get(owner) || []
          ).length !== expectedPerOwner)
        ) {
          throw new Error(`${selectedSceneContract.name} route-owned decoration source set changed`);
        }
        mainEntries.forEach((entry) => {
          const edge = edgeForEntry(entry);
          const owner = edge.dataset.edgeId || "";
          const sources = decorationsByOwner.get(owner) || [];
          const clones = sources.map((source) => {
            const clone = source.cloneNode(true);
            for (const element of [clone, ...clone.querySelectorAll("*")]) removeCloneIdentity(element);
            appendMotionAttributes(clone, "settled-owner-decoration", owner);
            clone.setAttribute("display", "none");
            clone.setAttribute("opacity", "0");
            const sourceOpacity = source.hasAttribute("opacity")
              ? Number(source.getAttribute("opacity"))
              : 1;
            if (!Number.isFinite(sourceOpacity) || sourceOpacity < 0 || sourceOpacity > 1) {
              throw new Error(`${selectedSceneContract.name} owner decoration ${source.id || owner} has invalid opacity`);
            }
            const cloneLayer = source.id.endsWith("-rail-casing")
              || source.id.endsWith("-critical-glow")
              ? casingLayer
              : settledOwnerDirectionLayer;
            cloneLayer.append(clone);
            settledOwnerDecorationClones.push(clone);
            return { clone, source, sourceOpacity };
          });
          effects.push((time) => {
            const frame = renderedFrameAtTime(time);
            clones.forEach(({ clone, sourceOpacity }) => {
              if (frame < entry.end) {
                clone.setAttribute("display", "none");
                clone.setAttribute("opacity", "0");
                return;
              }
              clone.setAttribute("display", "inline");
              clone.setAttribute("opacity", String(sourceOpacity * resetOpacity(frame)));
            });
          });
          settledOwnerDecorationReports.push({
            edge_id: owner,
            settle_frame: entry.end,
            source_ids: sources.map((source) => source.id),
            clone_count: clones.length,
            hidden_before_settle: true,
            source_geometry_mutated: false,
          });
        });
        motionLayer.append(casingLayer);
      };

      function addRouteLabel(label, edge, entry) {
        const labelClone = label.cloneNode(true);
        for (const element of [labelClone, ...labelClone.querySelectorAll("*")]) {
          for (const attribute of Array.from(element.attributes)) {
            if (attribute.name === "id" || attribute.name.startsWith("data-")) {
              element.removeAttribute(attribute.name);
            }
          }
        }
        labelClone.setAttribute("data-graph-role", "decoration");
        labelClone.setAttribute("data-motion-primitive", "route-label-arrival");
        labelClone.setAttribute("data-owner", edge.dataset.edgeId || "");
        labelClone.setAttribute("aria-hidden", "true");
        labelClone.setAttribute("pointer-events", "none");
        labelClone.setAttribute("display", "none");
        labelClone.setAttribute("opacity", "0");
        labelLayer.append(labelClone);
        effects.push((time) => {
          const frame = renderedFrameAtTime(time);
          if (frame < entry.end) {
            labelClone.setAttribute("display", "none");
            labelClone.setAttribute("opacity", "0");
            return;
          }
          labelClone.setAttribute("display", "inline");
          labelClone.setAttribute("opacity", String(resetOpacity(frame)));
        });
      }

      function addTerminalPromptCursor(contract) {
        const terminalNodes = nodes.filter((node) => node.dataset.nodeId === contract.nodeId);
        if (terminalNodes.length !== 1) {
          throw new Error(`${selectedSceneContract.name} requires exactly one terminal node`);
        }
        const sourceTexts = Array.from(terminalNodes[0].querySelectorAll("text"))
          .filter((text) => (text.textContent || "").trim() === contract.sourceText);
        if (sourceTexts.length !== 1) {
          throw new Error(`${selectedSceneContract.name} requires exactly one terminal prompt underscore`);
        }
        const sourceText = sourceTexts[0];
        const bounds = sourceText.getBBox();
        if (
          !Number.isFinite(bounds.x)
          || !Number.isFinite(bounds.y)
          || !Number.isFinite(bounds.width)
          || !Number.isFinite(bounds.height)
          || bounds.width <= 0
          || bounds.height < contract.height
        ) {
          throw new Error(`${selectedSceneContract.name} terminal prompt underscore has invalid bounds`);
        }

        const signatureLayer = document.createElementNS(SVG_NS, "g");
        signatureLayer.setAttribute("data-graph-role", "decoration");
        signatureLayer.setAttribute("data-motion-layer", "terminal-signature");
        signatureLayer.setAttribute("aria-hidden", "true");
        signatureLayer.setAttribute("pointer-events", "none");
        const cursor = document.createElementNS(SVG_NS, "rect");
        cursor.setAttribute("data-graph-role", "decoration");
        cursor.setAttribute("data-motion-primitive", contract.primitive);
        cursor.setAttribute("data-owner", contract.nodeId);
        cursor.setAttribute("x", String(bounds.x));
        cursor.setAttribute("y", String(bounds.y + bounds.height - contract.height));
        cursor.setAttribute("width", String(bounds.width));
        cursor.setAttribute("height", String(contract.height));
        cursor.setAttribute("fill", contract.fill);
        cursor.setAttribute("opacity", "0");
        cursor.setAttribute("aria-hidden", "true");
        cursor.setAttribute("pointer-events", "none");
        signatureLayer.append(cursor);
        root.append(signatureLayer);

        const markerFree = !cursor.hasAttribute("marker-start")
          && !cursor.hasAttribute("marker-mid")
          && !cursor.hasAttribute("marker-end");
        const filterFree = !cursor.hasAttribute("filter");
        if (!markerFree || !filterFree) {
          throw new Error("Terminal prompt cursor must be marker-free and filter-free");
        }

        effects.push((time) => {
          const frame = renderedFrameAtTime(time);
          let opacity = 0;
          if (frame >= resetRange[0]) {
            opacity = contract.brightOpacity * resetOpacity(frame);
          } else if (frame >= contract.start && frame <= fullOpacityEnd) {
            const cadenceFrame = Math.floor(frame - contract.start + 1e-9) % contract.periodFrames;
            opacity = cadenceFrame < contract.brightFrames ? contract.brightOpacity : 0;
          }
          cursor.setAttribute("opacity", String(opacity));
        });
        terminalPromptCursorReport = {
          primitive: contract.primitive,
          count: 1,
          node_id: contract.nodeId,
          source_text: contract.sourceText,
          source_text_hidden: false,
          source_text_mutated: false,
          geometry: "2.2px-high rectangle derived from underscore getBBox",
          source_text_bounds: {
            x: bounds.x,
            y: bounds.y,
            width: bounds.width,
            height: bounds.height,
          },
          rectangle: {
            x: bounds.x,
            y: bounds.y + bounds.height - contract.height,
            width: bounds.width,
            height: contract.height,
          },
          fill: contract.fill,
          movement: "opacity-only",
          animated_attributes: ["opacity"],
          settled_after: {
            role: "tool-call",
            order: 0,
            frame: contract.start,
          },
          cadence_frames: [contract.start, fullOpacityEnd],
          period_frames: contract.periodFrames,
          bright_frames_per_period: contract.brightFrames,
          absent_frames_per_period: contract.absentFrames,
          bright_opacity: contract.brightOpacity,
          reset_range: resetRange,
          reset_behavior: "bright opacity multiplied by shared reset opacity",
          marker_free: markerFree,
          filter_free: filterFree,
        };
      }

      addSettledOwnerDecorations();
      drawSchedule.forEach((entry) => {
        addDrawOnRoute(edgeForEntry(entry), entry);
      });
      if (settledOwnerDirectionLayer) motionLayer.append(settledOwnerDirectionLayer);
      drawSchedule.forEach((entry) => {
        if (selectedSceneContract.streamMode === "blueprint-registration-bead") {
          addBlueprintDistributionWave(edgeForEntry(entry), entry);
        } else if (selectedSceneContract.streamMode === "notion-memory-card-handoff") {
          addNotionMemoryCardHandoff(edgeForEntry(entry), entry);
        } else if (selectedSceneContract.streamMode === "specialized-live-signature") {
          addSpecializedLiveSignature(edgeForEntry(entry), entry);
        } else {
          addPersistentDataFlow(edgeForEntry(entry), entry);
        }
      });
      if (selectedSceneContract.streamMode === "specialized-live-signature") {
        addSpecializedAuxiliaries();
      }
      const greatestCommonDivisor = (left, right) => {
        let a = Math.abs(left);
        let b = Math.abs(right);
        while (b) {
          [a, b] = [b, a % b];
        }
        return a;
      };
      const phaseStep = Math.abs(persistentStreamContract.dashOffsetPerFrame);
      if (greatestCommonDivisor(persistentStreamContract.dashPeriod, phaseStep) !== 1) {
        throw new Error("Persistent stream dash period and travel step must be coprime");
      }
      const actualStreamPhases = persistentStreamReports.map((report) => report.initial_phase);
      if (
        actualStreamPhases.length !== expectedStreamPhases.length
        || actualStreamPhases.some((phase, index) => phase !== expectedStreamPhases[index])
      ) {
        throw new Error(
          `${selectedSceneContract.name} stream phases changed: expected ${expectedStreamPhases.join(",")}, got ${actualStreamPhases.join(",")}`,
        );
      }
      if (selectedSceneContract.streamMode === "blueprint-registration-bead") {
        const reportsForRole = (role) => persistentStreamReports.filter((report) => report.role === role);
        const fanout = reportsForRole("fanout");
        const dataWrite = reportsForRole("data-write");
        if (
          fanout.length !== 3
          || fanout.some((report) => report.initial_phase !== 21)
          || dataWrite.length !== 3
          || dataWrite.some((report) => report.initial_phase !== 28)
        ) {
          throw new Error("Blueprint fanout/data-write stage locks changed");
        }
        const dataWriteBeads = registrationBeadReports.filter((report) => report.role === "data-write");
        const pathLengths = dataWriteBeads.map((report) => report.path_length);
        const initialY = dataWriteBeads.map((report) => report.initial_point.y);
        if (
          dataWriteBeads.length !== 3
          || !pathLengths.every((length) => Math.abs(length - pathLengths[0]) < 0.001)
          || !initialY.every((value) => Math.abs(value - initialY[0]) < 0.001)
        ) {
          throw new Error("Blueprint data-write registration beads lost path-length/Y synchrony");
        }
      } else if (selectedSceneContract.streamMode === "notion-memory-card-handoff") {
        const progressVector = notionMemoryCardReports.map((report) => report.initial_normalized_progress);
        const rotationVector = notionMemoryCardReports.map((report) => report.tangent_rotation);
        const colorVector = notionMemoryCardReports.map((report) => report.semantic_color);
        if (
          progressVector.length !== persistentStreamContract.initialNormalizedProgress.length
          || progressVector.some((progress, index) => (
            progress !== persistentStreamContract.initialNormalizedProgress[index]
          ))
          || rotationVector.some((rotation, index) => (
            rotation !== persistentStreamContract.directionSentinels[index].tangentRotation
          ))
          || colorVector.some((color, index) => color !== persistentStreamContract.semanticColors[index])
        ) {
          throw new Error("Notion memory-card progress, tangent, or semantic-color vector changed");
        }
      }
      drawSchedule.forEach((entry) => {
        const edge = edgeForEntry(entry);
        const label = routeLabels.find((candidate) => candidate.dataset.owner === edge.dataset.edgeId);
        if (label) {
          addRouteLabel(label, edge, entry);
        }
      });
      motionLayer.append(labelLayer);
      if (signatureContract) {
        addTerminalPromptCursor(signatureContract);
      }

      const sampledDirections = (edge) => {
        const length = edge.getTotalLength();
        const sampleCount = Math.max(64, Math.ceil(length / 3));
        const directions = [];
        let previous = edge.getPointAtLength(0);
        for (let index = 1; index <= sampleCount; index += 1) {
          const point = edge.getPointAtLength(length * index / sampleCount);
          const deltaX = point.x - previous.x;
          const deltaY = point.y - previous.y;
          previous = point;
          if (Math.abs(deltaX) < 0.001 && Math.abs(deltaY) < 0.001) {
            continue;
          }
          const direction = Math.abs(deltaX) >= Math.abs(deltaY)
            ? (deltaX > 0 ? "right" : "left")
            : (deltaY > 0 ? "down" : "up");
          if (directions[directions.length - 1] !== direction) {
            directions.push(direction);
          }
        }
        return directions;
      };
      const containsOrderedDirections = (actual, expected) => {
        let expectedIndex = 0;
        actual.forEach((direction) => {
          if (direction === expected[expectedIndex]) {
            expectedIndex += 1;
          }
        });
        return expectedIndex === expected.length;
      };
      const directionSentinels = persistentStreamContract.directionSentinels.map((sentinel) => {
        const actual = sampledDirections(edgeForKey(sentinel.role, sentinel.order, sentinel.stage));
        const passed = selectedSceneContract.streamMode === "notion-memory-card-handoff"
          ? actual.length === sentinel.expected.length
            && actual.every((direction, index) => direction === sentinel.expected[index])
          : containsOrderedDirections(actual, sentinel.expected);
        const beadTravelPassed = selectedSceneContract.streamMode !== "blueprint-registration-bead"
          || persistentStreamContract.beadAdvancePerFrame > 0;
        const cardTravelPassed = selectedSceneContract.streamMode !== "notion-memory-card-handoff"
          || (
            persistentStreamContract.cardAdvancePerFrame > 0
            && notionMemoryCardReports.some((report) => (
              report.role === sentinel.role
              && report.motion_order === sentinel.order
              && report.tangent_rotation === sentinel.tangentRotation
            ))
          );
        if (
          !passed
          || persistentStreamContract.dashOffsetPerFrame >= 0
          || !beadTravelPassed
          || !cardTravelPassed
        ) {
          throw new Error(
            `Stream direction sentinel failed for ${routeKey(sentinel.role, sentinel.order, sentinel.stage)}: `
            + `expected ${sentinel.expected.join(" -> ")}, got ${actual.join(" -> ")}`,
          );
        }
        const report = {
          role: sentinel.role,
          expected: sentinel.expected,
          actual,
          dash_offset_per_rendered_frame: persistentStreamContract.dashOffsetPerFrame,
          ...(selectedSceneContract.streamMode === "blueprint-registration-bead" ? {
            bead_advance_per_rendered_frame: persistentStreamContract.beadAdvancePerFrame,
          } : selectedSceneContract.streamMode === "notion-memory-card-handoff" ? {
            card_advance_per_rendered_frame: persistentStreamContract.cardAdvancePerFrame,
            tangent_rotation: sentinel.tangentRotation,
          } : selectedSceneContract.streamMode === "specialized-live-signature" ? {
            signature_advance_per_rendered_frame: persistentStreamContract.advancePerFrame,
          } : {}),
          passed,
        };
        if (!selectedSceneContract.legacyStyleOneReport) {
          report.order = sentinel.order;
          if (sentinel.stage !== undefined) report.stage = sentinel.stage;
          report.schedule_key = routeKey(sentinel.role, sentinel.order, sentinel.stage);
        }
        return report;
      });
      if (!effects.length) {
        throw new Error(`${selectedSceneContract.name} did not create connector draw-on effects`);
      }

      window.__fireworksSetMotionTime = (time) => {
        effects.forEach((effect) => effect(time));
        assertStaticDomUnchanged();
      };
      window.__fireworksSetMotionFrame = (frameIndex) => {
        if (!Number.isInteger(frameIndex) || frameIndex < 0 || frameIndex > renderedFrameMax) {
          throw new Error(`Motion frame index ${frameIndex} is outside 0-${renderedFrameMax}`);
        }
        window.__fireworksSetMotionTime((frameIndex + 0.5) / selectedFps);
      };
      window.__fireworksSetMotionFrame(0);
      const isRenderedVisible = (element) => {
        const style = window.getComputedStyle(element);
        return style.display !== "none"
          && style.visibility !== "hidden"
          && Number(style.opacity || "1") > 0;
      };
      const ownerDecorationSourceOpeningVisibleCount = [11, 12].includes(selectedSceneContract.styleId)
        ? routeOwnerDecorations.filter(isRenderedVisible).length
        : 0;
      const ownerDecorationCloneOpeningVisibleCount = [11, 12].includes(selectedSceneContract.styleId)
        ? settledOwnerDecorationClones.filter(isRenderedVisible).length
        : 0;
      const traceSpanSourceOpeningVisibleCount = selectedSceneContract.styleId === 12
        ? transientTraceSpanSources.filter(isRenderedVisible).length
        : 0;
      const nonTraceSpanNodeOpeningHiddenCount = selectedSceneContract.styleId === 12
        ? nodes.filter((node) => !node.dataset.spanId).filter((node) => !isRenderedVisible(node)).length
        : 0;
      if (ownerDecorationSourceOpeningVisibleCount || ownerDecorationCloneOpeningVisibleCount) {
        throw new Error(`${selectedSceneContract.name} route-owner decoration leaked into the empty opening frame`);
      }
      if (traceSpanSourceOpeningVisibleCount) {
        throw new Error("Ops Pulse source trace spans leaked into the empty opening frame");
      }
      if (nonTraceSpanNodeOpeningHiddenCount) {
        throw new Error("Ops Pulse non-trace nodes were hidden in the empty opening frame");
      }

      return {
        effects: effects.length,
        edges: edges.length,
        nodes: nodes.length,
        fps: selectedFps,
        frame_count: selectedFrameCount,
        scene_report: {
          grammar_version: "3.4",
          preset: selectedPreset,
          primitive: "connector-draw-on-with-persistent-data-flow",
          empty_opening_frame: emptyOpeningFrame,
          connectors_visible_at_opening: false,
          nodes_visible_every_frame: true,
          topology_draw_on: true,
          settled_topology_dynamic: true,
          static_dom_guard: true,
          source_edges_hidden_by_transient_css: true,
          source_edges: edges.length,
          source_route_labels: routeLabels.length,
          draw_clones: drawReports.length,
          settled_marker_clones: drawReports.length,
          stream_count: persistentStreamReports.length,
          packet_head_count: persistentPacketHeadReports.length,
          ...(selectedSceneContract.streamMode === "blueprint-registration-bead" ? {
            registration_bead_count: registrationBeadReports.length,
          } : selectedSceneContract.streamMode === "notion-memory-card-handoff" ? {
            notion_memory_card_count: notionMemoryCardReports.length,
          } : selectedSceneContract.streamMode === "specialized-live-signature" ? {
            specialized_signature_count: specializedSignatureReports.length,
          } : {}),
          route_label_clones: routeLabels.length,
          node_motion: 0,
          text_motion: 0,
          text_geometry_motion: 0,
          route_label_opacity_states: routeLabels.length,
          halo_count: selectedSceneContract.streamMode === "specialized-live-signature"
            ? auxiliaryReports.filter((report) => String(report.primitive || "").includes("halo")).reduce((total, report) => total + Number(report.count || 0), 0)
            : 0,
          ripple_count: 0,
          maximum_concurrent_draws: maximumConcurrentDraws,
          draw_schedule: drawReports,
          persistent_streams: persistentStreamReports,
          persistent_packet_heads: persistentPacketHeadReports,
          ...(selectedSceneContract.streamMode === "blueprint-registration-bead" ? {
            blueprint_registration_beads: registrationBeadReports,
          } : selectedSceneContract.streamMode === "notion-memory-card-handoff" ? {
            notion_memory_cards: notionMemoryCardReports,
          } : selectedSceneContract.streamMode === "specialized-live-signature" ? {
            specialized_signatures: specializedSignatureReports,
            auxiliary_primitives: auxiliaryReports,
            trace_span_reveals: traceSpanRevealReports,
            waterfall_scanner: waterfallScannerReport,
          } : {}),
          direction_sentinels: directionSentinels,
          ...(selectedSceneContract.styleId === 2 ? {
            schedule_key: "(data-motion-role, data-motion-order)",
            cursor_count: terminalPromptCursorReport ? 1 : 0,
            terminal_prompt_cursor: terminalPromptCursorReport,
            extra_scene_primitives: terminalPromptCursorReport ? ["terminal-prompt-cursor"] : [],
            node_glow_pulse_count: 0,
            terminal_text_typing_count: 0,
            scan_line_count: 0,
            camera_motion_count: 0,
            animated_background_count: 0,
          } : selectedSceneContract.styleId === 3 ? {
            schedule_key: "(data-motion-role, data-motion-order)",
            extra_scene_primitives: [],
            node_glow_pulse_count: 0,
            terminal_text_typing_count: 0,
            terminal_cursor_count: 0,
            scan_line_count: 0,
            camera_motion_count: 0,
            animated_background_count: 0,
            blur_count: 0,
            shadow_count: 0,
            stage_locks: {
              fanout: { orders: [0, 1, 2], phase: 21 },
              data_write: { orders: [0, 1, 2], phase: 28, equal_length_paths: true },
            },
          } : selectedSceneContract.styleId === 4 ? {
            schedule_key: "(data-motion-role, data-motion-order)",
            extra_scene_primitives: ["notion-memory-card"],
            node_glow_pulse_count: 0,
            terminal_text_typing_count: 0,
            terminal_cursor_count: 0,
            circular_bead_count: 0,
            scan_line_count: 0,
            camera_motion_count: 0,
            animated_background_count: 0,
            blur_count: 0,
            shadow_count: 0,
            semantic_color_vector: persistentStreamContract.semanticColors,
            initial_progress_vector: persistentStreamContract.initialNormalizedProgress,
          } : selectedSceneContract.styleId >= 5 ? {
            schedule_key: selectedSceneContract.stageAwareRouteKey
              ? "(data-motion-role, data-motion-stage, data-motion-order)"
              : "(data-motion-role, data-motion-order)",
            scene_identity: selectedSceneContract.name,
            signature_kind: persistentStreamContract.signatureKind,
            extra_scene_primitives: [...new Set([
              ...specializedSignatureReports.map((report) => report.primitive),
              ...auxiliaryReports.map((report) => report.primitive),
              ...traceSpanRevealReports.map(() => "trace-span-reveal"),
              ...(waterfallScannerReport ? ["waterfall-scanner"] : []),
            ])],
            maximum_live_rail_width: Math.max(...persistentStreamReports.map((report) => report.stroke_width)),
            live_rail_width_ceiling: persistentStreamContract.bodyWidthMaximum,
            live_rail_width_ceiling_passed: persistentStreamReports.every((report) => report.stroke_width <= persistentStreamContract.bodyWidthMaximum),
            dynamic_not_thicker_than_source: persistentStreamReports.every((report) => report.dynamic_not_thicker_than_source),
            signature_travel_at_100_percent: persistentStreamContract.advancePerFrame,
            signature_travel_at_50_percent: persistentStreamContract.advancePerFrame / 2,
            source_geometry_mutation_count: 0,
            source_text_mutation_count: 0,
            source_marker_mutation_count: 0,
            node_motion_count: 0,
            text_motion_count: 0,
            camera_motion_count: 0,
            animated_background_count: 0,
            ...(selectedSceneContract.styleId === 8 ? {
              gem_filter_boundary: {
                filtered_elements_per_tracer: specializedSignatureReports.map((report) => report.filtered_element_count),
                only_gem_halo_filtered: specializedSignatureReports.every((report) => report.filtered_element_count === 1),
              },
            } : selectedSceneContract.styleId === 10 ? {
              pair_phase_locks: {
                global_route: persistentStreamReports.filter((report) => report.role === "global-route").map((report) => report.initial_phase),
                regional_write: persistentStreamReports.filter((report) => report.role === "regional-write").map((report) => report.initial_phase),
              },
              replication_direction: "left-to-right-only",
            } : selectedSceneContract.styleId === 11 ? {
              station_dwell_ring_count: auxiliaryReports.filter((report) => report.primitive === "station-dwell-ring").reduce((total, report) => total + Number(report.count || 0), 0),
              accepted_station_geometry_mutated: false,
              source_owner_decoration_count: routeOwnerDecorations.length,
              settled_owner_decoration_clone_count: settledOwnerDecorationClones.length,
              owner_decoration_source_opening_visible_count: ownerDecorationSourceOpeningVisibleCount,
              owner_decoration_clone_opening_visible_count: ownerDecorationCloneOpeningVisibleCount,
              owner_decorations: settledOwnerDecorationReports,
            } : selectedSceneContract.styleId === 12 ? {
              trace_span_reveal_count: traceSpanRevealReports.length,
              trace_span_source_count: transientTraceSpanSources.length,
              trace_span_source_opening_visible_count: traceSpanSourceOpeningVisibleCount,
              non_trace_span_node_opening_hidden_count: nonTraceSpanNodeOpeningHiddenCount,
              source_owner_decoration_count: routeOwnerDecorations.length,
              settled_owner_decoration_clone_count: settledOwnerDecorationClones.length,
              owner_decoration_source_opening_visible_count: ownerDecorationSourceOpeningVisibleCount,
              owner_decoration_clone_opening_visible_count: ownerDecorationCloneOpeningVisibleCount,
              owner_decorations: settledOwnerDecorationReports,
              status_card_blink_count: 0,
              metric_blink_count: 0,
              scanner_contained: Boolean(waterfallScannerReport),
              scanner_below_span_labels: Boolean(waterfallScannerReport?.below_span_labels),
            } : {}),
          } : {}),
          reset_range: resetRange,
          reset_opacity_samples: resetOpacitySamples,
          draw_contract: {
            source_geometry: "exact immutable edge clone",
            marker_during_draw: false,
            marker_after_arrival: true,
            easing: "linear",
          },
          persistent_stream_contract: {
            primitive: persistentStreamContract.bodyPrimitive,
            ...(selectedSceneContract.streamMode === "blueprint-registration-bead" ? {
              registration_bead_primitive: persistentStreamContract.beadPrimitive,
            } : selectedSceneContract.streamMode === "notion-memory-card-handoff" ? {
              memory_card_primitive: persistentStreamContract.cardPrimitive,
            } : selectedSceneContract.streamMode === "specialized-live-signature" ? {
              signature_primitive: persistentStreamContract.signaturePrimitive,
            } : {
              packet_head_primitive: persistentStreamContract.headPrimitive,
            }),
            stream_count: persistentStreamReports.length,
            ...(selectedSceneContract.streamMode === "blueprint-registration-bead" ? {
              registration_bead_count: registrationBeadReports.length,
            } : selectedSceneContract.streamMode === "notion-memory-card-handoff" ? {
              memory_card_count: notionMemoryCardReports.length,
            } : selectedSceneContract.streamMode === "specialized-live-signature" ? {
              signature_count: specializedSignatureReports.length,
            } : {
              packet_head_count: persistentPacketHeadReports.length,
            }),
            rendered_frames: [persistentStreamContract.start, renderedFrameMax],
            fade_in_frames: [persistentStreamContract.start, persistentStreamContract.start + 2],
            fade_in_factors: persistentStreamContract.fadeInFactors,
            full_opacity_frames: [persistentStreamContract.start + 2, fullOpacityEnd],
            body: {
              primitive: persistentStreamContract.bodyPrimitive,
              stroke_width: persistentStreamContract.bodyWidthDescription,
              ...(selectedSceneContract.styleId === 1 ? {
                resolved_style_1_source_stroke_width: persistentStreamContract.sourceStrokeWidth,
                resolved_style_1_stroke_width: persistentStreamContract.resolvedStrokeWidth,
              } : selectedSceneContract.styleId === 2 ? {
                resolved_style_2_source_stroke_width: persistentStreamContract.sourceStrokeWidth,
                resolved_style_2_stroke_width: persistentStreamContract.resolvedStrokeWidth,
                source_colors_in_schedule_order: persistentStreamContract.expectedSourceColors,
                semantic_colors: {
                  control: "#a855f7",
                  tool_read: "#38bdf8",
                  index_write: "#22c55e",
                  grounding_data: "#fb7185",
                  answer: "#f97316",
                },
              } : selectedSceneContract.styleId === 3 ? {
                resolved_style_3_source_stroke_width: persistentStreamContract.sourceStrokeWidth,
                resolved_style_3_stroke_width: persistentStreamContract.resolvedStrokeWidth,
                resolved_style_3_stroke_width_at_50_percent:
                  persistentStreamContract.resolvedStrokeWidth / 2,
                source_colors_in_schedule_order: persistentStreamContract.expectedSourceColors,
              } : selectedSceneContract.styleId === 4 ? {
                resolved_style_4_source_stroke_width: persistentStreamContract.sourceStrokeWidth,
                resolved_style_4_stroke_width: persistentStreamContract.resolvedStrokeWidth,
                resolved_style_4_stroke_width_at_50_percent:
                  persistentStreamContract.resolvedStrokeWidth / 2,
                source_colors_in_schedule_order: persistentStreamContract.expectedSourceColors,
                semantic_colors_in_schedule_order: persistentStreamContract.semanticColors,
              } : {
                maximum_live_width: persistentStreamContract.bodyWidthMaximum,
                resolved_widths_in_schedule_order: persistentStreamReports.map((report) => report.stroke_width),
                source_widths_in_schedule_order: persistentStreamReports.map((report) => report.source_stroke_width),
                dynamic_not_thicker_than_source: persistentStreamReports.every((report) => report.dynamic_not_thicker_than_source),
              }),
              color: persistentStreamContract.bodyColor || "inherit-source-stroke",
              opacity: persistentStreamContract.bodyOpacity,
              dash_pattern: persistentStreamContract.bodyDashPattern,
              linecap: "round",
              linejoin: "round",
              marker_free: true,
              filter_free: true,
            },
            ...(selectedSceneContract.streamMode === "blueprint-registration-bead" ? {
              registration_bead: {
                primitive: persistentStreamContract.beadPrimitive,
                shape: "circle",
                radius: persistentStreamContract.beadRadius,
                diameter_at_960px: persistentStreamContract.beadRadius * 2,
                diameter_at_50_percent: persistentStreamContract.beadRadius,
                fill: persistentStreamContract.beadFill,
                stroke: "inherit-source-stroke",
                stroke_width: persistentStreamContract.beadStrokeWidth,
                opacity: persistentStreamContract.beadOpacity,
                initial_path_distance: "stage-locked-phase",
                path_advance_per_rendered_frame: persistentStreamContract.beadAdvancePerFrame,
                direction: "source-to-target",
                wrap: "target-end-to-source-start",
                animated_attributes: ["cx", "cy", "opacity"],
                marker_free: true,
                filter_free: true,
              },
              bead_advance_per_rendered_frame: persistentStreamContract.beadAdvancePerFrame,
              stage_locks: {
                fanout: { orders: [0, 1, 2], phase: 21 },
                data_write: { orders: [0, 1, 2], phase: 28, equal_length_paths: true },
              },
            } : selectedSceneContract.streamMode === "notion-memory-card-handoff" ? {
              memory_card: {
                primitive: persistentStreamContract.cardPrimitive,
                shape: "group",
                outer_rect: {
                  ...persistentStreamContract.outerRect,
                  fill: persistentStreamContract.cardFill,
                  stroke: "semantic-memory-destination",
                  stroke_width: persistentStreamContract.cardStrokeWidth,
                },
                ink_lines: persistentStreamContract.inkLines,
                ink_stroke: "semantic-memory-destination",
                ink_stroke_width: persistentStreamContract.inkStrokeWidth,
                ink_linecap: persistentStreamContract.inkLinecap,
                ink_shape_rendering: persistentStreamContract.inkShapeRendering,
                opacity: persistentStreamContract.cardOpacity,
                semantic_colors_in_schedule_order: persistentStreamContract.semanticColors,
                initial_normalized_progress_by_stage:
                  persistentStreamContract.initialNormalizedProgress,
                initial_path_distance: "8 + progress * (pathLength - 16)",
                endpoint_clearance: persistentStreamContract.endpointClearance,
                path_advance_per_rendered_frame: persistentStreamContract.cardAdvancePerFrame,
                tangent_rotations_in_schedule_order:
                  persistentStreamContract.directionSentinels.map((sentinel) => sentinel.tangentRotation),
                direction: "source-to-target",
                wrap: "target-clearance-to-source-clearance",
                animated_attributes: ["transform", "opacity"],
                marker_free: true,
                filter_free: true,
                shadow_free: true,
                appended_below_labels_and_nodes: true,
              },
              card_advance_per_rendered_frame: persistentStreamContract.cardAdvancePerFrame,
            } : selectedSceneContract.streamMode === "specialized-live-signature" ? {
              signature: {
                primitive: persistentStreamContract.signaturePrimitive,
                signature_kind: persistentStreamContract.signatureKind,
                endpoint_clearance: persistentStreamContract.endpointClearance,
                path_advance_per_rendered_frame: persistentStreamContract.advancePerFrame,
                direction: "source-to-target",
                wrap: "target-clearance-to-source-clearance",
                tangent_aware_rotation: true,
                animated_attributes: ["transform", "opacity"],
                appended_below_labels_and_nodes: true,
                geometry_by_route: specializedSignatureReports.map((report) => ({
                  schedule_key: report.schedule_key,
                  primitive: report.primitive,
                  geometry: report.geometry,
                })),
              },
              signature_advance_per_rendered_frame: persistentStreamContract.advancePerFrame,
            } : {
              packet_head: {
                primitive: persistentStreamContract.headPrimitive,
                stroke_width: persistentStreamContract.headStrokeWidth,
                color: persistentStreamContract.headColor,
                opacity: persistentStreamContract.headOpacity,
                dash_pattern: persistentStreamContract.headDashPattern,
                dash_offset_from_body: persistentStreamContract.headLeadOffset,
                linecap: "round",
                linejoin: "round",
                marker_free: true,
                filter_free: true,
                appended_immediately_after_body: true,
              },
            }),
            dash_period: persistentStreamContract.dashPeriod,
            dash_offset_per_rendered_frame: persistentStreamContract.dashOffsetPerFrame,
            travel_user_units_per_rendered_frame: phaseStep,
            travel_pixels_per_frame_at_960px: selectedSceneContract.streamMode === "specialized-live-signature" ? persistentStreamContract.advancePerFrame : 6,
            travel_pixels_per_second_at_960px_20fps: (selectedSceneContract.streamMode === "specialized-live-signature" ? persistentStreamContract.advancePerFrame : 6) * 20,
            travel_pixels_per_frame_at_50_percent: (selectedSceneContract.streamMode === "specialized-live-signature" ? persistentStreamContract.advancePerFrame : 6) / 2,
            travel_pixels_per_second_at_50_percent_20fps: (selectedSceneContract.streamMode === "specialized-live-signature" ? persistentStreamContract.advancePerFrame : 6) * 10,
            phase_policy: persistentStreamContract.phasePolicy,
            expected_initial_phases: expectedStreamPhases,
            period_step_coprime: true,
            stream_interval_frame_count: renderedFrameMax - persistentStreamContract.start + 1,
            phase_repeat_within_stream_interval:
              renderedFrameMax - persistentStreamContract.start + 1 > persistentStreamContract.dashPeriod,
            direction: "source-to-target",
            travel_easing: "linear",
            minimum_review_scale: "50%",
            reset_range: resetRange,
            reset_opacity_samples: resetOpacitySamples,
            reset_behavior: persistentStreamContract.resetBehavior || "live rails, signatures, and scene auxiliaries keep advancing while the shared reset opacity fades to 0.03",
          },
        },
      };
    },
    {
      selectedPreset: preset,
      selectedFps: fps,
      selectedFrameCount: frameCount,
      selectedSceneContract: sceneContract,
      minimumFrameCount: MINIMUM_FRAME_COUNT,
      emptyOpeningFrame: EMPTY_OPENING_FRAME,
      resetOpacitySamples: RESET_OPACITY_SAMPLES,
    },
  );
}

async function renderFrames(arguments_) {
  const input = path.resolve(arguments_.input || "");
  const framesDirectory = path.resolve(arguments_["frames-dir"] || "");
  const preset = arguments_.preset || "";
  const sceneContract = SCENE_CONTRACTS[preset];
  const duration = Number(arguments_.duration);
  const fps = Number(arguments_.fps);
  const width = Number(arguments_.width);
  const height = Number(arguments_.height);
  if (!fs.existsSync(input) || !fs.statSync(input).isFile()) {
    throw new Error(`Input does not exist: ${input}`);
  }
  if (!fs.existsSync(framesDirectory) || !fs.statSync(framesDirectory).isDirectory()) {
    throw new Error(`Frames directory does not exist: ${framesDirectory}`);
  }
  if (
    !PRESETS.has(preset) ||
    !Number.isFinite(duration) || duration < 0.5 || duration > 20 ||
    !Number.isInteger(fps) || fps < 1 || fps > 25 ||
    !Number.isInteger(width) || width < 320 || width > 4096 ||
    !Number.isInteger(height) || height < 1 || height > 4096
  ) {
    throw new Error("Preset, duration, fps, or dimensions are invalid");
  }
  if (height > 4096 || width * height > 16777216) {
    throw new Error("Output must stay within 4096px per side and 16 megapixels");
  }

  const svg = fs.readFileSync(input, "utf8");
  const viewBoxMatch = svg.match(/viewBox="([^"]+)"/i);
  if (!viewBoxMatch) {
    throw new Error("SVG viewBox is unavailable");
  }
  const viewBox = viewBoxMatch[1].trim().split(/[\s,]+/).map(Number);
  if (viewBox.length !== 4 || viewBox.some((value) => !Number.isFinite(value))) {
    throw new Error("SVG viewBox is invalid");
  }
  const requestedFrames = duration * fps;
  const frameCount = Math.round(requestedFrames);
  if (Math.abs(requestedFrames - frameCount) > 1e-9 || frameCount > 500) {
    throw new Error("Duration multiplied by fps must be a whole number of at most 500 frames");
  }
  if (frameCount < MINIMUM_FRAME_COUNT) {
    throw new Error(`${sceneContract.name} requires at least ${MINIMUM_FRAME_COUNT} rendered frames`);
  }
  if (width * height * frameCount > 600000000) {
    throw new Error("Output dimensions multiplied by frame count may not exceed 600 million pixels");
  }

  const loaded = loadRenderer();
  const executablePath = chromeExecutable(loaded.api);
  if (!executablePath) {
    throw new Error("No compatible Chrome or Chromium executable was found");
  }
  const noSandbox = process.env.FIREWORKS_CHROME_NO_SANDBOX === "1";
  const chromeArguments = [
    "--disable-dev-shm-usage",
    "--disable-background-timer-throttling",
    "--disable-renderer-backgrounding",
    "--force-color-profile=srgb",
    "--font-render-hinting=none",
    "--hide-scrollbars",
  ];
  if (noSandbox) {
    chromeArguments.unshift("--no-sandbox", "--disable-setuid-sandbox");
  }
  const browser = await loaded.api.launch({
    headless: "new",
    executablePath,
    args: chromeArguments,
  });

  try {
    const page = await browser.newPage();
    await page.setViewport({ width, height, deviceScaleFactor: 1 });
    await page.setRequestInterception(true);
    page.on("request", (request) => {
      const url = request.url();
      if (url === "about:blank" || url.startsWith("data:") || url.startsWith("blob:")) {
        request.continue();
      } else {
        request.abort("blockedbyclient");
      }
    });
    await page.setContent(
      `<html><head><meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src data:; style-src 'unsafe-inline'">` +
      `<style>html,body{margin:0;width:${width}px;height:${height}px;overflow:hidden;background:transparent}` +
      `*{animation:none!important;transition:none!important;caret-color:transparent!important}` +
      `svg{display:block;width:${width}px!important;height:${height}px!important}</style></head>` +
      `<body><main id="diagram"></main></body></html>`,
      { waitUntil: "load" },
    );
    await page.$eval("#diagram", (container, source) => { container.innerHTML = source; }, svg);
    await page.evaluate(() => document.fonts.ready);
    const setup = await installMotionRuntime(page, preset, fps, frameCount);
    await page.evaluate(async () => {
      await document.fonts.ready;
      await new Promise((resolve) => {
        requestAnimationFrame(() => requestAnimationFrame(resolve));
      });
    });

    for (let index = 0; index < frameCount; index += 1) {
      await page.evaluate(async (frameIndex) => {
        window.__fireworksSetMotionFrame(frameIndex);
        await new Promise((resolve) => {
          requestAnimationFrame(() => requestAnimationFrame(resolve));
        });
      }, index);
      const framePath = path.join(framesDirectory, `frame-${String(index).padStart(6, "0")}.png`);
      await page.screenshot({
        path: framePath,
        type: "png",
        omitBackground: true,
        captureBeyondViewport: false,
      });
    }
    await page.close();
    return {
      ok: true,
      engine: "chromium-svg-draw-on-persistent-data-flow",
      module: loaded.module,
      resolved_module: loaded.resolvedModule,
      module_version: loaded.moduleVersion,
      chrome: executablePath,
      preset,
      duration_seconds: duration,
      fps,
      frame_count: frameCount,
      width,
      height,
      input_kind: "svg",
      effects: setup.effects,
      scene_report: setup.scene_report,
      loop_frame_policy: "integer-frame-index-centers-with-steady-state-plus-reset-boundary-repeat-scope",
      paint_barrier: "fonts-ready-plus-two-animation-frames-before-capture",
      sandbox: noSandbox ? "disabled-by-explicit-env" : "enabled",
    };
  } finally {
    await browser.close();
  }
}

async function main() {
  const arguments_ = parseArguments(process.argv.slice(2));
  const result = arguments_.probe ? probe() : await renderFrames(arguments_);
  process.stdout.write(`${JSON.stringify(result)}\n`);
}

main().catch((error) => {
  process.stderr.write(`${error.stack || error.message}\n`);
  process.exit(1);
});
skills/fireworks-tech-graph/docs/releases/README.md
# Release history

This directory is the source of truth for the curated GitHub Release notes. Version dates describe when each version originally became available, not when a historical GitHub Release page was backfilled.

| Version | Version date | Tag commit | npm `gitHead` | Distribution record | Notes |
| --- | --- | --- | --- | --- | --- |
| [`v1.2.0`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/releases/tag/v1.2.0) | 2026-07-17 | [`ea534c2d`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/ea534c2d2817d43e4c02fe0ea9f67dfced6dece3) | — | Latest GitHub release | [Release notes](v1.2.0.md) |
| [`v1.1.0`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/releases/tag/v1.1.0) | 2026-07-15 | [`1c7ba0fe`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/1c7ba0fe69ef5b166821eefb6fe447a0e5d70be9) | — | GitHub release | [Release notes](v1.1.0.md) |
| [`v1.0.5`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/releases/tag/v1.0.5) | 2026-07-11 | [`14be3ad3`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/14be3ad3b05389a5d603562c207eb37157637127) | — | Repository version; not published to npm | [Release notes](v1.0.5.md) |
| [`v1.0.4`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/releases/tag/v1.0.4) | 2026-04-12 | [`c8668407`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/c8668407ebfa72e00719be8f4312b71ab90c3c3e) | [`c3726124`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/c37261248ca674d5d94e24d9d7d85e50a88c77d1) | [npm `1.0.4`](https://www.npmjs.com/package/@yizhiyanhua-ai/fireworks-tech-graph/v/1.0.4) | [Release notes](v1.0.4.md) |
| [`v1.0.3`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/releases/tag/v1.0.3) | 2026-04-12 | [`c3726124`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/c37261248ca674d5d94e24d9d7d85e50a88c77d1) | [`921fb308`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/921fb30855cd76e03e1b96ff3463028ddcd36deb) | [npm `1.0.3`](https://www.npmjs.com/package/@yizhiyanhua-ai/fireworks-tech-graph/v/1.0.3) | [Release notes](v1.0.3.md) |
| [`v1.0.2`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/releases/tag/v1.0.2) | 2026-04-12 | [`921fb308`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/921fb30855cd76e03e1b96ff3463028ddcd36deb) | [`fee15924`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/fee15924e5d9a273d270067dce6380f27de3d033) | [npm `1.0.2`](https://www.npmjs.com/package/@yizhiyanhua-ai/fireworks-tech-graph/v/1.0.2) | [Release notes](v1.0.2.md) |
| [`v1.0.1`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/releases/tag/v1.0.1) | 2026-04-12 | [`27f41d98`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/27f41d98883113affbb28a2ebfff4b566e3cc819) | [`27f41d98`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/27f41d98883113affbb28a2ebfff4b566e3cc819) | [npm `1.0.1`](https://www.npmjs.com/package/@yizhiyanhua-ai/fireworks-tech-graph/v/1.0.1) | [Release notes](v1.0.1.md) |
| [`v1.0.0`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/releases/tag/v1.0.0) | 2026-04-12 | [`27f41d98`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/27f41d98883113affbb28a2ebfff4b566e3cc819) | [`27f41d98`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/27f41d98883113affbb28a2ebfff4b566e3cc819) | [npm `1.0.0`](https://www.npmjs.com/package/@yizhiyanhua-ai/fireworks-tech-graph/v/1.0.0) | [Release notes](v1.0.0.md) |

## Historical provenance

GitHub Release pages for `v1.0.0` through `v1.0.5` were backfilled on 2026-07-17. The npm packages were sometimes published from a working tree immediately before its matching commit: npm `1.0.2`, `1.0.3`, and `1.0.4` retain the preceding `gitHead`, while their complete published file sets match the subsequent commits shown in the Tag commit column. No fully committed tree reproduces every packaging change in npm `1.0.0` or `1.0.1`; those tags use the registry source anchor, and their notes state the limitation.
skills/fireworks-tech-graph/docs/releases/v1.0.1.md
# Fireworks Tech Graph v1.0.1

This patch release tightened the diagram-quality prompt contract and corrected npm repository metadata.

## Version information

- Version: `v1.0.1` / npm `1.0.1`
- Version date: 2026-04-12; npm published at `2026-04-12T00:12:37.149Z`
- Tag commit: [`27f41d98`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/27f41d98883113affbb28a2ebfff4b566e3cc819)
- npm registry `gitHead`: [`27f41d98`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/27f41d98883113affbb28a2ebfff4b566e3cc819)
- Previous version: [`v1.0.0`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/releases/tag/v1.0.0)
- Runtime metadata: Node.js 14 or newer
- Distribution: [npm package `1.0.1`](https://www.npmjs.com/package/@yizhiyanhua-ai/fireworks-tech-graph/v/1.0.1)

## Highlights

- Added an explicit validation checklist for connector-node collisions and orthogonal rerouting.
- Added text-overflow, endpoint-alignment, and connector-label background checks.
- Normalized the npm repository URL metadata.

## Provenance

The npm registry records the same `gitHead` for `1.0.0` and `1.0.1`. The published tarball contains two working-tree-only changes relative to that commit: `package.json` and an expanded `SKILL.md` layout-validation checklist; the other 24 files match byte-for-byte. No historical commit exactly reproduces this npm artifact, so the backfilled tag records the registry provenance anchor.
scripts/generate-from-template.py
#!/usr/bin/env python3
"""
Style-driven SVG diagram generator.

Usage:
  python3 generate-from-template.py <template-type> <output-path> [data-json]

This generator intentionally does more than "fill a template".
It encodes the visual language from the documented style guides so the output
tracks the showcase quality more closely than the previous generic renderer.
"""

from __future__ import annotations

import argparse
import copy
import hashlib
import heapq
import json
import math
import os
import re
import sys
from dataclasses import dataclass
from typing import Dict, List, Mapping, Optional, Sequence, Tuple
from xml.sax.saxutils import escape

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
if SCRIPT_DIR not in sys.path:
    sys.path.insert(0, SCRIPT_DIR)

import fireworks_geometry as geometry  # noqa: E402
import composition_quality as quality  # noqa: E402
from diagram_ir import normalize_diagram  # noqa: E402
from semantic_contracts import STYLE_NAMES, resolve_style_index  # noqa: E402

Point = Tuple[float, float]
Bounds = Tuple[float, float, float, float]

TEMPLATE_DIR = os.path.join(SCRIPT_DIR, "..", "templates")
DEFAULT_VIEWBOX = {
    "architecture": (960, 600),
    "data-flow": (960, 600),
    "flowchart": (960, 640),
    "sequence": (960, 700),
    "comparison": (960, 620),
    "timeline": (960, 520),
    "mind-map": (960, 620),
    "agent": (960, 700),
    "memory": (960, 720),
    "use-case": (960, 600),
    "class": (960, 700),
    "state-machine": (960, 620),
    "er-diagram": (960, 680),
    "network-topology": (960, 620),
}

FLOW_ALIASES = {
    "main": "control",
    "api": "control",
    "control": "control",
    "write": "write",
    "read": "read",
    "data": "data",
    "async": "async",
    "feedback": "feedback",
    "neutral": "neutral",
}

MARKER_IDS = {
    "control": "arrowA",
    "write": "arrowB",
    "read": "arrowC",
    "data": "arrowE",
    "async": "arrowF",
    "feedback": "arrowG",
    "neutral": "arrowH",
}

STYLE_PROFILES: Dict[int, Dict[str, object]] = {
    1: {
        "name": "Flat Icon",
        "font_family": "'Helvetica Neue', Helvetica, Arial, 'PingFang SC', 'Microsoft YaHei', 'Microsoft JhengHei', 'SimHei', sans-serif",
        "background": "#ffffff",
        "shadow": True,
        "title_align": "center",
        "title_fill": "#111827",
        "title_size": 30,
        "subtitle_fill": "#6b7280",
        "subtitle_size": 14,
        "node_fill": "#ffffff",
        "node_stroke": "#d1d5db",
        "node_radius": 10,
        "node_shadow": "url(#shadowSoft)",
        "section_fill": "none",
        "section_stroke": "#dbe5f1",
        "section_dash": "6 5",
        "section_label_fill": "#2563eb",
        "section_sub_fill": "#94a3b8",
        "title_divider": False,
        "section_upper": True,
        "arrow_width": 2.4,
        "arrow_colors": {
            "control": "#7c3aed",
            "write": "#10b981",
            "read": "#2563eb",
            "data": "#f97316",
            "async": "#7c3aed",
            "feedback": "#ef4444",
            "neutral": "#6b7280",
        },
        "arrow_label_bg": "#ffffff",
        "arrow_label_opacity": 0.94,
        "arrow_label_fill": "#6b7280",
        "type_label_fill": "#9ca3af",
        "type_label_size": 12,
        "text_primary": "#111827",
        "text_secondary": "#6b7280",
        "text_muted": "#94a3b8",
        "legend_fill": "#6b7280",
    },
    2: {
        "name": "Dark Terminal",
        "font_family": "'SF Mono', 'Fira Code', Menlo, 'Microsoft YaHei', 'SimHei', monospace",
        "background": "#0f172a",
        "shadow": False,
        "title_align": "center",
        "title_fill": "#e2e8f0",
        "title_size": 30,
        "subtitle_fill": "#94a3b8",
        "subtitle_size": 14,
        "node_fill": "#111827",
        "node_stroke": "#334155",
        "node_radius": 10,
        "node_shadow": "",
        "section_fill": "rgba(15,23,42,0.28)",
        "section_stroke": "#334155",
        "section_dash": "7 6",
        "section_label_fill": "#38bdf8",
        "section_sub_fill": "#64748b",
        "title_divider": False,
        "section_upper": True,
        "arrow_width": 2.3,
        "arrow_colors": {
            "control": "#a855f7",
            "write": "#22c55e",
            "read": "#38bdf8",
            "data": "#fb7185",
            "async": "#f59e0b",
            "feedback": "#f97316",
            "neutral": "#94a3b8",
        },
        "arrow_label_bg": "#0f172a",
        "arrow_label_opacity": 0.92,
        "arrow_label_fill": "#cbd5e1",
        "type_label_fill": "#64748b",
        "type_label_size": 12,
        "text_primary": "#e2e8f0",
        "text_secondary": "#94a3b8",
        "text_muted": "#64748b",
        "legend_fill": "#94a3b8",
    },
    3: {
        "name": "Blueprint",
        "font_family": "'SF Mono', 'Fira Code', Menlo, 'Microsoft YaHei', 'SimHei', monospace",
        "background": "#082f49",
        "shadow": False,
        "title_align": "center",
        "title_fill": "#e0f2fe",
        "title_size": 30,
        "subtitle_fill": "#7dd3fc",
        "subtitle_size": 14,
        "node_fill": "#0b3b5e",
        "node_stroke": "#67e8f9",
        "node_radius": 8,
        "node_shadow": "",
        "section_fill": "none",
        "section_stroke": "#0ea5e9",
        "section_dash": "6 4",
        "section_label_fill": "#67e8f9",
        "section_sub_fill": "#7dd3fc",
        "title_divider": False,
        "section_upper": True,
        "arrow_width": 2.1,
        "arrow_colors": {
            "control": "#67e8f9",
            "write": "#22d3ee",
            "read": "#38bdf8",
            "data": "#fde047",
            "async": "#c084fc",
            "feedback": "#fb7185",
            "neutral": "#bae6fd",
        },
        "arrow_label_bg": "#082f49",
        "arrow_label_opacity": 0.9,
        "arrow_label_fill": "#e0f2fe",
        "type_label_fill": "#7dd3fc",
        "type_label_size": 11,
        "text_primary": "#e0f2fe",
        "text_secondary": "#bae6fd",
        "text_muted": "#7dd3fc",
        "legend_fill": "#bae6fd",
    },
    4: {
        "name": "Notion Clean",
        "font_family": "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Arial, 'PingFang SC', 'Microsoft YaHei', 'Microsoft JhengHei', 'SimHei', sans-serif",
        "background": "#ffffff",
        "shadow": False,
        "title_align": "left",
        "title_fill": "#111827",
        "title_size": 18,
        "subtitle_fill": "#9ca3af",
        "subtitle_size": 13,
        "node_fill": "#f9fafb",
        "node_stroke": "#e5e7eb",
        "node_radius": 4,
        "node_shadow": "",
        "section_fill": "none",
        "section_stroke": "#e5e7eb",
        "section_dash": "",
        "section_label_fill": "#9ca3af",
        "section_sub_fill": "#d1d5db",
        "title_divider": True,
        "section_upper": True,
        "arrow_width": 1.8,
        "arrow_colors": {
            "control": "#3b82f6",
            "write": "#3b82f6",
            "read": "#3b82f6",
            "data": "#3b82f6",
            "async": "#9ca3af",
            "feedback": "#9ca3af",
            "neutral": "#d1d5db",
        },
        "arrow_label_bg": "#ffffff",
        "arrow_label_opacity": 0.96,
        "arrow_label_fill": "#6b7280",
        "type_label_fill": "#9ca3af",
        "type_label_size": 11,
        "text_primary": "#111827",
        "text_secondary": "#374151",
        "text_muted": "#9ca3af",
        "legend_fill": "#6b7280",
    },
    5: {
        "name": "Glassmorphism",
        "font_family": "'Helvetica Neue', Helvetica, Arial, 'PingFang SC', 'Microsoft YaHei', 'Microsoft JhengHei', 'SimHei', sans-serif",
        "background": "#0f172a",
        "shadow": True,
        "title_align": "center",
        "title_fill": "#f8fafc",
        "title_size": 30,
        "subtitle_fill": "#cbd5e1",
        "subtitle_size": 14,
        "node_fill": "rgba(255,255,255,0.12)",
        "node_stroke": "rgba(255,255,255,0.28)",
        "node_radius": 18,
        "node_shadow": "url(#shadowGlass)",
        "section_fill": "rgba(255,255,255,0.05)",
        "section_stroke": "rgba(255,255,255,0.18)",
        "section_dash": "7 6",
        "section_label_fill": "#e2e8f0",
        "section_sub_fill": "#94a3b8",
        "title_divider": False,
        "section_upper": True,
        "arrow_width": 2.2,
        "arrow_colors": {
            "control": "#c084fc",
            "write": "#34d399",
            "read": "#60a5fa",
            "data": "#fb923c",
            "async": "#f472b6",
            "feedback": "#f59e0b",
            "neutral": "#cbd5e1",
        },
        "arrow_label_bg": "rgba(15,23,42,0.7)",
        "arrow_label_opacity": 1,
        "arrow_label_fill": "#e2e8f0",
        "type_label_fill": "#cbd5e1",
        "type_label_size": 12,
        "text_primary": "#f8fafc",
        "text_secondary": "#cbd5e1",
        "text_muted": "#94a3b8",
        "legend_fill": "#cbd5e1",
    },
    6: {
        "name": "Claude Official",
        "font_family": "'Helvetica Neue', Helvetica, Arial, 'PingFang SC', 'Microsoft YaHei', 'Microsoft JhengHei', 'SimHei', sans-serif",
        "background": "#f8f6f3",
        "shadow": False,
        "title_align": "left",
        "title_fill": "#141413",
        "title_size": 24,
        "subtitle_fill": "#8f8a80",
        "subtitle_size": 13,
        "node_fill": "#fffcf7",
        "node_stroke": "#d9d0c3",
        "node_radius": 10,
        "node_shadow": "",
        "section_fill": "none",
        "section_stroke": "#ded8cf",
        "section_dash": "5 4",
        "section_label_fill": "#8b7355",
        "section_sub_fill": "#b4aba0",
        "title_divider": True,
        "section_upper": True,
        "arrow_width": 2.0,
        "arrow_colors": {
            "control": "#d97757",
            "write": "#7b8b5c",
            "read": "#8c6f5a",
            "data": "#b45309",
            "async": "#9a6fb0",
            "feedback": "#7c5c96",
            "neutral": "#8f8a80",
        },
        "arrow_label_bg": "#f8f6f3",
        "arrow_label_opacity": 0.96,
        "arrow_label_fill": "#6b6257",
        "type_label_fill": "#a29a8f",
        "type_label_size": 11,
        "text_primary": "#141413",
        "text_secondary": "#6b6257",
        "text_muted": "#a29a8f",
        "legend_fill": "#6b6257",
    },
    7: {
        "name": "OpenAI",
        "font_family": "'Helvetica Neue', Helvetica, Arial, 'PingFang SC', 'Microsoft YaHei', 'Microsoft JhengHei', 'SimHei', sans-serif",
        "background": "#ffffff",
        "shadow": False,
        "title_align": "left",
        "title_fill": "#0f172a",
        "title_size": 24,
        "subtitle_fill": "#64748b",
        "subtitle_size": 13,
        "node_fill": "#ffffff",
        "node_stroke": "#dce5e3",
        "node_radius": 14,
        "node_shadow": "",
        "section_fill": "none",
        "section_stroke": "#e2e8f0",
        "section_dash": "5 4",
        "section_label_fill": "#10a37f",
        "section_sub_fill": "#94a3b8",
        "title_divider": True,
        "section_upper": True,
        "arrow_width": 2.0,
        "arrow_colors": {
            "control": "#10a37f",
            "write": "#0f766e",
            "read": "#0891b2",
            "data": "#f59e0b",
            "async": "#64748b",
            "feedback": "#475569",
            "neutral": "#94a3b8",
        },
        "arrow_label_bg": "#ffffff",
        "arrow_label_opacity": 0.96,
        "arrow_label_fill": "#475569",
        "type_label_fill": "#94a3b8",
        "type_label_size": 11,
        "text_primary": "#0f172a",
        "text_secondary": "#475569",
        "text_muted": "#94a3b8",
        "legend_fill": "#475569",
    },
    9: {
        "name": "C4 Review Canvas",
        "font_family": "'Avenir Next', Avenir, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif",
        "background": "#f7f2e8",
        "shadow": False,
        "title_align": "left",
        "title_fill": "#24312f",
        "title_size": 27,
        "subtitle_fill": "#6f756f",
        "subtitle_size": 13,
        "node_fill": "#fffdf7",
        "node_stroke": "#365f56",
        "node_radius": 7,
        "node_shadow": "",
        "section_fill": "rgba(255,253,247,0.48)",
        "section_stroke": "#8c7d68",
        "section_dash": "9 6",
        "section_label_fill": "#5b5144",
        "section_sub_fill": "#8c7d68",
        "title_divider": False,
        "section_upper": False,
        "arrow_width": 2.0,
        "arrow_colors": {
            "control": "#365f56",
            "write": "#a44a3f",
            "read": "#356a8a",
            "data": "#c06b35",
            "async": "#7a5c99",
            "feedback": "#b13e53",
            "neutral": "#746b60",
        },
        "arrow_label_bg": "#f7f2e8",
        "arrow_label_opacity": 0.94,
        "arrow_label_fill": "#4b5563",
        "type_label_fill": "#8a6f43",
        "type_label_size": 10,
        "text_primary": "#24312f",
        "text_secondary": "#5f665f",
        "text_muted": "#8a8d86",
        "legend_fill": "#5f665f",
        "canvas_treatment": "review",
    },
    10: {
        "name": "Cloud Fabric",
        "font_family": "Inter, 'Helvetica Neue', 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif",
        "background": "#edf5fb",
        "shadow": True,
        "title_align": "left",
        "title_fill": "#102a43",
        "title_size": 27,
        "subtitle_fill": "#52718d",
        "subtitle_size": 13,
        "node_fill": "#ffffff",
        "node_stroke": "#9bb7cf",
        "node_radius": 12,
        "node_shadow": "url(#shadowSoft)",
        "section_fill": "rgba(255,255,255,0.54)",
        "section_stroke": "#7fa3c2",
        "section_dash": "7 5",
        "section_label_fill": "#315d7e",
        "section_sub_fill": "#7892a8",
        "title_divider": False,
        "section_upper": True,
        "arrow_width": 2.2,
        "arrow_colors": {
            "control": "#2563eb",
            "write": "#ea580c",
            "read": "#0891b2",
            "data": "#059669",
            "async": "#7c3aed",
            "feedback": "#db2777",
            "neutral": "#64748b",
        },
        "arrow_label_bg": "#f7fbfe",
        "arrow_label_opacity": 0.96,
        "arrow_label_fill": "#334e68",
        "type_label_fill": "#6b879d",
        "type_label_size": 10,
        "text_primary": "#102a43",
        "text_secondary": "#486581",
        "text_muted": "#829ab1",
        "legend_fill": "#486581",
        "canvas_treatment": "cloud",
    },
    11: {
        "name": "Event Transit",
        "font_family": "'Avenir Next', Avenir, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif",
        "background": "#fbf7ee",
        "shadow": False,
        "title_align": "left",
        "title_fill": "#17213c",
        "title_size": 27,
        "subtitle_fill": "#6e6a61",
        "subtitle_size": 13,
        "node_fill": "#fffdf8",
        "node_stroke": "#c9c2b4",
        "node_radius": 24,
        "node_shadow": "",
        "section_fill": "rgba(255,255,255,0.38)",
        "section_stroke": "#d4cbbb",
        "section_dash": "4 5",
        "section_label_fill": "#514c43",
        "section_sub_fill": "#8d867b",
        "title_divider": False,
        "section_upper": True,
        "arrow_width": 2.8,
        "arrow_colors": {
            "control": "#e4475b",
            "write": "#00897b",
            "read": "#2563eb",
            "data": "#f59e0b",
            "async": "#7c3aed",
            "feedback": "#c62828",
            "neutral": "#7a746a",
        },
        "arrow_label_bg": "#fbf7ee",
        "arrow_label_opacity": 0.96,
        "arrow_label_fill": "#4b5563",
        "type_label_fill": "#7a746a",
        "type_label_size": 10,
        "text_primary": "#17213c",
        "text_secondary": "#5e5a52",
        "text_muted": "#8d867b",
        "legend_fill": "#5e5a52",
        "canvas_treatment": "transit",
        "rail_casing": "#514c43",
    },
    12: {
        "name": "Ops Pulse",
        "font_family": "'SF Mono', 'Fira Code', Menlo, 'Microsoft YaHei', monospace",
        "background": "#07111f",
        "shadow": False,
        "title_align": "left",
        "title_fill": "#eff6ff",
        "title_size": 27,
        "subtitle_fill": "#8aa4bd",
        "subtitle_size": 13,
        "node_fill": "#0d1b2a",
        "node_stroke": "#29435d",
        "node_radius": 12,
        "node_shadow": "",
        "section_fill": "rgba(13,27,42,0.72)",
        "section_stroke": "#28445f",
        "section_dash": "6 5",
        "section_label_fill": "#38bdf8",
        "section_sub_fill": "#6f8ba5",
        "title_divider": False,
        "section_upper": True,
        "arrow_width": 2.4,
        "arrow_colors": {
            "control": "#f59e0b",
            "write": "#22c55e",
            "read": "#38bdf8",
            "data": "#fb7185",
            "async": "#22d3ee",
            "feedback": "#f43f5e",
            "neutral": "#7892a8",
        },
        "arrow_label_bg": "#07111f",
        "arrow_label_opacity": 0.94,
        "arrow_label_fill": "#cbd5e1",
        "type_label_fill": "#6f8ba5",
        "type_label_size": 10,
        "text_primary": "#eff6ff",
        "text_secondary": "#9fb3c8",
        "text_muted": "#647f99",
        "legend_fill": "#9fb3c8",
        "canvas_treatment": "ops",
    },
}


@dataclass
class Node:
    node_id: str
    kind: str
    shape: str
    data: Dict[str, object]
    bounds: Bounds
    cx: float
    cy: float


@dataclass
class ArrowRender:
    edge_id: str
    path_svg: str
    label_svg: str
    label_bounds: Optional[Bounds]
    route: List[Point]
    report: Dict[str, object]


def style_value(style: Dict[str, object], key: str) -> object:
    return style[key]


def to_float(value: object, default: float = 0.0) -> float:
    try:
        return float(value)
    except (TypeError, ValueError):
        return default


def normalize_text(value: object) -> str:
    return escape(str(value)) if value is not None else ""


def normalize_attribute(value: object) -> str:
    return escape(str(value), {'"': "&quot;"}) if value is not None else ""


def safe_identifier(value: object, fallback: str) -> str:
    cleaned = re.sub(r"[^A-Za-z0-9_.:-]+", "-", str(value or fallback)).strip("-")
    return cleaned or fallback


_RESERVED_DOM_IDS = frozenset(
    set(MARKER_IDS.values())
    | {
        "blueprint-title-block",
        "blueprintGrid",
        "cloudGradient",
        "cloudGrid",
        "footer",
        "glowBlue",
        "glowGreen",
        "glowOrange",
        "glowPurple",
        "legend",
        "legend-zone",
        "opsGradient",
        "opsGrid",
        "pulseGlow",
        "reviewGrid",
        "shadowGlass",
        "shadowSoft",
        "style-signature",
        "terminalGradient",
        "transitDots",
    }
)
_EDGE_DOM_SUFFIXES = (
    "-bridge-mask",
    "-critical-glow",
    "-direction",
    "-hop",
    "-label",
    "-rail-casing",
    "-review-stroke",
)


def allocate_dom_identifier(base: str, used: set[str], suffixes: Sequence[str] = ()) -> str:
    """Allocate one deterministic SVG id while preserving readable ids when safe."""

    candidate = safe_identifier(base, "element")
    sequence = 2
    while any(identifier in used for identifier in (candidate, *(candidate + suffix for suffix in suffixes))):
        candidate = f"{safe_identifier(base, 'element')}-{sequence}"
        sequence += 1
    used.add(candidate)
    used.update(candidate + suffix for suffix in suffixes)
    return candidate


# Style 8 is AI-authored: the AI reads references/style-8-dark-luxury.md and hand-crafts
# the SVG directly. It cannot be driven by this template generator.
_AI_AUTHORED_STYLES: Dict[int, str] = {8: "Style 8 (Dark Luxury)"}
_AI_AUTHORED_MSG = (
    "{name} is an AI-authored style and cannot be used with generate-from-template.py. "
    "Load references/style-8-dark-luxury.md for the full spec and hand-craft the SVG directly."
)


def parse_style(raw: object) -> Tuple[int, Dict[str, object]]:
    index = resolve_style_index({"style": raw}) if raw is not None else 1
    if index in _AI_AUTHORED_STYLES:
        raise ValueError(_AI_AUTHORED_MSG.format(name=_AI_AUTHORED_STYLES[index]))
    if index not in STYLE_PROFILES:
        raise ValueError(f"Unsupported style: {raw}")
    return index, copy.deepcopy(STYLE_PROFILES[index])


def parse_template_viewbox(template_type: str) -> Tuple[float, float]:
    template_path = os.path.join(TEMPLATE_DIR, f"{template_type}.svg")
    if os.path.exists(template_path):
        with open(template_path, "r", encoding="utf-8") as handle:
            content = handle.read()
        match = re.search(r'viewBox="0 0 ([0-9.]+) ([0-9.]+)"', content)
        if match:
            return float(match.group(1)), float(match.group(2))
    return DEFAULT_VIEWBOX.get(template_type, (960, 600))


def render_defs(style_index: int, style: Dict[str, object]) -> str:
    marker_size = "8" if style_index == 4 else "12" if style_index == 11 else "10"
    marker_height = "6" if style_index == 4 else "9" if style_index == 11 else "7"
    ref_x = "7" if style_index == 4 else "11" if style_index == 11 else "9"
    ref_y = "3" if style_index == 4 else "4.5" if style_index == 11 else "3.5"
    marker_units = ' markerUnits="userSpaceOnUse"' if style_index == 11 else ""
    color_map = style_value(style, "arrow_colors")
    marker_lines = []
    for key, color in color_map.items():
        marker_id = MARKER_IDS.get(key, "arrowA")
        marker_lines.append(
            f'    <marker id="{marker_id}" markerWidth="{marker_size}" markerHeight="{marker_height}" '
            f'refX="{ref_x}" refY="{ref_y}" orient="auto"{marker_units}>'
        )
        if style_index == 4:
            marker_lines.append(f'      <polygon points="0 0, 8 3, 0 6" fill="{color}"/>')
        elif style_index == 11:
            marker_lines.append(f'      <polygon points="0 0, 12 4.5, 0 9" fill="{color}"/>')
        else:
            marker_lines.append(f'      <polygon points="0 0, 10 3.5, 0 7" fill="{color}"/>')
        marker_lines.append("    </marker>")

    filters = []
    if style_value(style, "shadow"):
        filters.extend(
            [
                '    <filter id="shadowSoft" x="-20%" y="-20%" width="140%" height="160%">',
                '      <feDropShadow dx="0" dy="3" stdDeviation="6" flood-color="#0f172a" flood-opacity="0.12"/>',
                "    </filter>",
                '    <filter id="shadowGlass" x="-20%" y="-20%" width="140%" height="160%">',
                '      <feDropShadow dx="0" dy="10" stdDeviation="16" flood-color="#020617" flood-opacity="0.28"/>',
                "    </filter>",
            ]
        )

    if style_index == 3:
        filters.extend(
            [
                '    <pattern id="blueprintGrid" width="32" height="32" patternUnits="userSpaceOnUse">',
                '      <path d="M 32 0 L 0 0 0 32" fill="none" stroke="#0ea5e9" stroke-opacity="0.12" stroke-width="1"/>',
                "    </pattern>",
            ]
        )
    if style_index == 2:
        filters.extend(
            [
                '    <linearGradient id="terminalGradient" x1="0%" y1="0%" x2="100%" y2="100%">',
                '      <stop offset="0%" stop-color="#0f0f1a"/>',
                '      <stop offset="100%" stop-color="#1a1a2e"/>',
                "    </linearGradient>",
                '    <filter id="glowBlue" x="-30%" y="-30%" width="160%" height="160%">',
                '      <feDropShadow dx="0" dy="0" stdDeviation="5" flood-color="#3b82f6" flood-opacity="0.65"/>',
                "    </filter>",
                '    <filter id="glowPurple" x="-30%" y="-30%" width="160%" height="160%">',
                '      <feDropShadow dx="0" dy="0" stdDeviation="5" flood-color="#a855f7" flood-opacity="0.72"/>',
                "    </filter>",
                '    <filter id="glowGreen" x="-30%" y="-30%" width="160%" height="160%">',
                '      <feDropShadow dx="0" dy="0" stdDeviation="5" flood-color="#22c55e" flood-opacity="0.62"/>',
                "    </filter>",
                '    <filter id="glowOrange" x="-30%" y="-30%" width="160%" height="160%">',
                '      <feDropShadow dx="0" dy="0" stdDeviation="5" flood-color="#f97316" flood-opacity="0.62"/>',
                "    </filter>",
            ]
        )
    if style_index == 9:
        filters.extend(
            [
                '    <pattern id="reviewGrid" width="24" height="24" patternUnits="userSpaceOnUse">',
                '      <circle cx="1" cy="1" r="0.8" fill="#8c7d68" fill-opacity="0.18"/>',
                "    </pattern>",
            ]
        )
    if style_index == 10:
        filters.extend(
            [
                '    <linearGradient id="cloudGradient" x1="0%" y1="0%" x2="100%" y2="100%">',
                '      <stop offset="0%" stop-color="#f8fcff"/>',
                '      <stop offset="100%" stop-color="#dfedf7"/>',
                "    </linearGradient>",
                '    <pattern id="cloudGrid" width="32" height="32" patternUnits="userSpaceOnUse">',
                '      <path d="M 32 0 L 0 0 0 32" fill="none" stroke="#7fa3c2" stroke-opacity="0.10" stroke-width="1"/>',
                "    </pattern>",
            ]
        )
    if style_index == 11:
        filters.extend(
            [
                '    <pattern id="transitDots" width="28" height="28" patternUnits="userSpaceOnUse">',
                '      <circle cx="2" cy="2" r="0.9" fill="#8d867b" fill-opacity="0.12"/>',
                "    </pattern>",
            ]
        )
    if style_index == 12:
        filters.extend(
            [
                '    <linearGradient id="opsGradient" x1="0%" y1="0%" x2="100%" y2="100%">',
                '      <stop offset="0%" stop-color="#07111f"/>',
                '      <stop offset="100%" stop-color="#0b1b2e"/>',
                "    </linearGradient>",
                '    <pattern id="opsGrid" width="36" height="36" patternUnits="userSpaceOnUse">',
                '      <path d="M 36 0 L 0 0 0 36" fill="none" stroke="#38bdf8" stroke-opacity="0.055" stroke-width="1"/>',
                "    </pattern>",
                '    <filter id="pulseGlow" x="-30%" y="-30%" width="160%" height="160%">',
                '      <feDropShadow dx="0" dy="0" stdDeviation="4" flood-color="#f59e0b" flood-opacity="0.62"/>',
                "    </filter>",
            ]
        )

    styles = [
        f"    text {{ font-family: {style_value(style, 'font_family')}; }}",
        f"    .title {{ font-size: {style_value(style, 'title_size')}px; font-weight: 700; fill: {style_value(style, 'title_fill')}; }}",
        f"    .subtitle {{ font-size: {style_value(style, 'subtitle_size')}px; font-weight: 500; fill: {style_value(style, 'subtitle_fill')}; }}",
        f"    .section {{ font-size: 13px; font-weight: 700; fill: {style_value(style, 'section_label_fill')}; letter-spacing: 1.4px; }}",
        f"    .section-sub {{ font-size: 12px; font-weight: 500; fill: {style_value(style, 'section_sub_fill')}; }}",
        f"    .node-title {{ font-weight: 700; fill: {style_value(style, 'text_primary')}; }}",
        f"    .node-sub {{ font-size: 12px; font-weight: 500; fill: {style_value(style, 'text_secondary')}; }}",
        f"    .node-type {{ font-size: {style_value(style, 'type_label_size')}px; font-weight: 700; fill: {style_value(style, 'type_label_fill')}; letter-spacing: 0.08em; }}",
        f"    .arrow-label {{ font-size: 12px; font-weight: 600; fill: {style_value(style, 'arrow_label_fill')}; }}",
        f"    .legend {{ font-size: 12px; font-weight: 500; fill: {style_value(style, 'legend_fill')}; }}",
        f"    .footnote {{ font-size: 12px; font-weight: 500; fill: {style_value(style, 'text_muted')}; }}",
        f"    .metric-label {{ font-size: 8.5px; font-weight: 700; fill: {style_value(style, 'text_muted')}; text-transform: uppercase; }}",
        f"    .metric-value {{ font-size: 9.5px; font-weight: 700; fill: {style_value(style, 'text_primary')}; }}",
    ]
    return "\n".join(
        ["  <defs>"] + marker_lines + filters + ["    <style>"] + styles + ["    </style>", "  </defs>"]
    )


def render_canvas(style_index: int, style: Dict[str, object], width: float, height: float) -> str:
    background = str(style_value(style, "background"))
    if style_index == 2:
        parts = [f'  <rect data-graph-role="background" width="{width}" height="{height}" fill="url(#terminalGradient)"/>']
    elif style_index == 9:
        parts = [
            f'  <rect data-graph-role="background" width="{width}" height="{height}" fill="{background}"/>',
            f'  <rect data-graph-role="decoration" width="{width}" height="{height}" fill="url(#reviewGrid)"/>',
        ]
    elif style_index == 10:
        parts = [
            f'  <rect data-graph-role="background" width="{width}" height="{height}" fill="url(#cloudGradient)"/>',
            f'  <rect data-graph-role="decoration" width="{width}" height="{height}" fill="url(#cloudGrid)"/>',
        ]
    elif style_index == 11:
        parts = [
            f'  <rect data-graph-role="background" width="{width}" height="{height}" fill="{background}"/>',
            f'  <rect data-graph-role="decoration" width="{width}" height="{height}" fill="url(#transitDots)"/>',
        ]
    elif style_index == 12:
        parts = [
            f'  <rect data-graph-role="background" width="{width}" height="{height}" fill="url(#opsGradient)"/>',
            f'  <rect data-graph-role="decoration" width="{width}" height="{height}" fill="url(#opsGrid)"/>',
        ]
    else:
        parts = [f'  <rect data-graph-role="background" width="{width}" height="{height}" fill="{background}"/>']

    return "\n".join(parts)


def title_position(style: Dict[str, object], width: float) -> Tuple[float, str]:
    if style_value(style, "title_align") == "left":
        return 48.0, "start"
    return width / 2.0, "middle"


def render_title_block(style: Dict[str, object], data: Dict[str, object], width: float) -> Tuple[str, float]:
    title = normalize_text(data.get("title", "Diagram"))
    subtitle = normalize_text(data.get("subtitle", ""))
    x, anchor = title_position(style, width)
    if anchor == "middle":
        parts = [f'  <text x="{x}" y="56" text-anchor="{anchor}" class="title">{title}</text>']
        cursor_y = 82
        if subtitle:
            parts.append(f'  <text x="{x}" y="{cursor_y}" text-anchor="{anchor}" class="subtitle">{subtitle}</text>')
            cursor_y += 24
        return "\n".join(parts), cursor_y + 10

    parts = [f'  <text x="{x}" y="48" text-anchor="{anchor}" class="title">{title}</text>']
    cursor_y = 72
    if subtitle:
        parts.append(f'  <text x="{x}" y="{cursor_y}" text-anchor="{anchor}" class="subtitle">{subtitle}</text>')
        cursor_y += 18
    if style_value(style, "title_divider"):
        parts.append(
            f'  <line x1="48" y1="{cursor_y + 10}" x2="{width - 48}" y2="{cursor_y + 10}" '
            f'stroke="{style_value(style, "section_stroke")}" stroke-width="1"/>'
        )
        cursor_y += 26
    return "\n".join(parts), cursor_y + 8


def render_window_controls(data: Dict[str, object], style_index: int, width: float) -> str:
    controls = data.get("window_controls")
    if not controls:
        return ""
    if controls is True:
        controls = ["#ef4444", "#f59e0b", "#10b981"]
    if style_index != 2:
        return ""
    cursor_x = 20.0
    lines = []
    for color in controls:
        lines.append(f'  <circle cx="{cursor_x}" cy="20" r="5.5" fill="{color}"/>')
        cursor_x += 18
    return "\n".join(lines)


def render_header_meta(data: Dict[str, object], style: Dict[str, object], width: float) -> str:
    meta_left = normalize_text(data.get("meta_left", ""))
    meta_center = normalize_text(data.get("meta_center", ""))
    meta_right = normalize_text(data.get("meta_right", ""))
    if not any([meta_left, meta_center, meta_right]):
        return ""
    fill = str(data.get("meta_fill", style_value(style, "text_muted")))
    size = to_float(data.get("meta_size", 11))
    lines = []
    if meta_left:
        lines.append(f'  <text x="28" y="24" font-size="{size}" font-weight="600" fill="{fill}">{meta_left}</text>')
    if meta_center:
        lines.append(f'  <text x="{width / 2}" y="24" text-anchor="middle" font-size="{size}" font-weight="600" fill="{fill}">{meta_center}</text>')
    if meta_right:
        lines.append(f'  <text x="{width - 28}" y="24" text-anchor="end" font-size="{size}" font-weight="600" fill="{fill}">{meta_right}</text>')
    return "\n".join(lines)


def render_style_signature(style_index: int, data: Dict[str, object], width: float) -> str:
    """Expose each engineering style's domain evidence as a compact visual fingerprint."""

    if style_index not in {9, 10, 11, 12}:
        return ""
    badge_width = 176.0
    badge_height = 34.0
    x = width - 48 - badge_width
    y = 22.0
    if style_index == 9:
        level = str(data.get("c4_level", "review")).upper()
        state = str(data.get("review_state", "REVIEW READY")).upper()
        top_raw = f"C4 · {level} VIEW"
        top_text, top_size = fit_single_line_text(top_raw, badge_width - 46, preferred=8.5, minimum=6.2)
        state_text, state_size = fit_single_line_text(state, badge_width - 46, preferred=8, minimum=6.2)
        return "\n".join(
            [
            '  <g id="style-signature" data-graph-role="decoration" data-style-signature="c4-review-board">',
                f'    <rect x="{x}" y="{y}" width="{badge_width}" height="{badge_height}" rx="7" fill="#fffdf7" stroke="#8c7d68" stroke-width="1.2" stroke-dasharray="6 4"/>',
                f'    <path d="M {x + 12} {y + 17} l 5 5 9 -11" fill="none" stroke="#365f56" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
                f'    <text data-full-text="{normalize_attribute(top_raw)}" x="{x + 34}" y="{y + 14}" font-size="{top_size}" font-weight="800" fill="#8a6f43">{normalize_text(top_text)}</text>',
                f'    <text data-full-text="{normalize_attribute(state)}" x="{x + 34}" y="{y + 27}" font-size="{state_size}" font-weight="700" fill="#5f665f">{normalize_text(state_text)}</text>',
                "  </g>",
            ]
        )
    if style_index == 10:
        platform = str(data.get("platform_profile", "cloud")).upper()
        mode = str(data.get("deployment_mode", "DEPLOYMENT MAP")).upper()
        regions = sum(
            1
            for container in data.get("containers", [])
            if isinstance(container, Mapping) and container.get("deployment_kind") == "region"
        )
        top_raw = f"{platform} · {regions} REGIONS"
        top_text, top_size = fit_single_line_text(top_raw, badge_width - 55, preferred=8.5, minimum=6.2)
        mode_text, mode_size = fit_single_line_text(mode, badge_width - 55, preferred=8, minimum=6.2)
        return "\n".join(
            [
            '  <g id="style-signature" data-graph-role="decoration" data-style-signature="cloud-ownership-map">',
                f'    <rect x="{x}" y="{y}" width="{badge_width}" height="{badge_height}" rx="9" fill="#ffffff" fill-opacity="0.82" stroke="#7fa3c2" stroke-width="1.1"/>',
                f'    <rect x="{x + 11}" y="{y + 9}" width="14" height="14" rx="4" fill="#dbeafe" stroke="#2563eb" stroke-width="1"/>',
                f'    <rect x="{x + 20}" y="{y + 13}" width="14" height="14" rx="4" fill="#dcfce7" stroke="#059669" stroke-width="1"/>',
                f'    <text data-full-text="{normalize_attribute(top_raw)}" x="{x + 43}" y="{y + 14}" font-size="{top_size}" font-weight="800" fill="#315d7e">{normalize_text(top_text)}</text>',
                f'    <text data-full-text="{normalize_attribute(mode)}" x="{x + 43}" y="{y + 27}" font-size="{mode_size}" font-weight="700" fill="#52718d">{normalize_text(mode_text)}</text>',
                "  </g>",
            ]
        )
    if style_index == 11:
        topics = data.get("topics", [])
        line_count = len(topics) if isinstance(topics, list) else 0
        line_code = str(data.get("line_code", "EVENT METRO")).upper()
        signature_width = 226.0
        signature_x = width - 48 - signature_width
        line_code_text, line_code_size = fit_single_line_text(
            line_code, signature_width - 58, preferred=8.5, minimum=6.2
        )
        detail_raw = f"{line_count} TOPIC LINES · DECLARED STOPS"
        detail_text, detail_size = fit_single_line_text(
            detail_raw, signature_width - 58, preferred=8, minimum=6.2
        )
        return "\n".join(
            [
            '  <g id="style-signature" data-graph-role="decoration" data-style-signature="event-metro-map">',
                f'    <rect x="{signature_x}" y="{y}" width="{signature_width}" height="{badge_height}" rx="7" fill="#17213c" stroke="#514c43" stroke-width="1"/>',
                f'    <line x1="{signature_x + 12}" y1="{y + 17}" x2="{signature_x + 36}" y2="{y + 17}" stroke="#e4475b" stroke-width="3"/>',
                f'    <circle cx="{signature_x + 18}" cy="{y + 17}" r="4" fill="#fbf7ee" stroke="#e4475b" stroke-width="2"/>',
                f'    <circle cx="{signature_x + 31}" cy="{y + 17}" r="4" fill="#fbf7ee" stroke="#e4475b" stroke-width="2"/>',
                f'    <text data-full-text="{normalize_attribute(line_code)}" x="{signature_x + 46}" y="{y + 14}" font-size="{line_code_size}" font-weight="800" fill="#ffffff">{normalize_text(line_code_text)}</text>',
                f'    <text data-full-text="{normalize_attribute(detail_raw)}" x="{signature_x + 46}" y="{y + 27}" font-size="{detail_size}" font-weight="700" fill="#f3d5d9">{normalize_text(detail_text)}</text>',
                "  </g>",
            ]
        )

    services = [
        node
        for node in data.get("nodes", [])
        if isinstance(node, Mapping) and node.get("ops_role") == "service"
    ]
    rank = {"unknown": 0, "ok": 1, "warn": 2, "critical": 3}
    worst = max(
        (str(node.get("status", "unknown")) for node in services),
        key=lambda item: rank.get(item, 0),
        default="unknown",
    )
    window = str(data.get("observation_window", ""))
    if not window:
        for node in services:
            signals = node.get("signals")
            if isinstance(signals, Mapping):
                first_signal = next((value for value in signals.values() if isinstance(value, Mapping)), None)
                if first_signal:
                    window = str(first_signal.get("window", ""))
                    break
    status_color = {"ok": "#22c55e", "warn": "#f59e0b", "critical": "#f43f5e", "unknown": "#64748b"}.get(worst, "#64748b")
    top_raw = f'LIVE · {window.upper() or "WINDOW"}'
    detail_raw = f"{worst.upper()} · CORRELATED TRACE"
    top_text, top_size = fit_single_line_text(top_raw, badge_width - 70, preferred=8.5, minimum=6.2)
    detail_text, detail_size = fit_single_line_text(detail_raw, badge_width - 70, preferred=8, minimum=6.2)
    return "\n".join(
        [
            '  <g id="style-signature" data-graph-role="decoration" data-style-signature="ops-live-investigation">',
            f'    <rect x="{x}" y="{y}" width="{badge_width}" height="{badge_height}" rx="7" fill="#0d1b2a" stroke="#29435d" stroke-width="1.1"/>',
            f'    <circle cx="{x + 15}" cy="{y + 17}" r="4" fill="{status_color}"/>',
            f'    <path d="M {x + 25} {y + 18} h 5 l 3 -6 5 12 4 -8 h 7" fill="none" stroke="#38bdf8" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>',
            f'    <text data-full-text="{normalize_attribute(top_raw)}" x="{x + 58}" y="{y + 14}" font-size="{top_size}" font-weight="800" fill="#eff6ff">{normalize_text(top_text)}</text>',
            f'    <text data-full-text="{normalize_attribute(detail_raw)}" x="{x + 58}" y="{y + 27}" font-size="{detail_size}" font-weight="700" fill="{status_color}">{normalize_text(detail_text)}</text>',
            "  </g>",
        ]
    )


def render_blueprint_title_block(
    data: Dict[str, object],
    style: Dict[str, object],
    style_index: int,
    width: float,
    height: float,
) -> Tuple[str, Optional[Bounds]]:
    if style_index != 3:
        return "", None
    block = data.get("blueprint_title_block")
    if not block:
        return "", None
    block_width = to_float(block.get("width", 256))
    block_height = to_float(block.get("height", 92))
    x = to_float(block.get("x", width - block_width - 28))
    y = to_float(block.get("y", height - block_height - 18))
    title = normalize_text(block.get("title", data.get("title", "")))
    subtitle = normalize_text(block.get("subtitle", "SYSTEM ARCHITECTURE"))
    left_caption = normalize_text(block.get("left_caption", "REV: 1.0"))
    center_caption = normalize_text(block.get("center_caption", "AUTO-GENERATED"))
    right_caption = normalize_text(block.get("right_caption", "DWG: ARCH-001"))
    stroke = str(block.get("stroke", style_value(style, "section_stroke")))
    fill = str(block.get("fill", "#0b3552"))
    title_fill = str(block.get("title_fill", style_value(style, "text_primary")))
    sub_fill = str(block.get("subtitle_fill", style_value(style, "section_label_fill")))
    muted_fill = str(block.get("muted_fill", style_value(style, "text_muted")))
    footer_top = y + 54
    column_width = block_width / 3
    caption_width = max(24.0, column_width - 12)

    def caption_size(value: str) -> float:
        estimated = geometry.estimate_text_width(value, 9.5)
        return round(max(6.0, min(9.5, 9.5 * caption_width / max(estimated, 1.0))), 2)

    caption_y = min(y + block_height - 4, footer_top + max(12.0, block_height - 54) / 2 + 3)
    captions = (
        (left_caption, x + column_width / 2, muted_fill),
        (center_caption, x + block_width / 2, sub_fill),
        (right_caption, x + block_width - column_width / 2, muted_fill),
    )
    lines = [
        f'  <rect x="{x}" y="{y}" width="{block_width}" height="{block_height}" fill="{fill}" stroke="{stroke}" stroke-width="1.2"/>',
        f'  <line x1="{x}" y1="{y + 18}" x2="{x + block_width}" y2="{y + 18}" stroke="{stroke}" stroke-width="1"/>',
        f'  <line x1="{x}" y1="{y + 54}" x2="{x + block_width}" y2="{y + 54}" stroke="{stroke}" stroke-width="1"/>',
        f'  <line x1="{x + column_width}" y1="{footer_top}" x2="{x + column_width}" y2="{y + block_height}" stroke="{stroke}" stroke-width="0.7"/>',
        f'  <line x1="{x + 2 * column_width}" y1="{footer_top}" x2="{x + 2 * column_width}" y2="{y + block_height}" stroke="{stroke}" stroke-width="0.7"/>',
        f'  <text x="{x + block_width / 2}" y="{y + 13}" text-anchor="middle" font-size="10" font-weight="600" fill="{muted_fill}">{subtitle}</text>',
        f'  <text x="{x + block_width / 2}" y="{y + 42}" text-anchor="middle" font-size="18" font-weight="700" fill="{title_fill}">{title}</text>',
    ]
    lines.extend(
        f'  <text x="{caption_x}" y="{caption_y}" text-anchor="middle" font-size="{caption_size(caption)}" '
        f'font-weight="600" fill="{caption_fill}">{caption}</text>'
        for caption, caption_x, caption_fill in captions
    )
    return "\n".join(lines), rectangle_bounds(x - 6, y - 6, block_width + 12, block_height + 12)


def infer_shape(kind: str) -> str:
    mapping = {
        "rect": "rect",
        "double_rect": "rect",
        "cylinder": "rect",
        "document": "rect",
        "folder": "rect",
        "terminal": "rect",
        "hexagon": "rect",
        "circle_cluster": "cluster",
        "user_avatar": "rect",
        "bot": "rect",
        "speech": "rect",
        "icon_box": "rect",
    }
    return mapping.get(kind, "rect")


def node_bounds(data: Dict[str, object]) -> Bounds:
    kind = str(data.get("kind", data.get("shape", "rect")))
    x = to_float(data.get("x"))
    y = to_float(data.get("y"))
    if kind == "circle":
        r = to_float(data.get("r", 50))
        return (x - r, y - r, x + r, y + r)
    width = to_float(data.get("width", 180))
    height = to_float(data.get("height", 76))
    return (x, y, x + width, y + height)


def normalize_node(node_data: Dict[str, object], fallback_id: str) -> Node:
    kind = str(node_data.get("kind", node_data.get("shape", "rect")))
    bounds = node_bounds(node_data)
    left, top, right, bottom = bounds
    return Node(
        node_id=str(node_data.get("id", fallback_id)),
        kind=kind,
        shape=infer_shape(kind),
        data=node_data,
        bounds=bounds,
        cx=(left + right) / 2,
        cy=(top + bottom) / 2,
    )


def anchor_on_side(node: Node, side: str, offset: float = 0.0) -> Point:
    left, top, right, bottom = node.bounds
    cx, cy = node.cx, node.cy
    side = side.lower()
    safe_x = min(max(cx + offset, left + 12), right - 12)
    safe_y = min(max(cy + offset, top + 12), bottom - 12)
    if side == "left":
        return (left, safe_y)
    if side == "right":
        return (right, safe_y)
    if side == "top":
        return (safe_x, top)
    if side == "bottom":
        return (safe_x, bottom)
    if side == "top-left":
        return (left, top)
    if side == "top-right":
        return (right, top)
    if side == "bottom-left":
        return (left, bottom)
    if side == "bottom-right":
        return (right, bottom)
    return (cx, cy)


def anchor_point(node: Node, toward: Point, port: Optional[str] = None, offset: float = 0.0) -> Point:
    if port:
        return anchor_on_side(node, port, offset)
    left, top, right, bottom = node.bounds
    dx = toward[0] - node.cx
    dy = toward[1] - node.cy
    width = right - left
    height = bottom - top
    if abs(dx) * height >= abs(dy) * width:
        return (right, node.cy) if dx >= 0 else (left, node.cy)
    return (node.cx, bottom) if dy >= 0 else (node.cx, top)


def expand_bounds(bounds: Bounds, padding: float) -> Bounds:
    left, top, right, bottom = bounds
    return (left - padding, top - padding, right + padding, bottom + padding)


def segment_hits_bounds(p1: Point, p2: Point, bounds: Bounds) -> bool:
    x1, y1 = p1
    x2, y2 = p2
    left, top, right, bottom = bounds
    eps = 1e-6

    if abs(y1 - y2) < eps:
        y = y1
        if not (top + eps < y < bottom - eps):
            return False
        seg_left = min(x1, x2)
        seg_right = max(x1, x2)
        overlap_left = max(seg_left, left)
        overlap_right = min(seg_right, right)
        if overlap_right - overlap_left <= eps:
            return False
        if abs(overlap_left - x1) < eps and abs(overlap_right - x1) < eps:
            return False
        if abs(overlap_left - x2) < eps and abs(overlap_right - x2) < eps:
            return False
        return True

    if abs(x1 - x2) < eps:
        x = x1
        if not (left + eps < x < right - eps):
            return False
        seg_top = min(y1, y2)
        seg_bottom = max(y1, y2)
        overlap_top = max(seg_top, top)
        overlap_bottom = min(seg_bottom, bottom)
        if overlap_bottom - overlap_top <= eps:
            return False
        if abs(overlap_top - y1) < eps and abs(overlap_bottom - y1) < eps:
            return False
        if abs(overlap_top - y2) < eps and abs(overlap_bottom - y2) < eps:
            return False
        return True

    return False


def segment_axis(p1: Point, p2: Point) -> str:
    if abs(p1[1] - p2[1]) < 1e-6:
        return "horizontal"
    if abs(p1[0] - p2[0]) < 1e-6:
        return "vertical"
    return "other"


def port_axis(port: Optional[str]) -> Optional[str]:
    if not port:
        return None
    port = port.lower()
    if port in {"left", "right"}:
        return "horizontal"
    if port in {"top", "bottom"}:
        return "vertical"
    return None


def offset_point(point: Point, port: Optional[str], distance: float) -> Point:
    if not port:
        return point
    x, y = point
    port = port.lower()
    if port == "left":
        return (x - distance, y)
    if port == "right":
        return (x + distance, y)
    if port == "top":
        return (x, y - distance)
    if port == "bottom":
        return (x, y + distance)
    return point


def clear_port_point(
    endpoint: Point,
    port: Optional[str],
    desired_distance: float,
    obstacles: Sequence[Bounds],
    canvas_bounds: Optional[Bounds],
) -> Point:
    """Choose the longest safe straight lead from a node port."""

    distances = [desired_distance * fraction for fraction in (1.0, 0.75, 0.5, 0.35, 0.2, 0.0)]
    for distance in distances:
        candidate = offset_point(endpoint, port, distance)
        if canvas_bounds is not None and not geometry.point_in_bounds(candidate, canvas_bounds):
            continue
        if any(
            geometry.point_in_bounds(candidate, obstacle, interior=True)
            or segment_hits_bounds(endpoint, candidate, obstacle)
            for obstacle in obstacles
        ):
            continue
        return candidate
    return endpoint


def route_length(points: Sequence[Point]) -> float:
    return sum(abs(x1 - x2) + abs(y1 - y2) for (x1, y1), (x2, y2) in zip(points, points[1:]))


def route_uses_lane(points: Sequence[Point], value: float, axis: str, tolerance: float = 1.0) -> bool:
    if axis == "x":
        return any(abs(x - value) <= tolerance for x, _ in points)
    return any(abs(y - value) <= tolerance for _, y in points)


def collision_count(points: Sequence[Point], obstacles: Sequence[Bounds]) -> int:
    """Count how many (segment, obstacle) pairs collide."""
    return sum(
        1
        for p1, p2 in zip(points, points[1:])
        for obs in obstacles
        if segment_hits_bounds(p1, p2, obs)
    )


def route_is_orthogonal(points: Sequence[Point]) -> bool:
    return all(segment_axis(p1, p2) != "other" for p1, p2 in zip(points, points[1:]))


def route_crossing_count(points: Sequence[Point], existing_routes: Sequence[Sequence[Point]]) -> int:
    return geometry.route_crossing_count(points, existing_routes)


def route_score(
    points: Sequence[Point],
    hint_x: Sequence[float],
    hint_y: Sequence[float],
    source_port: Optional[str],
    target_port: Optional[str],
    existing_routes: Sequence[Sequence[Point]] = (),
) -> float:
    length = route_length(points)
    bends = max(0, len(points) - 2)
    score = length + bends * 22
    if len(points) >= 2 and source_port:
        first_axis = segment_axis(points[0], points[1])
        if first_axis != port_axis(source_port):
            score += 180
    if len(points) >= 2 and target_port:
        last_axis = segment_axis(points[-2], points[-1])
        if last_axis != port_axis(target_port):
            score += 180
    for lane in hint_x:
        score -= 28 if route_uses_lane(points, lane, "x") else 0
    for lane in hint_y:
        score -= 28 if route_uses_lane(points, lane, "y") else 0
    interactions = geometry.route_interactions(points, existing_routes)
    score += len(interactions.crossings) * 640
    score += interactions.overlap_count * 900 + interactions.overlap_length * 18
    return score


def simplify_points(points: Sequence[Point], protected: Sequence[Point] = ()) -> List[Point]:
    protected_points = {(round(point[0], 2), round(point[1], 2)) for point in protected}
    simplified: List[Point] = []
    for x, y in points:
        pt = (round(x, 2), round(y, 2))
        if simplified and pt == simplified[-1]:
            continue
        simplified.append(pt)

    collapsed: List[Point] = []
    for point in simplified:
        if len(collapsed) < 2:
            collapsed.append(point)
            continue
        x0, y0 = collapsed[-2]
        x1, y1 = collapsed[-1]
        x2, y2 = point
        # A same-axis reversal adds length and can create sub-pixel hairpins
        # when two port-clearance leads nearly meet.  The straight segment
        # from the first to the third point covers the same safe corridor, so
        # collapse both monotonic and reversing collinear triples unless the
        # middle point is an explicit user waypoint.
        collinear_vertical = x0 == x1 == x2
        collinear_horizontal = y0 == y1 == y2
        if (collinear_vertical or collinear_horizontal) and (x1, y1) not in protected_points:
            collapsed[-1] = point
        else:
            collapsed.append(point)
    return collapsed


def route_collides(points: Sequence[Point], obstacles: Sequence[Bounds]) -> bool:
    return collision_count(points, obstacles) > 0


def visibility_grid_route(
    start: Point,
    end: Point,
    obstacles: Sequence[Bounds],
    *,
    canvas_bounds: Optional[Bounds],
    hint_x: Sequence[float],
    hint_y: Sequence[float],
    existing_routes: Sequence[Sequence[Point]],
) -> Optional[List[Point]]:
    """Find a deterministic rectilinear route on an obstacle visibility grid."""

    if start == end:
        return [start]
    if canvas_bounds is None:
        all_x = [start[0], end[0], *[value for bounds in obstacles for value in (bounds[0], bounds[2])]]
        all_y = [start[1], end[1], *[value for bounds in obstacles for value in (bounds[1], bounds[3])]]
        canvas_bounds = (min(all_x) - 64, min(all_y) - 64, max(all_x) + 64, max(all_y) + 64)
    canvas_left, canvas_top, canvas_right, canvas_bottom = canvas_bounds
    inset = 4.0

    x_values = {
        round(start[0], 2),
        round(end[0], 2),
        round((start[0] + end[0]) / 2, 2),
        round(canvas_left + inset, 2),
        round(canvas_right - inset, 2),
        *[round(value, 2) for value in hint_x],
    }
    y_values = {
        round(start[1], 2),
        round(end[1], 2),
        round((start[1] + end[1]) / 2, 2),
        round(canvas_top + inset, 2),
        round(canvas_bottom - inset, 2),
        *[round(value, 2) for value in hint_y],
    }
    for left, top, right, bottom in obstacles:
        x_values.update((round(left, 2), round(right, 2)))
        y_values.update((round(top, 2), round(bottom, 2)))
    for route in existing_routes:
        for x, y in route:
            for delta in (-10.0, 0.0, 10.0):
                x_values.add(round(x + delta, 2))
                y_values.add(round(y + delta, 2))

    x_values = {value for value in x_values if canvas_left - 1e-6 <= value <= canvas_right + 1e-6}
    y_values = {value for value in y_values if canvas_top - 1e-6 <= value <= canvas_bottom + 1e-6}
    points = {
        (x, y)
        for x in sorted(x_values)
        for y in sorted(y_values)
        if not any(geometry.point_in_bounds((x, y), bounds, interior=True) for bounds in obstacles)
    }
    points.update((start, end))

    adjacency: Dict[Point, List[Point]] = {point: [] for point in points}
    by_y: Dict[float, List[Point]] = {}
    by_x: Dict[float, List[Point]] = {}
    for point in points:
        by_y.setdefault(point[1], []).append(point)
        by_x.setdefault(point[0], []).append(point)

    def connect(line: Sequence[Point], sort_key: int) -> None:
        ordered = sorted(line, key=lambda point: (point[sort_key], point[1 - sort_key]))
        for first, second in zip(ordered, ordered[1:]):
            if any(segment_hits_bounds(first, second, obstacle) for obstacle in obstacles):
                continue
            adjacency[first].append(second)
            adjacency[second].append(first)

    for line in by_y.values():
        connect(line, 0)
    for line in by_x.values():
        connect(line, 1)

    # State includes the incoming axis so bends have an explicit cost.
    start_state = (start, "")
    distances: Dict[Tuple[Point, str], float] = {start_state: 0.0}
    paths: Dict[Tuple[Point, str], Tuple[Point, ...]] = {start_state: (start,)}
    queue: List[Tuple[float, Tuple[Point, ...], Point, str]] = [(0.0, (start,), start, "")]
    best_end: Optional[Tuple[float, Tuple[Point, ...]]] = None

    while queue:
        cost, path_key, point, incoming = heapq.heappop(queue)
        state = (point, incoming)
        if cost > distances.get(state, float("inf")) + 1e-6 or path_key != paths.get(state):
            continue
        if point == end:
            best_end = (cost, path_key)
            break
        for neighbor in sorted(adjacency.get(point, [])):
            axis = segment_axis(point, neighbor)
            if axis == "other":
                continue
            distance = abs(neighbor[0] - point[0]) + abs(neighbor[1] - point[1])
            segment_route = [point, neighbor]
            interactions = geometry.route_interactions(segment_route, existing_routes)
            extra = distance
            if incoming and incoming != axis:
                extra += 22.0
            extra += len(interactions.crossings) * 640.0
            extra += interactions.overlap_count * 10000.0 + interactions.overlap_length * 30.0
            if axis == "vertical" and any(abs(point[0] - value) <= 1 for value in hint_x):
                extra -= min(18.0, distance * 0.08)
            if axis == "horizontal" and any(abs(point[1] - value) <= 1 for value in hint_y):
                extra -= min(18.0, distance * 0.08)
            next_state = (neighbor, axis)
            next_cost = cost + extra
            next_path = (*path_key, neighbor)
            old_cost = distances.get(next_state, float("inf"))
            old_path = paths.get(next_state)
            if next_cost < old_cost - 1e-6 or (
                abs(next_cost - old_cost) <= 1e-6 and (old_path is None or next_path < old_path)
            ):
                distances[next_state] = next_cost
                paths[next_state] = next_path
                heapq.heappush(queue, (next_cost, next_path, neighbor, axis))

    if best_end is None:
        return None
    return simplify_points(list(best_end[1]))


def build_orthogonal_route(
    start: Point,
    end: Point,
    obstacles: Sequence[Bounds],
    arrow_data: Dict[str, object],
    *,
    canvas_bounds: Optional[Bounds] = None,
    existing_routes: Sequence[Sequence[Point]] = (),
) -> List[Point]:
    raw_waypoints = arrow_data.get("route_points") or []
    if raw_waypoints:
        waypoints: List[Point] = []
        for index, raw_point in enumerate(raw_waypoints):
            if not isinstance(raw_point, (list, tuple)) or len(raw_point) != 2:
                raise ValueError(f"route waypoint {index + 1} must be [x, y]")
            waypoint = (to_float(raw_point[0]), to_float(raw_point[1]))
            if any(geometry.point_in_bounds(waypoint, bounds, interior=True) for bounds in obstacles):
                raise ValueError(f"route waypoint {index + 1} intersects an obstacle: {waypoint}")
            if canvas_bounds is not None and not geometry.point_in_bounds(waypoint, canvas_bounds):
                raise ValueError(f"route waypoint {index + 1} is outside the canvas: {waypoint}")
            waypoints.append(waypoint)

        mandatory = [start, *waypoints, end]
        assembled: List[Point] = []
        for index, (segment_start, segment_end) in enumerate(zip(mandatory, mandatory[1:])):
            segment_data = dict(arrow_data)
            segment_data.pop("route_points", None)
            if index > 0:
                segment_data.pop("source_port", None)
            if index < len(mandatory) - 2:
                segment_data.pop("target_port", None)
            occupied_routes: List[Sequence[Point]] = list(existing_routes)
            if len(assembled) >= 2:
                occupied_routes.append(assembled)
            segment_route = build_orthogonal_route(
                segment_start,
                segment_end,
                obstacles,
                segment_data,
                canvas_bounds=canvas_bounds,
                existing_routes=occupied_routes,
            )
            if assembled:
                assembled.extend(segment_route[1:])
            else:
                assembled.extend(segment_route)
        result = simplify_points(assembled, waypoints)
        if not route_is_orthogonal(result):
            raise ValueError("explicit route waypoints could not be connected orthogonally")
        if any(waypoint not in result for waypoint in waypoints):
            raise ValueError("explicit route waypoint was not preserved")
        if route_collides(result, obstacles):
            raise ValueError("explicit route waypoints cannot be connected without crossing an obstacle")
        return result

    sx, sy = start
    ex, ey = end
    routing_padding = to_float(arrow_data.get("routing_padding", 24))
    port_clearance = to_float(arrow_data.get("port_clearance", max(18, routing_padding * 0.85)))
    source_port = str(arrow_data.get("source_port", "")).strip().lower() or None
    target_port = str(arrow_data.get("target_port", "")).strip().lower() or None
    inner_start = clear_port_point(start, source_port, port_clearance, obstacles, canvas_bounds)
    inner_end = clear_port_point(end, target_port, port_clearance, obstacles, canvas_bounds)
    ssx, ssy = inner_start
    eex, eey = inner_end
    expanded: List[Bounds] = []
    for bounds in obstacles:
        padded = expand_bounds(bounds, routing_padding)
        # Mandatory waypoints are exact. If one sits inside a clearance halo,
        # keep the real obstacle hard while locally relaxing only that halo.
        if any(
            geometry.point_in_bounds(point, padded, interior=True)
            and not geometry.point_in_bounds(point, bounds, interior=True)
            for point in (start, end, inner_start, inner_end)
        ):
            expanded.append(bounds)
        else:
            expanded.append(padded)
    hint_x = [to_float(value) for value in arrow_data.get("corridor_x", [])]
    hint_y = [to_float(value) for value in arrow_data.get("corridor_y", [])]
    lane_x = sorted({ssx, eex, round((ssx + eex) / 2, 2), *hint_x, *[b[0] for b in expanded], *[b[2] for b in expanded]})
    lane_y = sorted({ssy, eey, round((ssy + eey) / 2, 2), *hint_y, *[b[1] for b in expanded], *[b[3] for b in expanded]})
    if expanded:
        left_rail = min(b[0] for b in expanded) - 24
        right_rail = max(b[2] for b in expanded) + 24
        top_rail = min(b[1] for b in expanded) - 24
        bottom_rail = max(b[3] for b in expanded) + 24
    else:
        left_rail = min(ssx, eex) - 48
        right_rail = max(ssx, eex) + 48
        top_rail = min(ssy, eey) - 48
        bottom_rail = max(ssy, eey) + 48

    candidates = [
        [start, inner_start, inner_end, end],
        [start, inner_start, (eex, ssy), inner_end, end],
        [start, inner_start, (ssx, eey), inner_end, end],
        [start, inner_start, ((ssx + eex) / 2, ssy), ((ssx + eex) / 2, eey), inner_end, end],
        [start, inner_start, (ssx, (ssy + eey) / 2), (eex, (ssy + eey) / 2), inner_end, end],
        [start, inner_start, (left_rail, ssy), (left_rail, eey), inner_end, end],
        [start, inner_start, (right_rail, ssy), (right_rail, eey), inner_end, end],
        [start, inner_start, (ssx, top_rail), (eex, top_rail), inner_end, end],
        [start, inner_start, (ssx, bottom_rail), (eex, bottom_rail), inner_end, end],
    ]
    for x in lane_x:
        candidates.append([start, inner_start, (x, ssy), (x, eey), inner_end, end])
    for y in lane_y:
        candidates.append([start, inner_start, (ssx, y), (eex, y), inner_end, end])
    for x in hint_x:
        for y in hint_y:
            candidates.append([start, inner_start, (x, ssy), (x, y), (eex, y), inner_end, end])

    visibility = visibility_grid_route(
        inner_start,
        inner_end,
        expanded,
        canvas_bounds=canvas_bounds,
        hint_x=hint_x,
        hint_y=hint_y,
        existing_routes=existing_routes,
    )
    if visibility:
        candidates.append([start, *visibility, end])

    best_route: Optional[List[Point]] = None
    best_score = float("inf")
    for candidate in candidates:
        simplified = simplify_points(candidate)
        if not route_is_orthogonal(simplified):
            continue
        if canvas_bounds is not None and not geometry.route_inside_canvas(simplified, canvas_bounds):
            continue
        coll = collision_count(simplified, expanded)
        score = route_score(simplified, hint_x, hint_y, source_port, target_port, existing_routes)
        if coll == 0:
            if score < best_score or (abs(score - best_score) < 1e-6 and (best_route is None or tuple(simplified) < tuple(best_route))):
                best_score = score
                best_route = simplified

    if best_route is not None:
        return best_route
    raise ValueError("no collision-free orthogonal route satisfies the current constraints")


def choose_label_position(points: Sequence[Point]) -> Point:
    segments = list(zip(points, points[1:]))
    if not segments:
        return points[0]
    best = max(segments, key=lambda seg: abs(seg[0][0] - seg[1][0]) + abs(seg[0][1] - seg[1][1]))
    return ((best[0][0] + best[1][0]) / 2, (best[0][1] + best[1][1]) / 2)


def color_for_flow(style: Dict[str, object], arrow_data: Dict[str, object]) -> str:
    if arrow_data.get("color"):
        return str(arrow_data["color"])
    flow = FLOW_ALIASES.get(str(arrow_data.get("flow", "control")).lower(), "control")
    return str(style_value(style, "arrow_colors")[flow])


def marker_for_color(style: Dict[str, object], color: str, arrow_data: Dict[str, object]) -> str:
    if arrow_data.get("marker"):
        return f"url(#{arrow_data['marker']})"
    colors = style_value(style, "arrow_colors")
    for name, token in colors.items():
        if token == color:
            return f"url(#{MARKER_IDS.get(name, 'arrowA')})"
    return "url(#arrowA)"


def render_label_badge(x: float, y: float, text: str, style: Dict[str, object], label_style: str = "offset") -> str:
    width = max(36.0, geometry.estimate_text_width(text, 12) + 14)
    parts: List[str] = []
    if label_style == "badge":
        bg = style_value(style, "arrow_label_bg")
        opacity = style_value(style, "arrow_label_opacity")
        parts.append(f'  <rect x="{round(x - width / 2, 2)}" y="{round(y - 10, 2)}" width="{width}" height="20" rx="6" fill="{bg}" opacity="{opacity}"/>')
    parts.append(f'  <text x="{round(x, 2)}" y="{round(y + 4, 2)}" text-anchor="middle" class="arrow-label">{normalize_text(text)}</text>')
    return "\n".join(parts)


def rectangle_bounds(x: float, y: float, width: float, height: float) -> Bounds:
    return (x, y, x + width, y + height)


def bounds_intersect(a: Bounds, b: Bounds, padding: float = 0.0) -> bool:
    ax1, ay1, ax2, ay2 = a
    bx1, by1, bx2, by2 = b
    return not (
        ax2 + padding <= bx1
        or bx2 + padding <= ax1
        or ay2 + padding <= by1
        or by2 + padding <= ay1
    )


def estimate_label_bounds(x: float, y: float, text: str) -> Bounds:
    width = max(36.0, geometry.estimate_text_width(text, 12) + 14)
    return rectangle_bounds(x - width / 2, y - 10, width, 20)


def render_dual_label_badge(x: float, y: float, primary: str, secondary: str, style: Dict[str, object]) -> str:
    width = max(
        42.0,
        geometry.estimate_text_width(primary, 11.5) + 16,
        geometry.estimate_text_width(secondary, 8.5) + 16,
    )
    bg = style_value(style, "arrow_label_bg")
    opacity = style_value(style, "arrow_label_opacity")
    return "\n".join(
        [
            f'  <rect x="{round(x - width / 2, 2)}" y="{round(y - 16, 2)}" width="{round(width, 2)}" height="32" rx="7" fill="{bg}" opacity="{opacity}"/>',
            f'  <text x="{round(x, 2)}" y="{round(y - 1, 2)}" text-anchor="middle" class="arrow-label" font-size="11.5">{normalize_text(primary)}</text>',
            f'  <text x="{round(x, 2)}" y="{round(y + 11, 2)}" text-anchor="middle" font-size="8.5" font-weight="800" fill="{style_value(style, "type_label_fill")}">[{normalize_text(secondary)}]</text>',
        ]
    )


def estimate_dual_label_bounds(x: float, y: float, primary: str, secondary: str) -> Bounds:
    width = max(
        42.0,
        geometry.estimate_text_width(primary, 11.5) + 16,
        geometry.estimate_text_width(secondary, 8.5) + 16,
    )
    return rectangle_bounds(x - width / 2, y - 16, width, 32)


def section_header_text(container: Dict[str, object], style: Dict[str, object]) -> str:
    if container.get("header_text"):
        text = str(container.get("header_text", ""))
    else:
        label = str(container.get("label", ""))
        prefix = str(container.get("header_prefix", "")).strip()
        separator = str(container.get("header_separator", " // " if prefix else ""))
        text = f"{prefix}{separator}{label}" if prefix else label
    if style_value(style, "section_upper") and not container.get("preserve_case"):
        text = text.upper()
    return text


def deterministic_jitter(seed: object, element_id: object, pass_index: int, amplitude: float = 1.5) -> float:
    """Return stable decorative jitter without relying on process-randomized hash()."""

    digest = hashlib.sha256(f"{seed}:{element_id}:{pass_index}".encode("utf-8")).digest()
    unit = int.from_bytes(digest[:4], "big") / float(2**32 - 1)
    return round((unit * 2.0 - 1.0) * amplitude, 3)


def render_section(container: Dict[str, object], style: Dict[str, object]) -> str:
    x = to_float(container["x"])
    y = to_float(container["y"])
    width = to_float(container["width"])
    height = to_float(container["height"])
    rx = to_float(container.get("rx", 16 if style_value(style, "name") != "Notion Clean" else 4))
    fill = str(container.get("fill", style_value(style, "section_fill")))
    stroke = str(container.get("stroke", style_value(style, "section_stroke")))
    dash = str(container.get("stroke_dasharray", style_value(style, "section_dash")))
    label = section_header_text(container, style)
    subtitle = str(container.get("subtitle", ""))
    side_label = str(container.get("side_label", "")).strip()
    side_label_fill = str(container.get("side_label_fill", style_value(style, "text_secondary")))
    side_label_size = to_float(container.get("side_label_size", 14))
    side_label_weight = str(container.get("side_label_weight", "600"))
    side_label_anchor = str(container.get("side_label_anchor", "end"))
    lines = [f'  <rect data-graph-role="container" x="{x}" y="{y}" width="{width}" height="{height}" rx="{rx}" fill="{fill}" stroke="{stroke}" stroke-width="1.4"']
    if dash:
        lines[-1] += f' stroke-dasharray="{dash}"'
    lines[-1] += "/>"
    treatment = str(style.get("canvas_treatment", ""))
    if treatment == "review":
        seed = container.get("_rough_seed", 0)
        element_id = container.get("id", label or "container")
        dx = deterministic_jitter(seed, element_id, 1)
        dy = deterministic_jitter(seed, element_id, 2)
        lines.append(
            f'  <rect data-graph-role="decoration" x="{x + 2 + dx}" y="{y + 2 + dy}" '
            f'width="{width - 4}" height="{height - 4}" rx="{max(3, rx - 1)}" fill="none" '
            f'stroke="{stroke}" stroke-width="0.9" stroke-dasharray="13 7" opacity="0.42"/>'
        )
    elif treatment == "cloud" and container.get("deployment_kind"):
        deployment_kind = str(container.get("deployment_kind", ""))
        badge = normalize_text(deployment_kind.upper())
        badge_width = max(54.0, geometry.estimate_text_width(str(container.get("deployment_kind", "")), 9) + 18)
        spine_color = {"global": "#2563eb", "region": "#0891b2", "network": "#7c3aed"}.get(deployment_kind, "#7fa3c2")
        lines.append(
            f'  <rect data-graph-role="decoration" x="{x + width - badge_width - 14}" y="{y + 10}" '
            f'width="{badge_width}" height="18" rx="9" fill="#dbeafe" stroke="#93c5fd" stroke-width="0.8"/>'
        )
        lines.append(
            f'  <text data-graph-role="decoration" x="{x + width - badge_width / 2 - 14}" y="{y + 22.5}" '
            f'text-anchor="middle" font-size="9" font-weight="700" fill="#315d7e">{badge}</text>'
        )
        lines.append(
            f'  <line data-graph-role="decoration" x1="{x + 8}" y1="{y + 42}" x2="{x + 8}" y2="{y + height - 14}" '
            f'stroke="{spine_color}" stroke-width="2.2" stroke-linecap="round" opacity="0.72"/>'
        )
    elif treatment == "transit":
        lines.append(
            f'  <line data-graph-role="decoration" x1="{x + 18}" y1="{y + 34}" x2="{x + width - 18}" y2="{y + 34}" '
            f'stroke="{stroke}" stroke-width="1" stroke-dasharray="2 7" opacity="0.35"/>'
        )
    elif treatment == "ops":
        lines.append(
            f'  <line data-graph-role="decoration" x1="{x + 14}" y1="{y + 34}" x2="{x + width - 14}" y2="{y + 34}" '
            f'stroke="#38bdf8" stroke-width="1" opacity="0.16"/>'
        )
        if "trace" in str(container.get("id", "")).lower():
            ruler_left = x + width - 244
            ruler_right = x + width - 24
            lines.append(
                f'  <line data-graph-role="decoration" x1="{ruler_left}" y1="{y + 22}" x2="{ruler_right}" y2="{y + 22}" '
                f'stroke="#38bdf8" stroke-width="1" opacity="0.42"/>'
            )
            for index in range(5):
                tick_x = ruler_left + (ruler_right - ruler_left) * index / 4
                lines.append(
                    f'  <line data-graph-role="decoration" x1="{tick_x}" y1="{y + 18}" x2="{tick_x}" y2="{y + 26}" '
                    f'stroke="#38bdf8" stroke-width="1" opacity="0.52"/>'
                )
                lines.append(
                    f'  <text data-graph-role="decoration" x="{tick_x}" y="{y + 14}" text-anchor="middle" '
                    f'font-size="7" font-weight="700" fill="#6f8ba5">{index * 25}%</text>'
                )
    if label:
        lines.append(f'  <text x="{x + 18}" y="{y + 24}" class="section">{normalize_text(label)}</text>')
    if subtitle:
        lines.append(f'  <text x="{x + 18}" y="{y + 44}" class="section-sub">{normalize_text(subtitle)}</text>')
    if side_label:
        side_x = to_float(container.get("side_label_x", max(28, x - 18)))
        side_y = to_float(container.get("side_label_y", y + height / 2))
        lines.append(
            f'  <text x="{side_x}" y="{side_y}" text-anchor="{side_label_anchor}" dominant-baseline="middle" '
            f'font-size="{side_label_size}" font-weight="{side_label_weight}" fill="{side_label_fill}">{normalize_text(side_label)}</text>'
        )
    return "\n".join(lines)


def container_header_bounds(container: Dict[str, object], style: Optional[Dict[str, object]] = None) -> Optional[Bounds]:
    label = section_header_text(container, style) if style is not None else str(container.get("header_text", "") or container.get("label", ""))
    label = label.strip()
    subtitle = str(container.get("subtitle", "")).strip()
    if not label and not subtitle:
        return None
    x = to_float(container["x"])
    y = to_float(container["y"])
    width = to_float(container["width"])
    header_height = to_float(container.get("header_height", 54 if subtitle else 30))
    label_width = geometry.estimate_text_width(label, 13) if label else 0.0
    subtitle_width = geometry.estimate_text_width(subtitle, 12) if subtitle else 0.0
    reserved_width = min(width - 12, max(label_width, subtitle_width) + 30)
    return rectangle_bounds(x + 8, y + 6, reserved_width, header_height)


def label_position_candidates(points: Sequence[Point], text: str = "") -> List[Point]:
    segments = list(zip(points, points[1:]))
    if not segments:
        return [points[0]]
    ranked_segments = sorted(
        segments,
        key=lambda seg: abs(seg[0][0] - seg[1][0]) + abs(seg[0][1] - seg[1][1]),
        reverse=True,
    )
    candidates: List[Point] = []
    horizontal_offset = 17.0
    vertical_offset = max(22.0, geometry.estimate_text_width(text, 12) / 2 + 10)
    global_x = (min(point[0] for point in points) + max(point[0] for point in points)) / 2
    global_y = (min(point[1] for point in points) + max(point[1] for point in points)) / 2
    for (x1, y1), (x2, y2) in ranked_segments:
        length = abs(x1 - x2) + abs(y1 - y2)
        if length < 34:
            continue
        centers = [
            ((x1 + x2) / 2, (y1 + y2) / 2),
            (x1 * 0.7 + x2 * 0.3, y1 * 0.7 + y2 * 0.3),
            (x1 * 0.3 + x2 * 0.7, y1 * 0.3 + y2 * 0.7),
        ]
        for mx, my in centers:
            if abs(y1 - y2) < 1e-6:
                candidates.extend([(mx, my - horizontal_offset), (mx, my + horizontal_offset), (mx, my - 30), (mx, my + 30), (mx, my)])
                candidates.extend([(global_x, my - horizontal_offset), (global_x, my + horizontal_offset)])
            elif abs(x1 - x2) < 1e-6:
                candidates.extend([(mx - vertical_offset, my), (mx + vertical_offset, my), (mx - vertical_offset - 14, my), (mx + vertical_offset + 14, my), (mx, my)])
                candidates.extend([(mx - vertical_offset, global_y), (mx + vertical_offset, global_y)])
            else:
                candidates.extend([(mx, my - 16), (mx, my + 16), (mx, my)])
    return candidates or [choose_label_position(points)]


def route_clearance_bounds(points: Sequence[Point], padding: float = 3.0) -> List[Bounds]:
    result: List[Bounds] = []
    for first, second in zip(points, points[1:]):
        left = min(first[0], second[0]) - padding
        right = max(first[0], second[0]) + padding
        top = min(first[1], second[1]) - padding
        bottom = max(first[1], second[1]) + padding
        result.append((left, top, right, bottom))
    return result


def choose_label_position_avoiding(
    points: Sequence[Point],
    text: str,
    occupied: Sequence[Bounds],
    *,
    routes: Sequence[Sequence[Point]] = (),
    canvas_bounds: Optional[Bounds] = None,
    dx: float = 0.0,
    dy: float = -4.0,
) -> Point:
    route_bounds = [bounds for route in routes for bounds in route_clearance_bounds(route)]
    offset_options = [
        (dx, dy),
        (0.0, -4.0),
        (0.0, 0.0),
        (0.0, -14.0),
        (0.0, 14.0),
        (-18.0, -4.0),
        (18.0, -4.0),
        (-32.0, 0.0),
        (32.0, 0.0),
    ]
    for candidate in label_position_candidates(points, text):
        for offset_x, offset_y in offset_options:
            adjusted = (candidate[0] + offset_x, candidate[1] + offset_y)
            label_box = estimate_label_bounds(adjusted[0], adjusted[1], text)
            if canvas_bounds is not None and not geometry.bounds_inside(label_box, canvas_bounds, 4):
                continue
            if any(bounds_intersect(label_box, other, 4) for other in occupied):
                continue
            if any(bounds_intersect(label_box, other, 1) for other in route_bounds):
                continue
            return adjusted
    raise ValueError(f"no collision-free label position for {text!r}")


def legend_layout(data: Dict[str, object], legend: Sequence[Dict[str, object]], width: float, height: float) -> Optional[Tuple[float, float, Bounds]]:
    if not legend:
        return None
    orientation = str(data.get("legend_orientation", "vertical")).strip().lower()
    if orientation not in {"vertical", "horizontal"}:
        raise ValueError("legend_orientation must be vertical or horizontal")
    x = to_float(data.get("_legend_x", data.get("legend_x", 42)))
    default_y = height - 82 if orientation == "horizontal" else height - (len(legend) * 22 + 34)
    y = to_float(data.get("_legend_y", data.get("legend_y", default_y)))
    position = str(data.get("legend_position", "bottom-left"))
    label_widths = [geometry.estimate_text_width(str(item.get("label", "")), 12) for item in legend]
    if orientation == "horizontal":
        block_width = sum(40 + label_width + 28 for label_width in label_widths) - 18
        block_height = 28
    else:
        block_width = 40 + max(label_widths, default=84) + 12
        block_height = len(legend) * 22 + 6
    if "_legend_x" not in data and position == "bottom-right":
        x = to_float(data.get("legend_x", width - block_width - 42))
    elif "_legend_x" not in data and position == "top-right":
        x = to_float(data.get("legend_x", width - block_width - 42))
        y = to_float(data.get("legend_y", 96))
    elif "_legend_x" not in data and position == "top-left":
        x = to_float(data.get("legend_x", 42))
        y = to_float(data.get("legend_y", 96))
    return (x, y, rectangle_bounds(x - 10, y - 14, block_width + 20, block_height + 18))


def route_hint_points(arrow: Dict[str, object], node_map: Dict[str, Node]) -> List[Point]:
    source = node_map.get(str(arrow.get("source"))) if arrow.get("source") else None
    target = node_map.get(str(arrow.get("target"))) if arrow.get("target") else None
    start_hint = (to_float(arrow.get("x1")), to_float(arrow.get("y1")))
    end_hint = (to_float(arrow.get("x2")), to_float(arrow.get("y2")))
    if source is not None:
        toward = end_hint if target is None else (target.cx, target.cy)
        start = anchor_point(source, toward, str(arrow.get("source_port")) if arrow.get("source_port") else None)
    else:
        start = start_hint
    if target is not None:
        toward = start_hint if source is None else (source.cx, source.cy)
        end = anchor_point(target, toward, str(arrow.get("target_port")) if arrow.get("target_port") else None)
    else:
        end = end_hint
    waypoints = [(to_float(point[0]), to_float(point[1])) for point in (arrow.get("route_points") or [])]
    return [start, *waypoints, end]


def resolve_legend_layout(
    data: Dict[str, object],
    legend: Sequence[Dict[str, object]],
    width: float,
    height: float,
    obstacles: Sequence[Bounds],
    arrows: Sequence[Dict[str, object]],
    node_map: Dict[str, Node],
) -> Optional[Dict[str, object]]:
    requested = legend_layout(data, legend, width, height)
    if requested is None:
        return None
    requested_x, requested_y, requested_bounds = requested
    hint_routes = [route_hint_points(arrow, node_map) for arrow in arrows if arrow.get("route_points")]
    canvas = (0.0, 0.0, width, height)

    def is_safe(bounds: Bounds) -> bool:
        if not geometry.bounds_inside(bounds, canvas, 8):
            return False
        if any(bounds_intersect(bounds, obstacle, 6) for obstacle in obstacles):
            return False
        return not any(
            segment_hits_bounds(first, second, bounds)
            for route in hint_routes
            for first, second in zip(route, route[1:])
        )

    if is_safe(requested_bounds):
        return {
            "requested": [round(requested_x, 2), round(requested_y, 2)],
            "actual": [round(requested_x, 2), round(requested_y, 2)],
            "moved": False,
            "bounds": requested_bounds,
        }
    if data.get("legend_locked"):
        raise ValueError("locked legend intersects diagram content or a mandatory route")

    block_width = requested_bounds[2] - requested_bounds[0]
    block_height = requested_bounds[3] - requested_bounds[1]
    candidates: List[Tuple[float, float]] = []
    for top in range(84, max(85, int(height - block_height - 8)) + 1, 8):
        for left in range(8, max(9, int(width - block_width - 8)) + 1, 8):
            x = left + 10
            y = top + 14
            candidates.append((float(x), float(y)))
    candidates.sort(key=lambda point: (abs(point[0] - requested_x) + abs(point[1] - requested_y), point[1], point[0]))
    for x, y in candidates:
        bounds = (x - 10, y - 14, x - 10 + block_width, y - 14 + block_height)
        if is_safe(bounds):
            data["_legend_x"] = x
            data["_legend_y"] = y
            return {
                "requested": [round(requested_x, 2), round(requested_y, 2)],
                "actual": [round(x, 2), round(y, 2)],
                "moved": True,
                "bounds": bounds,
            }
    raise ValueError("no collision-free legend position is available")


def footer_layout(data: Dict[str, object], width: float, height: float) -> Optional[Tuple[float, float, Bounds]]:
    text = str(data.get("footer", "")).strip()
    if not text:
        return None
    footer_width = max(140, len(text) * 7)
    x = to_float(data.get("footer_x", 42))
    y = to_float(data.get("footer_y", height - 16))
    position = str(data.get("footer_position", "bottom-left"))
    if position == "bottom-right":
        x = to_float(data.get("footer_x", width - footer_width - 42))
    return (x, y, rectangle_bounds(x, y - 12, footer_width, 16))


def render_tags(node: Dict[str, object], x: float, y: float, style: Dict[str, object]) -> List[str]:
    tags = node.get("tags", [])
    if not tags:
        return []
    cursor_x = x
    lines = []
    for tag in tags:
        label = normalize_text(tag.get("label", ""))
        width = max(62, len(str(tag.get("label", ""))) * 8 + 18)
        fill = tag.get("fill", "#eff6ff")
        stroke = tag.get("stroke", "#bfdbfe")
        text_fill = tag.get("text_fill", style_value(style, "arrow_colors")["read"])
        lines.append(
            f'  <rect x="{cursor_x}" y="{y}" width="{width}" height="16" rx="3" fill="{fill}" stroke="{stroke}" stroke-width="1"/>'
        )
        lines.append(
            f'  <text x="{cursor_x + width / 2}" y="{y + 11.5}" text-anchor="middle" font-size="11" font-weight="500" fill="{text_fill}">{label}</text>'
        )
        cursor_x += width + 8
    return lines


def fitted_text_size(text: str, available_width: float, *, preferred: float = 18.0, minimum: float = 12.0) -> float:
    """Keep a single-line node title inside its card without manual tuning."""

    estimated = geometry.estimate_text_width(text, preferred, weight=1.08)
    if estimated <= max(1.0, available_width):
        return preferred
    scaled = max(minimum, preferred * available_width / max(estimated, 1.0))
    return math.floor(scaled * 100) / 100


def fit_single_line_text(
    text: object,
    available_width: float,
    *,
    preferred: float = 18.0,
    minimum: float = 12.0,
) -> Tuple[str, float]:
    """Fit one line, then fail visually closed with an ellipsis at the minimum size."""

    value = " ".join(str(text or "").split())
    size = fitted_text_size(value, available_width, preferred=preferred, minimum=minimum)
    if geometry.estimate_text_width(value, size, weight=1.08) <= available_width:
        return value, size
    candidate = value
    while candidate and geometry.estimate_text_width(candidate + "…", size, weight=1.08) > available_width:
        candidate = candidate[:-1].rstrip()
    return (candidate + "…" if candidate else "…"), size


def wrap_text_lines(text: object, available_width: float, *, font_size: float = 11.5, max_lines: int = 2) -> List[str]:
    """Balance short card copy across lines and fail visually closed with an ellipsis."""

    value = " ".join(str(text or "").split())
    if not value:
        return []
    if geometry.estimate_text_width(value, font_size) <= available_width or max_lines <= 1:
        return [value]
    words = value.split()
    if max_lines == 2 and len(words) > 1:
        candidates: List[Tuple[Tuple[float, float], List[str]]] = []
        for index in range(1, len(words)):
            candidate = [" ".join(words[:index]), " ".join(words[index:])]
            widths = [geometry.estimate_text_width(line, font_size) for line in candidate]
            overflow = sum(max(0.0, width - available_width) for width in widths)
            candidates.append(((overflow, abs(widths[0] - widths[1])), candidate))
        lines = min(candidates, key=lambda item: item[0])[1]
    else:
        lines = []
        current = ""
        for word in words:
            candidate = f"{current} {word}".strip()
            if current and geometry.estimate_text_width(candidate, font_size) > available_width:
                lines.append(current)
                current = word
            else:
                current = candidate
        if current:
            lines.append(current)
        if len(lines) > max_lines:
            lines = lines[: max_lines - 1] + [" ".join(lines[max_lines - 1 :])]

    fitted: List[str] = []
    for line in lines[:max_lines]:
        candidate = line
        while len(candidate) > 1 and geometry.estimate_text_width(candidate, font_size) > available_width:
            candidate = candidate[:-1].rstrip()
        if candidate != line:
            candidate = candidate.rstrip(" …") + "…"
            while len(candidate) > 1 and geometry.estimate_text_width(candidate, font_size) > available_width:
                candidate = candidate[:-2].rstrip() + "…"
        fitted.append(candidate)
    return fitted


def render_review_node(node: Dict[str, object], style: Dict[str, object]) -> str:
    x = to_float(node["x"])
    y = to_float(node["y"])
    width = to_float(node.get("width", 180))
    height = to_float(node.get("height", 84))
    fill = str(node.get("fill", style_value(style, "node_fill")))
    stroke = str(node.get("stroke", style_value(style, "node_stroke")))
    seed = node.get("_rough_seed", 0)
    element_id = node.get("id", "review-card")
    dx = deterministic_jitter(seed, element_id, 1)
    dy = deterministic_jitter(seed, element_id, 2)
    raw_c4_type = str(node.get("c4_type", node.get("type_label", "element")))
    c4_type_raw = raw_c4_type.upper().replace("_", " ")
    c4_type_text, c4_type_size = fit_single_line_text(
        c4_type_raw, width - 28, preferred=to_float(style_value(style, "type_label_size"), 10), minimum=7.5
    )
    c4_type = normalize_text(c4_type_text)
    external_dash = ' stroke-dasharray="6 4"' if raw_c4_type == "external_system" else ""
    title_raw = str(node.get("label", ""))
    title_text, title_size = fit_single_line_text(title_raw, width - 28, preferred=15, minimum=10.5)
    title = normalize_text(title_text)
    description_raw = str(node.get("description", node.get("sublabel", "")))
    technology_raw = str(node.get("technology", ""))
    technology_text, technology_size = fit_single_line_text(
        technology_raw, width - 24, preferred=9.5, minimum=7.5
    )
    technology = normalize_text(technology_text)
    description_size = 11.5
    description_lines = wrap_text_lines(description_raw, width - 28, font_size=description_size, max_lines=2)
    lines = [
        f'  <rect x="{x}" y="{y}" width="{width}" height="{height}" rx="7" fill="{fill}" stroke="{stroke}" '
        f'stroke-width="1.8"{external_dash}/>',
        f'  <path data-graph-role="decoration" d="M {x + 7 + dx} {y + 2 + dy} H {x + width - 7 + dx} '
        f'Q {x + width - 2 + dx} {y + 2 + dy} {x + width - 2 + dx} {y + 8 + dy} '
        f'V {y + height - 7 + dy}" fill="none" stroke="{stroke}" stroke-width="0.8" opacity="0.42"/>',
        f'  <text data-text-role="type" data-full-text="{normalize_attribute(c4_type_raw)}" '
        f'data-text-max-width="{width - 28}" x="{x + 14}" y="{y + 18}" class="node-type" '
        f'font-size="{c4_type_size}">{c4_type}</text>',
        f'  <text data-text-role="title" data-full-text="{normalize_attribute(title_raw)}" '
        f'data-text-max-width="{width - 28}" x="{x + 14}" y="{y + 41}" class="node-title" '
        f'font-size="{title_size}" text-anchor="start">{title}</text>',
    ]
    if description_lines:
        description_y = y + (56 if len(description_lines) > 1 else 60)
        for index, description_line in enumerate(description_lines):
            lines.append(
                f'  <text data-text-role="description" data-line="{index + 1}" '
                f'data-full-text="{normalize_attribute(description_raw)}" x="{x + 14}" '
                f'y="{description_y + index * 14}" class="node-sub" font-size="{description_size}" '
                f'text-anchor="start">{normalize_text(description_line)}</text>'
            )
    if technology:
        lines.append(
            f'  <text data-text-role="technology" data-full-text="{normalize_attribute(technology_raw)}" '
            f'data-text-max-width="{width - 24}" x="{x + width - 12}" y="{y + height - 9}" '
            f'text-anchor="end" font-size="{technology_size}" font-weight="700" '
            f'fill="{style_value(style, "type_label_fill")}">{technology}</text>'
        )
    return "\n".join(lines)


def render_cloud_glyph(glyph: str, cx: float, cy: float, color: str) -> List[str]:
    """Render manifest-backed neutral geometry without bundling vendor logos."""

    common = f'fill="none" stroke="{color}" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"'
    if glyph == "globe":
        return [
            f'  <circle cx="{cx}" cy="{cy}" r="11" {common}/>',
            f'  <path d="M {cx - 11} {cy} H {cx + 11} M {cx} {cy - 11} C {cx - 6} {cy - 5} {cx - 6} {cy + 5} {cx} {cy + 11} M {cx} {cy - 11} C {cx + 6} {cy - 5} {cx + 6} {cy + 5} {cx} {cy + 11}" {common}/>',
        ]
    if glyph == "database":
        return [
            f'  <ellipse cx="{cx}" cy="{cy - 8}" rx="11" ry="4" {common}/>',
            f'  <path d="M {cx - 11} {cy - 8} V {cy + 8} C {cx - 11} {cy + 13} {cx + 11} {cy + 13} {cx + 11} {cy + 8} V {cy - 8}" {common}/>',
            f'  <path d="M {cx - 11} {cy} C {cx - 11} {cy + 5} {cx + 11} {cy + 5} {cx + 11} {cy}" {common}/>',
        ]
    if glyph == "gateway":
        return [
            f'  <path d="M {cx} {cy - 12} L {cx + 12} {cy} L {cx} {cy + 12} L {cx - 12} {cy} Z" {common}/>',
            f'  <path d="M {cx - 6} {cy} H {cx + 6} M {cx + 3} {cy - 3} L {cx + 6} {cy} L {cx + 3} {cy + 3}" {common}/>',
        ]
    if glyph == "stream":
        return [
            f'  <path d="M {cx - 12} {cy - 7} H {cx + 5} M {cx - 5} {cy} H {cx + 12} M {cx - 12} {cy + 7} H {cx + 5}" {common}/>',
            f'  <circle cx="{cx + 8}" cy="{cy - 7}" r="2.4" fill="{color}"/><circle cx="{cx - 8}" cy="{cy}" r="2.4" fill="{color}"/><circle cx="{cx + 8}" cy="{cy + 7}" r="2.4" fill="{color}"/>',
        ]
    if glyph == "observe":
        return [
            f'  <path d="M {cx - 13} {cy} Q {cx} {cy - 12} {cx + 13} {cy} Q {cx} {cy + 12} {cx - 13} {cy} Z" {common}/>',
            f'  <path d="M {cx - 6} {cy} H {cx - 2} L {cx + 1} {cy - 5} L {cx + 4} {cy + 5} L {cx + 7} {cy}" {common}/>',
        ]
    return [
        f'  <rect x="{cx - 11}" y="{cy - 9}" width="22" height="18" rx="4" {common}/>',
        f'  <path d="M {cx - 6} {cy - 3} H {cx + 6} M {cx - 6} {cy + 3} H {cx + 3}" {common}/>',
    ]


def render_cloud_node(node: Dict[str, object], style: Dict[str, object]) -> str:
    x = to_float(node["x"])
    y = to_float(node["y"])
    width = to_float(node.get("width", 160))
    height = to_float(node.get("height", 72))
    fill = str(node.get("fill", style_value(style, "node_fill")))
    stroke = str(node.get("stroke", style_value(style, "node_stroke")))
    icon_color = str(node.get("icon_color", "#2563eb"))
    glyph = str(node.get("glyph", "compute"))
    provider = normalize_text(str(node.get("provider", node.get("platform", "cloud"))).upper())
    title_raw = str(node.get("label", ""))
    subtitle_raw = str(node.get("sublabel", node.get("service", "")))
    available_text_width = max(28.0, width - 78)
    title_text, title_size = fit_single_line_text(title_raw, available_text_width, preferred=13.5, minimum=10.5)
    subtitle_text, subtitle_size = fit_single_line_text(subtitle_raw, available_text_width, preferred=11.5, minimum=9.5)
    title = normalize_text(title_text)
    subtitle = normalize_text(subtitle_text)
    icon_box_size = min(42.0, max(28.0, height - 16.0))
    icon_box_x = x + 12
    icon_box_y = y + (height - icon_box_size) / 2
    icon_center_x = icon_box_x + icon_box_size / 2
    icon_center_y = y + height / 2
    lines = [
        f'  <rect x="{x}" y="{y}" width="{width}" height="{height}" rx="12" fill="{fill}" stroke="{stroke}" stroke-width="1.4" filter="url(#shadowSoft)"/>',
        f'  <rect x="{icon_box_x}" y="{icon_box_y}" width="{icon_box_size}" height="{icon_box_size}" rx="11" fill="{icon_color}" opacity="0.12" stroke="{icon_color}" stroke-width="1.2"/>',
        f'  <text data-text-role="title" data-full-text="{normalize_attribute(title_raw)}" x="{x + 66}" y="{y + 31}" text-anchor="start" class="node-title" font-size="{title_size}">{title}</text>',
        f'  <text data-text-role="subtitle" data-full-text="{normalize_attribute(subtitle_raw)}" x="{x + 66}" y="{y + 50}" text-anchor="start" class="node-sub" font-size="{subtitle_size}">{subtitle}</text>',
        f'  <text x="{x + width - 10}" y="{y + 14}" text-anchor="end" font-size="8.5" font-weight="800" fill="{style_value(style, "type_label_fill")}">{provider}</text>',
    ]
    lines[2:2] = render_cloud_glyph(glyph, icon_center_x, icon_center_y, icon_color)
    return "\n".join(lines)


def render_transit_node(node: Dict[str, object], style: Dict[str, object]) -> str:
    x = to_float(node["x"])
    y = to_float(node["y"])
    width = to_float(node.get("width", 140))
    height = to_float(node.get("height", 72))
    role = str(node.get("transit_role", "station"))
    color = str(node.get("rail_color", node.get("stroke", style_value(style, "arrow_colors")["control"])))
    if role == "dlq":
        color = str(node.get("stroke", style_value(style, "arrow_colors")["feedback"]))
    fill = str(node.get("fill", style_value(style, "node_fill")))
    title_raw = str(node.get("label", ""))
    subtitle_raw = str(node.get("sublabel", node.get("operation", "")))
    text_width = max(36.0, width - 50)
    title_text, title_size = fit_single_line_text(title_raw, text_width, preferred=13.5, minimum=10.5)
    subtitle_text, subtitle_size = fit_single_line_text(subtitle_raw, text_width, preferred=11.5, minimum=9.2)
    title = normalize_text(title_text)
    subtitle = normalize_text(subtitle_text)
    badge = normalize_text(node.get("badge", node.get("partition_badge", "")))
    marker_x = x + 18
    dash = ' stroke-dasharray="5 3"' if role == "dlq" else ""
    lines = [
        f'  <rect x="{x}" y="{y}" width="{width}" height="{height}" rx="{height / 2}" fill="{fill}" stroke="{color}" stroke-width="1.6"{dash}/>',
        f'  <circle cx="{marker_x}" cy="{y + height / 2}" r="8" fill="{style_value(style, "background")}" stroke="{color}" stroke-width="2.4"/>',
        f'  <text data-text-role="title" data-full-text="{normalize_attribute(title_raw)}" x="{x + 36}" y="{y + 38}" text-anchor="start" class="node-title" font-size="{title_size}">{title}</text>',
        f'  <text data-text-role="subtitle" data-full-text="{normalize_attribute(subtitle_raw)}" x="{x + 36}" y="{y + 58}" text-anchor="start" class="node-sub" font-size="{subtitle_size}">{subtitle}</text>',
    ]
    if role == "junction":
        lines.append(f'  <circle cx="{marker_x}" cy="{y + height / 2}" r="3" fill="{color}"/>')
    elif role == "dlq":
        lines.append(
            f'  <path data-graph-role="decoration" d="M {marker_x - 3} {y + height / 2 - 3} '
            f'L {marker_x + 3} {y + height / 2 + 3} M {marker_x + 3} {y + height / 2 - 3} '
            f'L {marker_x - 3} {y + height / 2 + 3}" stroke="{color}" stroke-width="1.7" stroke-linecap="round"/>'
        )
    elif role == "state_store":
        lines.append(
            f'  <rect data-graph-role="decoration" x="{marker_x - 3.5}" y="{y + height / 2 - 3.5}" '
            f'width="7" height="7" rx="1.2" fill="none" stroke="{color}" stroke-width="1.5"/>'
        )
    elif isinstance(node.get("station_order"), int):
        station_number = int(node["station_order"]) + 1
        lines.append(
            f'  <text data-graph-role="decoration" x="{marker_x}" y="{y + height / 2 + 2.5}" '
            f'text-anchor="middle" font-size="7" font-weight="800" fill="{color}">{station_number:02d}</text>'
        )
    if badge:
        badge_width = max(34.0, geometry.estimate_text_width(str(node.get("badge", node.get("partition_badge", ""))), 9) + 12)
        lines.extend(
            [
                f'  <rect x="{x + width - badge_width - 10}" y="{y + 6}" width="{badge_width}" height="15" rx="7.5" fill="{color}" opacity="0.13"/>',
                f'  <text x="{x + width - badge_width / 2 - 10}" y="{y + 16.5}" text-anchor="middle" font-size="8.2" font-weight="800" fill="{color}">{badge}</text>',
            ]
        )
    return "\n".join(lines)


def render_ops_service(node: Dict[str, object], style: Dict[str, object]) -> str:
    x = to_float(node["x"])
    y = to_float(node["y"])
    width = to_float(node.get("width", 180))
    height = to_float(node.get("height", 108))
    status = str(node.get("status", "ok"))
    status_colors = {"ok": "#22c55e", "warn": "#f59e0b", "critical": "#f43f5e", "unknown": "#64748b"}
    status_color = status_colors.get(status, status_colors["unknown"])
    title_raw = str(node.get("label", ""))
    status_raw = str(node.get("status_label", status.upper()))
    status_preferred = 8.5
    status_needed = geometry.estimate_text_width(status_raw, status_preferred, weight=1.08) + 4
    status_budget = min(width * 0.38, max(36.0, status_needed))
    title_budget = max(28.0, width - 46.0 - status_budget)
    title_text, title_size = fit_single_line_text(title_raw, title_budget, preferred=13.5, minimum=9.5)
    status_text, status_size = fit_single_line_text(status_raw, status_budget, preferred=status_preferred, minimum=6.8)
    title = normalize_text(title_text)
    status_label = normalize_text(status_text)
    lines = [
        f'  <rect x="{x}" y="{y}" width="{width}" height="{height}" rx="12" fill="{node.get("fill", style_value(style, "node_fill"))}" stroke="{node.get("stroke", style_value(style, "node_stroke"))}" stroke-width="1.4"/>',
        f'  <rect data-graph-role="decoration" x="{x}" y="{y + 12}" width="4" height="{height - 24}" rx="2" fill="{status_color}"/>',
        f'  <circle cx="{x + 16}" cy="{y + 19}" r="5" fill="{status_color}"/>',
        f'  <text data-text-role="title" data-full-text="{normalize_attribute(title_raw)}" '
        f'data-text-max-width="{title_budget}" x="{x + 28}" y="{y + 24}" text-anchor="start" '
        f'class="node-title" font-size="{title_size}">{title}</text>',
        f'  <text data-text-role="status" data-full-text="{normalize_attribute(status_raw)}" '
        f'data-text-max-width="{status_budget}" x="{x + width - 10}" y="{y + 21}" text-anchor="end" '
        f'font-size="{status_size}" font-weight="800" fill="{status_color}">{status_label}</text>',
    ]
    metrics = list(node.get("metric_badges", []))[:4]
    chip_gap = 6.0
    chip_width = (width - 24 - chip_gap) / 2
    chip_height = 27.0
    for index, metric in enumerate(metrics):
        column = index % 2
        row = index // 2
        chip_x = x + 10 + column * (chip_width + chip_gap)
        chip_y = y + 38 + row * (chip_height + 5)
        metric_status = status_colors.get(str(metric.get("status", "unknown")), status_colors["unknown"])
        metric_name = normalize_text(str(metric.get("name", ""))[:3].upper())
        metric_value_raw = f'{metric.get("value", "")}{metric.get("unit", "")}'
        metric_window_raw = f'@{metric.get("window", "")}'
        metric_value_budget = max(20.0, chip_width - 16.0)
        metric_window_budget = max(18.0, chip_width - 44.0)
        metric_value_text, metric_value_size = fit_single_line_text(
            metric_value_raw, metric_value_budget, preferred=9.5, minimum=7.0
        )
        metric_window_text, metric_window_size = fit_single_line_text(
            metric_window_raw, metric_window_budget, preferred=6.8, minimum=5.8
        )
        metric_value = normalize_text(metric_value_text)
        metric_window = normalize_text(metric_window_text)
        lines.extend(
            [
                f'  <rect x="{chip_x}" y="{chip_y}" width="{chip_width}" height="{chip_height}" rx="6" fill="#13263a" stroke="#29435d" stroke-width="0.8"/>',
                f'  <circle cx="{chip_x + 8}" cy="{chip_y + 9}" r="2.5" fill="{metric_status}"/>',
                f'  <text x="{chip_x + 14}" y="{chip_y + 11.5}" class="metric-label">{metric_name}</text>',
                f'  <text data-text-role="metric-window" data-full-text="{normalize_attribute(metric_window_raw)}" '
                f'data-text-max-width="{metric_window_budget}" x="{chip_x + chip_width - 6}" y="{chip_y + 11.5}" '
                f'text-anchor="end" font-size="{metric_window_size}" font-weight="700" '
                f'fill="{style_value(style, "text_muted")}">{metric_window}</text>',
                f'  <text data-text-role="metric-value" data-full-text="{normalize_attribute(metric_value_raw)}" '
                f'data-text-max-width="{metric_value_budget}" x="{chip_x + 8}" y="{chip_y + 23}" '
                f'class="metric-value" font-size="{metric_value_size}">{metric_value}</text>',
            ]
        )
    return "\n".join(lines)


def render_trace_span(node: Dict[str, object], style: Dict[str, object]) -> str:
    x = to_float(node["x"])
    y = to_float(node["y"])
    width = to_float(node.get("width", 300))
    height = to_float(node.get("height", 28))
    status = str(node.get("status", "ok"))
    color = {"ok": "#38bdf8", "warn": "#f59e0b", "critical": "#f43f5e"}.get(status, "#64748b")
    title_raw = str(node.get("label", node.get("span_id", "span")))
    duration_raw = f'{node.get("duration_ms", "")} ms'
    duration_needed = geometry.estimate_text_width(duration_raw, 10, weight=1.08) + 4
    duration_budget = min(width * 0.3, max(34.0, duration_needed))
    title_budget = max(30.0, width - 34.0 - duration_budget)
    title_text, title_size = fit_single_line_text(title_raw, title_budget, preferred=11.5, minimum=8.0)
    duration_text, duration_size = fit_single_line_text(duration_raw, duration_budget, preferred=10, minimum=7.0)
    title = normalize_text(title_text)
    duration = normalize_text(duration_text)
    return "\n".join(
        [
            f'  <rect x="{x}" y="{y}" width="{width}" height="{height}" rx="6" fill="{color}" fill-opacity="0.12" stroke="{color}" stroke-width="1.2"/>',
            f'  <rect x="{x}" y="{y}" width="5" height="{height}" rx="2.5" fill="{color}"/>',
            f'  <text data-text-role="title" data-full-text="{normalize_attribute(title_raw)}" '
            f'data-text-max-width="{title_budget}" x="{x + 14}" y="{y + 18.5}" text-anchor="start" '
            f'class="node-title" font-size="{title_size}">{title}</text>',
            f'  <text data-text-role="duration" data-full-text="{normalize_attribute(duration_raw)}" '
            f'data-text-max-width="{duration_budget}" x="{x + width - 10}" y="{y + 18.5}" text-anchor="end" '
            f'font-size="{duration_size}" font-weight="700" fill="{style_value(style, "text_secondary")}">{duration}</text>',
        ]
    )


def render_otel_collector(node: Dict[str, object], style: Dict[str, object]) -> str:
    x = to_float(node["x"])
    y = to_float(node["y"])
    width = to_float(node.get("width", 180))
    height = to_float(node.get("height", 60))
    title_raw = str(node.get("label", "OTel Collector"))
    title_budget = max(24.0, width - 24.0)
    title_text, title_size = fit_single_line_text(title_raw, title_budget, preferred=13, minimum=8.5)
    title = normalize_text(title_text)
    return "\n".join(
        [
            f'  <rect x="{x}" y="{y}" width="{width}" height="{height}" rx="10" fill="#10263a" stroke="#22d3ee" stroke-width="1.5"/>',
            f'  <text data-text-role="title" data-full-text="{normalize_attribute(title_raw)}" '
            f'data-text-max-width="{title_budget}" x="{x + 12}" y="{y + 24}" text-anchor="start" '
            f'class="node-title" font-size="{title_size}">{title}</text>',
            f'  <text x="{x + 12}" y="{y + 44}" text-anchor="start" font-size="8.8" font-weight="700" fill="#67e8f9">RECEIVE → PROCESS → EXPORT</text>',
        ]
    )


def render_special_node(node: Dict[str, object], style: Dict[str, object], kind: str) -> Optional[str]:
    if kind == "review_card":
        return render_review_node(node, style)
    if kind == "cloud_service":
        return render_cloud_node(node, style)
    if kind in {"transit_station", "transit_junction", "transit_terminal"}:
        return render_transit_node(node, style)
    if kind == "ops_service":
        return render_ops_service(node, style)
    if kind == "trace_span":
        return render_trace_span(node, style)
    if kind == "otel_collector":
        return render_otel_collector(node, style)
    return None


def render_rect_node(node: Dict[str, object], style: Dict[str, object], kind: str) -> str:
    special = render_special_node(node, style, kind)
    if special is not None:
        return special
    x = to_float(node["x"])
    y = to_float(node["y"])
    width = to_float(node.get("width", 180))
    height = to_float(node.get("height", 76))
    rx = to_float(node.get("rx", style_value(style, "node_radius")))
    fill = str(node.get("fill", style_value(style, "node_fill")))
    stroke = str(node.get("stroke", style_value(style, "node_stroke")))
    stroke_width = to_float(node.get("stroke_width", 2.0 if kind != "rect" else 1.8))
    filter_attr = ""
    node_shadow = node.get("filter")
    if node_shadow:
        filter_attr = f' filter="url(#{node_shadow})"'
    elif node.get("glow"):
        glow_name = str(node.get("glow"))
        glow_map = {
            "blue": "glowBlue",
            "purple": "glowPurple",
            "green": "glowGreen",
            "orange": "glowOrange",
        }
        if glow_name in glow_map:
            filter_attr = f' filter="url(#{glow_map[glow_name]})"'
    elif style_value(style, "node_shadow"):
        if not node.get("flat", False):
            filter_attr = f' filter="{style_value(style, "node_shadow")}"'
    title_text = str(node.get("label", ""))
    title = normalize_text(title_text)
    subtitle = normalize_text(node.get("sublabel", ""))
    type_label = normalize_text(node.get("type_label", ""))
    accent_fill = node.get("accent_fill")
    lines = []

    if kind == "double_rect":
        lines.append(
            f'  <rect x="{x}" y="{y}" width="{width}" height="{height}" rx="{rx}" fill="{fill}" stroke="{stroke}" stroke-width="{stroke_width}"{filter_attr}/>'
        )
        lines.append(
            f'  <rect x="{x + 6}" y="{y + 6}" width="{width - 12}" height="{height - 12}" rx="{max(rx - 3, 4)}" fill="none" stroke="{stroke}" stroke-width="1.2" opacity="0.65"/>'
        )
    elif kind == "terminal":
        lines.append(
            f'  <rect x="{x}" y="{y}" width="{width}" height="{height}" rx="{rx}" fill="{fill}" stroke="{stroke}" stroke-width="{stroke_width}"{filter_attr}/>'
        )
        lines.append(
            f'  <rect x="{x}" y="{y}" width="{width}" height="18" rx="{rx}" fill="{node.get("header_fill", "#1f2937")}" opacity="0.95"/>'
        )
        header_colors = node.get("header_dots", ["#ef4444", "#f59e0b", "#10b981"])
        for idx, color in enumerate(header_colors):
            lines.append(f'  <circle cx="{x + 16 + idx * 14}" cy="{y + 9}" r="4" fill="{color}"/>')
        lines.append(
            f'  <text x="{x + 18}" y="{y + 44}" font-size="28" font-weight="700" fill="{node.get("prompt_fill", "#10b981")}">$</text>'
        )
        lines.append(
            f'  <text x="{x + 38}" y="{y + 44}" font-size="22" font-weight="500" fill="{style_value(style, "text_secondary")}">_</text>'
        )
    elif kind == "document":
        fold = min(18, width * 0.18, height * 0.22)
        path = (
            f"M {x} {y} L {x + width - fold} {y} L {x + width} {y + fold} "
            f"L {x + width} {y + height} L {x} {y + height} Z"
        )
        lines.append(
            f'  <path d="{path}" fill="{fill}" stroke="{stroke}" stroke-width="{stroke_width}"{filter_attr}/>'
        )
        lines.append(
            f'  <path d="M {x + width - fold} {y} L {x + width - fold} {y + fold} L {x + width} {y + fold}" fill="none" stroke="{stroke}" stroke-width="{stroke_width}"/>'
        )
        for idx in range(4):
            line_y = y + 26 + idx * 14
            lines.append(
                f'  <line x1="{x + 18}" y1="{line_y}" x2="{x + width - 28}" y2="{line_y}" stroke="{node.get("line_stroke", "#c4b5fd")}" stroke-width="1.2"/>'
            )
    elif kind == "folder":
        tab_w = min(54, width * 0.34)
        tab_h = 18
        path = (
            f"M {x} {y + tab_h} L {x + tab_w * 0.4} {y + tab_h} L {x + tab_w * 0.58} {y} "
            f"L {x + tab_w} {y} L {x + width} {y} L {x + width} {y + height} L {x} {y + height} Z"
        )
        lines.append(
            f'  <path d="{path}" fill="{fill}" stroke="{stroke}" stroke-width="{stroke_width}"{filter_attr}/>'
        )
        for idx in range(3):
            line_y = y + 42 + idx * 14
            lines.append(
                f'  <line x1="{x + 22}" y1="{line_y}" x2="{x + width - 22}" y2="{line_y}" stroke="{node.get("line_stroke", stroke)}" stroke-opacity="0.35" stroke-width="1.2"/>'
            )
    elif kind == "hexagon":
        inset = 22
        path = (
            f"M {x + inset} {y} L {x + width - inset} {y} L {x + width} {y + height / 2} "
            f"L {x + width - inset} {y + height} L {x + inset} {y + height} L {x} {y + height / 2} Z"
        )
        lines.append(f'  <path d="{path}" fill="{fill}" stroke="{stroke}" stroke-width="{stroke_width}"{filter_attr}/>')
    elif kind == "speech":
        tail = 18
        path = (
            f"M {x + rx} {y} L {x + width - rx} {y} Q {x + width} {y} {x + width} {y + rx} "
            f"L {x + width} {y + height - rx} Q {x + width} {y + height} {x + width - rx} {y + height} "
            f"L {x + 26} {y + height} L {x + 12} {y + height + tail} L {x + 16} {y + height} "
            f"L {x + rx} {y + height} Q {x} {y + height} {x} {y + height - rx} "
            f"L {x} {y + rx} Q {x} {y} {x + rx} {y} Z"
        )
        lines.append(f'  <path d="{path}" fill="{fill}" stroke="{stroke}" stroke-width="{stroke_width}"{filter_attr}/>')
    else:
        lines.append(
            f'  <rect x="{x}" y="{y}" width="{width}" height="{height}" rx="{rx}" fill="{fill}" stroke="{stroke}" stroke-width="{stroke_width}"{filter_attr}/>'
        )

    if accent_fill and kind == "icon_box":
        lines.append(
            f'  <rect x="{x + 12}" y="{y + 12}" width="{width - 24}" height="{height - 24}" rx="{max(rx - 4, 4)}" fill="{accent_fill}" opacity="0.9"/>'
        )

    if kind == "user_avatar":
        circle_fill = node.get("icon_fill", "#dbeafe")
        icon_stroke = node.get("icon_stroke", stroke)
        cx = x + 26
        cy = y + height / 2
        lines.append(f'  <circle cx="{cx}" cy="{cy}" r="18" fill="{circle_fill}" stroke="{icon_stroke}" stroke-width="1.6"/>')
        lines.append(f'  <circle cx="{cx}" cy="{cy - 6}" r="5" fill="{icon_stroke}"/>')
        lines.append(f'  <path d="M {cx - 10} {cy + 11} Q {cx} {cy + 2} {cx + 10} {cy + 11}" fill="none" stroke="{icon_stroke}" stroke-width="2"/>')

    if kind == "bot":
        cx = x + width / 2
        cy = y + height / 2 + 2
        body_fill = node.get("body_fill", "#1e293b")
        accent = node.get("accent_fill", "#34d399")
        lines.append(f'  <rect x="{cx - 42}" y="{cy - 32}" width="84" height="84" rx="18" fill="{body_fill}" stroke="#334155" stroke-width="1.8"{filter_attr}/>')
        lines.append(f'  <rect x="{cx - 26}" y="{cy - 16}" width="52" height="22" rx="6" fill="#0f172a" stroke="#475569" stroke-width="1.2"/>')
        lines.append(f'  <circle cx="{cx - 12}" cy="{cy - 5}" r="5" fill="{accent}"/>')
        lines.append(f'  <circle cx="{cx + 12}" cy="{cy - 5}" r="5" fill="{accent}"/>')
        lines.append(f'  <rect x="{cx - 14}" y="{cy + 14}" width="28" height="6" rx="3" fill="#334155"/>')
        lines.append(f'  <line x1="{cx}" y1="{cy - 36}" x2="{cx}" y2="{cy - 50}" stroke="{accent}" stroke-width="3"/>')
        lines.append(f'  <circle cx="{cx}" cy="{cy - 54}" r="5" fill="{accent}"/>')

    if kind == "circle_cluster":
        r = min(width, height) / 4.0
        centers = [(x + width * 0.36, y + height * 0.56), (x + width * 0.58, y + height * 0.45), (x + width * 0.74, y + height * 0.58)]
        for cx, cy in centers:
            lines.append(f'  <circle cx="{cx}" cy="{cy}" r="{r}" fill="{fill}" stroke="{stroke}" stroke-width="{stroke_width}"/>')

    type_offset = y + 18 if kind not in {"terminal", "bot"} else y + 18
    title_y = y + height / 2 - (4 if type_label and kind not in {"terminal", "bot"} else 0)
    if kind in {"document", "folder"}:
        title_y = y + height + 26
    elif kind == "circle_cluster":
        title_y = y + height / 2 + 8
    elif kind == "bot":
        title_y = y + height + 22
    elif kind == "user_avatar":
        title_y = y + height / 2 + 6

    if type_label:
        lines.append(f'  <text x="{x + (54 if kind == "user_avatar" else width / 2)}" y="{type_offset}" text-anchor="middle" class="node-type">{type_label}</text>')
        title_y += 10 if kind not in {"document", "folder", "circle_cluster", "bot"} else 0

    title_x = x + width / 2
    text_anchor = "middle"
    if kind == "user_avatar":
        title_x = x + 64
        text_anchor = "start"
    if kind == "terminal":
        title_y = y + height - 14
    if kind == "bot":
        title_x = x + width / 2
        text_anchor = "middle"
    title_size = to_float(
        node.get("title_size"),
        fitted_text_size(title_text, width - (32 if kind == "double_rect" else 24)),
    )
    lines.append(
        f'  <text x="{title_x}" y="{title_y}" text-anchor="{text_anchor}" class="node-title" '
        f'font-size="{title_size}">{title}</text>'
    )

    if subtitle:
        sub_y = title_y + 22
        if kind == "document":
            sub_y = y + height + 44
            title_y = y + height + 24
        if kind == "folder":
            sub_y = y + height + 44
        if kind == "circle_cluster":
            sub_y = y + height / 2 + 28
        if kind == "bot":
            sub_y = y + height + 42
        if kind == "terminal":
            sub_y = y + height + 20
        if kind == "user_avatar":
            sub_y = title_y + 22
        lines.append(f'  <text x="{title_x}" y="{sub_y}" text-anchor="{text_anchor}" class="node-sub">{subtitle}</text>')

    tag_lines = []
    if node.get("tags"):
        tag_x = x + 18
        tag_y = y + height - 20
        if kind in {"document", "folder", "circle_cluster", "bot", "terminal"}:
            tag_y = y + height + 52
        tag_lines = render_tags(node, tag_x, tag_y, style)
    lines.extend(tag_lines)

    return "\n".join(lines)


def render_node(node: Dict[str, object], style: Dict[str, object]) -> str:
    kind = str(node.get("kind", node.get("shape", "rect")))
    if kind == "cylinder":
        x = to_float(node["x"])
        y = to_float(node["y"])
        width = to_float(node.get("width", 160))
        height = to_float(node.get("height", 120))
        rx = width / 2
        ry = min(18, height / 8)
        fill = str(node.get("fill", "#ecfdf5"))
        stroke = str(node.get("stroke", "#10b981"))
        stroke_width = to_float(node.get("stroke_width", 2.2))
        label_text = str(node.get("label", ""))
        label = normalize_text(label_text)
        subtitle = normalize_text(node.get("sublabel", ""))
        lines = [
            f'  <ellipse cx="{x + width / 2}" cy="{y + ry}" rx="{rx / 2}" ry="{ry}" fill="{fill}" stroke="{stroke}" stroke-width="{stroke_width}"/>',
            f'  <rect x="{x}" y="{y + ry}" width="{width}" height="{height - 2 * ry}" fill="{fill}" stroke="{stroke}" stroke-width="{stroke_width}"/>',
            f'  <ellipse cx="{x + width / 2}" cy="{y + height - ry}" rx="{rx / 2}" ry="{ry}" fill="{fill}" stroke="{stroke}" stroke-width="{stroke_width}"/>',
            f'  <ellipse cx="{x + width / 2}" cy="{y + height * 0.38}" rx="{rx / 2}" ry="{ry}" fill="none" stroke="{stroke}" stroke-opacity="0.45" stroke-width="1.2"/>',
            f'  <ellipse cx="{x + width / 2}" cy="{y + height * 0.6}" rx="{rx / 2}" ry="{ry}" fill="none" stroke="{stroke}" stroke-opacity="0.25" stroke-width="1.2"/>',
            f'  <text x="{x + width / 2}" y="{y + height / 2 - 6}" text-anchor="middle" class="node-title" '
            f'font-size="{to_float(node.get("title_size"), fitted_text_size(label_text, width - 24))}">{label}</text>',
        ]
        if subtitle:
            lines.append(f'  <text x="{x + width / 2}" y="{y + height / 2 + 18}" text-anchor="middle" class="node-sub">{subtitle}</text>')
        return "\n".join(lines)
    return render_rect_node(node, style, kind)


def inferred_port(node: Node, toward: Point) -> str:
    left, top, right, bottom = node.bounds
    dx = toward[0] - node.cx
    dy = toward[1] - node.cy
    width = right - left
    height = bottom - top
    if abs(dx) * height >= abs(dy) * width:
        return "right" if dx >= 0 else "left"
    return "bottom" if dy >= 0 else "top"


def prepare_arrows(arrows: Sequence[Dict[str, object]], node_map: Dict[str, Node]) -> List[Dict[str, object]]:
    prepared = [copy.deepcopy(arrow) for arrow in arrows]
    endpoint_groups: Dict[Tuple[str, str], List[Tuple[int, str, str]]] = {}

    for index, arrow in enumerate(prepared):
        edge_id = str(arrow.get("id") or f"edge-{index:03d}")
        arrow["_edge_id"] = edge_id
        arrow["_edge_dom_id"] = str(arrow.get("_dom_id") or safe_identifier(edge_id, f"edge-{index:03d}"))
        source = node_map.get(str(arrow.get("source"))) if arrow.get("source") else None
        target = node_map.get(str(arrow.get("target"))) if arrow.get("target") else None
        start_hint = (to_float(arrow.get("x1")), to_float(arrow.get("y1")))
        end_hint = (to_float(arrow.get("x2")), to_float(arrow.get("y2")))

        if source is not None:
            toward = end_hint if target is None else (target.cx, target.cy)
            source_port = str(arrow.get("source_port") or inferred_port(source, toward)).lower()
            arrow["_resolved_source_port"] = source_port
            endpoint_groups.setdefault((source.node_id, source_port), []).append((index, "source", edge_id))
        if target is not None:
            toward = start_hint if source is None else (source.cx, source.cy)
            target_port = str(arrow.get("target_port") or inferred_port(target, toward)).lower()
            arrow["_resolved_target_port"] = target_port
            endpoint_groups.setdefault((target.node_id, target_port), []).append((index, "target", edge_id))

    for (node_id, port), endpoints in sorted(endpoint_groups.items()):
        node = node_map[node_id]
        span = (node.bounds[3] - node.bounds[1]) if port in {"left", "right"} else (node.bounds[2] - node.bounds[0])
        ordered = sorted(endpoints, key=lambda item: item[2])
        count = len(ordered)
        usable_span = max(0.0, span - 24.0)
        if count > 1 and usable_span / (count - 1) < 6.0:
            raise ValueError(f"PORT_CAPACITY: {node_id}.{port} cannot fit {count} distinct endpoints")
        spacing = 0.0 if count <= 1 else min(18.0, usable_span / (count - 1))
        for position, (arrow_index, endpoint, _) in enumerate(ordered):
            offset = (position - (count - 1) / 2.0) * spacing
            prepared[arrow_index][f"_{endpoint}_port_offset"] = round(offset, 2)
    return prepared


def render_arrow(
    arrow: Dict[str, object],
    style: Dict[str, object],
    node_map: Dict[str, Node],
    route_obstacles: Sequence[Bounds],
    label_obstacles: Sequence[Bounds],
    *,
    existing_routes: Sequence[Sequence[Point]],
    canvas_bounds: Bounds,
) -> ArrowRender:
    edge_id = str(arrow.get("_edge_id") or "edge")
    edge_dom_id = str(arrow.get("_edge_dom_id") or safe_identifier(edge_id, "edge"))
    start_hint = (to_float(arrow.get("x1")), to_float(arrow.get("y1")))
    end_hint = (to_float(arrow.get("x2")), to_float(arrow.get("y2")))
    source_node = node_map.get(str(arrow.get("source"))) if arrow.get("source") else None
    target_node = node_map.get(str(arrow.get("target"))) if arrow.get("target") else None
    source_port = arrow.get("_resolved_source_port") or arrow.get("source_port")
    target_port = arrow.get("_resolved_target_port") or arrow.get("target_port")

    if source_node is not None:
        toward = end_hint if target_node is None else (target_node.cx, target_node.cy)
        start = anchor_point(
            source_node,
            toward,
            str(source_port) if source_port else None,
            to_float(arrow.get("_source_port_offset")),
        )
    else:
        start = start_hint

    if target_node is not None:
        toward = start_hint if source_node is None else (source_node.cx, source_node.cy)
        end = anchor_point(
            target_node,
            toward,
            str(target_port) if target_port else None,
            to_float(arrow.get("_target_port_offset")),
        )
    else:
        end = end_hint

    # Keep source and target bounds in the graph. Port endpoints lie on their
    # boundaries, so valid leads remain possible while routes cannot cut
    # through either node and re-enter from the wrong side.
    obstacles = list(route_obstacles)

    routing_data = dict(arrow)
    if source_port:
        routing_data["source_port"] = source_port
    if target_port:
        routing_data["target_port"] = target_port
    route = build_orthogonal_route(
        start,
        end,
        obstacles,
        routing_data,
        canvas_bounds=canvas_bounds,
        existing_routes=existing_routes,
    )
    interactions = geometry.route_interactions(route, existing_routes)
    if interactions.overlap_count:
        raise ValueError(f"edge {edge_id} has an unresolved collinear overlap")
    bridges = list(interactions.crossings)
    bends = geometry.bend_count(route)
    stretch = quality.route_stretch(route)
    path_d = geometry.path_with_bridges(route, bridges)
    color = color_for_flow(style, arrow)
    width = to_float(arrow.get("stroke_width", style_value(style, "arrow_width")))
    dash = arrow.get("stroke_dasharray")
    if dash is None and arrow.get("dashed"):
        dash = "6,4"
    marker = marker_for_color(style, color, arrow)
    source_id = normalize_attribute(arrow.get("source", ""))
    target_id = normalize_attribute(arrow.get("target", ""))
    bridge_attr = ";".join(f"{geometry.format_number(x)},{geometry.format_number(y)}" for x, y in bridges)
    edge_kind = str(arrow.get("edge_kind", arrow.get("transit_type", "flow")))
    topic_id = str(arrow.get("topic_id", ""))
    protocol = str(arrow.get("protocol", ""))
    via = str(arrow.get("via", ""))
    critical_path_id = str(arrow.get("critical_path_id", ""))
    critical = "true" if arrow.get("critical") else "false"
    flow = str(arrow.get("flow", ""))
    motion_role = str(arrow.get("motion_role", ""))
    motion_stage = str(arrow.get("motion_stage", ""))
    motion_order = str(arrow.get("motion_order", ""))
    critical_hop = str(arrow.get("critical_hop", ""))
    critical_hops = str(arrow.get("critical_hops", ""))
    shared_attributes = (
        f'data-edge-id="{normalize_attribute(edge_id)}" data-source="{source_id}" data-target="{target_id}" '
        f'data-edge-kind="{normalize_attribute(edge_kind)}" data-topic-id="{normalize_attribute(topic_id)}" '
        f'data-flow="{normalize_attribute(flow)}" '
        f'data-motion-role="{normalize_attribute(motion_role)}" '
        f'data-motion-stage="{normalize_attribute(motion_stage)}" '
        f'data-motion-order="{normalize_attribute(motion_order)}" '
        f'data-protocol="{normalize_attribute(protocol)}" data-via="{normalize_attribute(via)}" '
        f'data-critical-path-id="{normalize_attribute(critical_path_id)}" '
        f'data-critical-hop="{normalize_attribute(critical_hop)}" '
        f'data-critical-hops="{normalize_attribute(critical_hops)}" '
        f'data-critical="{critical}" data-bends="{bends}" data-route-stretch="{round(stretch, 3)}"'
    )
    rendered_paths: List[str] = []
    if bridges:
        background = str(style_value(style, "background"))
        rendered_paths.append(
            f'  <path id="{normalize_attribute(edge_dom_id)}-bridge-mask" data-graph-role="bridge-mask" '
            f'data-owner="{normalize_attribute(edge_id)}" d="{path_d}" fill="none" stroke="{background}" '
            f'stroke-width="{round(width + 4.5, 2)}" stroke-linecap="round" stroke-linejoin="round"/>'
        )
    style_index = int(style.get("_style_index", 0))
    if style_index == 9:
        # A second, deliberately imperfect pencil stroke is decorative only;
        # the routable edge remains the single semantic connector below.
        rough_offset = deterministic_jitter(arrow.get("_rough_seed", 0), edge_id, 7, amplitude=1.2)
        rendered_paths.append(
            f'  <path id="{normalize_attribute(edge_dom_id)}-review-stroke" data-graph-role="decoration" '
            f'data-owner="{normalize_attribute(edge_id)}" d="{path_d}" fill="none" stroke="{color}" '
            f'stroke-width="0.9" stroke-dasharray="2.5,2" opacity="0.30" '
            f'stroke-dashoffset="{round(rough_offset, 2)}"/>'
        )
    elif style_index == 11 and str(arrow.get("transit_type", "")) == "rail":
        rendered_paths.append(
            f'  <path id="{normalize_attribute(edge_dom_id)}-rail-casing" data-graph-role="decoration" '
            f'data-owner="{normalize_attribute(edge_id)}" d="{path_d}" fill="none" '
            f'stroke="{style_value(style, "rail_casing")}" stroke-width="{round(width + 3.2, 2)}" '
            f'stroke-linecap="round" stroke-linejoin="round" opacity="0.28"/>'
        )
    elif style_index == 12 and arrow.get("critical"):
        rendered_paths.append(
            f'  <path id="{normalize_attribute(edge_dom_id)}-critical-glow" data-graph-role="decoration" '
            f'data-owner="{normalize_attribute(edge_id)}" d="{path_d}" fill="none" stroke="{color}" '
            f'stroke-width="{round(width + 5, 2)}" stroke-linecap="round" stroke-linejoin="round" '
            f'opacity="0.22" filter="url(#pulseGlow)"/>'
        )
    path = (
        f'  <path id="{normalize_attribute(edge_dom_id)}" data-graph-role="edge" {shared_attributes} '
        f'data-bridges="{bridge_attr}" d="{path_d}" fill="none" stroke="{color}" '
        f'stroke-width="{width}" stroke-linecap="round" stroke-linejoin="round" marker-end="{marker}"'
    )
    if dash:
        path += f' stroke-dasharray="{dash}"'
    if arrow.get("opacity") is not None:
        path += f' opacity="{arrow["opacity"]}"'
    path += "/>"
    rendered_paths.append(path)
    if style_index == 11 and str(arrow.get("transit_type", "")) == "rail" and len(route) >= 2:
        direction_x = (route[0][0] + route[-1][0]) / 2
        direction_y = (route[0][1] + route[-1][1]) / 2
        rendered_paths.append(
            f'  <path id="{normalize_attribute(edge_dom_id)}-direction" data-graph-role="decoration" '
            f'data-owner="{normalize_attribute(edge_id)}" d="M {direction_x - 3.5} {direction_y - 3.5} '
            f'L {direction_x + 1.5} {direction_y} L {direction_x - 3.5} {direction_y + 3.5}" '
            f'fill="none" stroke="{style_value(style, "background")}" stroke-width="1.4" '
            f'stroke-linecap="round" stroke-linejoin="round"/>'
        )
    if style_index == 12 and arrow.get("critical") and len(route) >= 2:
        hop_x = (route[0][0] + route[-1][0]) / 2
        hop_y = (route[0][1] + route[-1][1]) / 2
        hop = int(arrow.get("critical_hop", 1))
        total_hops = int(arrow.get("critical_hops", 1))
        rendered_paths.extend(
            [
                f'  <circle id="{normalize_attribute(edge_dom_id)}-hop" data-graph-role="decoration" '
                f'data-owner="{normalize_attribute(edge_id)}" cx="{hop_x}" cy="{hop_y}" r="9" '
                f'fill="{style_value(style, "background")}" stroke="{color}" stroke-width="1.2"/>',
                f'  <text data-graph-role="decoration" data-owner="{normalize_attribute(edge_id)}" '
                f'x="{hop_x}" y="{hop_y + 2.7}" text-anchor="middle" font-size="7" font-weight="800" '
                f'fill="{color}">{hop}/{total_hops}</text>',
            ]
        )
    label_svg = ""
    label_bounds = None

    label = str(arrow.get("label", "")).strip()
    secondary_label = protocol.strip() if style_index == 9 else ""
    if style_index == 10 and not label and via.strip():
        label = via.strip()
    if label:
        label_proxy = label
        if secondary_label and geometry.estimate_text_width(secondary_label, 12) > geometry.estimate_text_width(label, 12):
            label_proxy = secondary_label
        label_x, label_y = choose_label_position_avoiding(
            route,
            label_proxy,
            label_obstacles,
            routes=existing_routes,
            canvas_bounds=canvas_bounds,
            dx=to_float(arrow.get("label_dx", 0)),
            dy=to_float(arrow.get("label_dy", -4)),
        )
        if secondary_label:
            label_content = render_dual_label_badge(label_x, label_y, label, secondary_label, style)
            label_bounds = estimate_dual_label_bounds(label_x, label_y, label, secondary_label)
        else:
            label_content = render_label_badge(label_x, label_y, label, style, label_style=str(arrow.get("label_style", "badge")))
            label_bounds = estimate_label_bounds(label_x, label_y, label)
        label_svg = (
            f'  <g id="{normalize_attribute(edge_dom_id)}-label" data-graph-role="label" '
            f'data-owner="{normalize_attribute(edge_id)}" data-graph-bounds="'
            f'{geometry.format_number(label_bounds[0])},{geometry.format_number(label_bounds[1])},'
            f'{geometry.format_number(label_bounds[2])},{geometry.format_number(label_bounds[3])}">\n'
            f'{label_content}\n  </g>'
        )

    report: Dict[str, object] = {
        "id": edge_id,
        "source": str(arrow.get("source", "")),
        "target": str(arrow.get("target", "")),
        "source_port": [round(start[0], 2), round(start[1], 2)],
        "target_port": [round(end[0], 2), round(end[1], 2)],
        "waypoints": [[to_float(point[0]), to_float(point[1])] for point in (arrow.get("route_points") or [])],
        "route": [[round(x, 2), round(y, 2)] for x, y in route],
        "length": round(geometry.route_length(route), 2),
        "bends": bends,
        "route_stretch": round(stretch, 3),
        "crossings": [[round(x, 2), round(y, 2)] for x, y in interactions.crossings],
        "bridges": [[round(x, 2), round(y, 2)] for x, y in bridges],
    }
    return ArrowRender(edge_id, "\n".join(rendered_paths), label_svg, label_bounds, route, report)


def render_legend(
    legend: Sequence[Dict[str, object]],
    style: Dict[str, object],
    width: float,
    height: float,
    data: Dict[str, object],
) -> str:
    layout = legend_layout(data, legend, width, height)
    if not layout:
        return ""
    legend_x, legend_y, bounds = layout
    lines = [
        '  <g id="legend" data-graph-role="legend">',
        f'    <rect id="legend-zone" data-graph-role="reserved" data-reserved-kind="legend" '
        f'x="{geometry.format_number(bounds[0])}" y="{geometry.format_number(bounds[1])}" '
        f'width="{geometry.format_number(bounds[2] - bounds[0])}" height="{geometry.format_number(bounds[3] - bounds[1])}" '
        f'rx="10" fill="none" stroke="none"/>',
    ]
    orientation = str(data.get("legend_orientation", "vertical")).strip().lower()
    cursor_x = legend_x
    for idx, item in enumerate(legend):
        y = legend_y if orientation == "horizontal" else legend_y + idx * 22
        color = item.get("color")
        if not color:
            color = style_value(style, "arrow_colors")[FLOW_ALIASES.get(str(item.get("flow", "control")).lower(), "control")]
        marker = marker_for_color(style, str(color), {"flow": item.get("flow", "control")})
        item_x = cursor_x if orientation == "horizontal" else legend_x
        lines.append(f'    <line data-graph-role="decoration" x1="{item_x}" y1="{y}" x2="{item_x + 30}" y2="{y}" stroke="{color}" stroke-width="{style_value(style, "arrow_width")}" marker-end="{marker}"/>')
        lines.append(f'    <text data-graph-role="decoration" x="{item_x + 40}" y="{y + 4}" class="legend">{normalize_text(item.get("label", ""))}</text>')
        if orientation == "horizontal":
            cursor_x += 40 + geometry.estimate_text_width(str(item.get("label", "")), 12) + 28
    if data.get("legend_box"):
        bg = data.get("legend_box_fill", style_value(style, "arrow_label_bg"))
        opacity = data.get("legend_box_opacity", 0.88)
        lines.insert(
            2,
            f'    <rect data-graph-role="decoration" x="{geometry.format_number(bounds[0])}" '
            f'y="{geometry.format_number(bounds[1])}" width="{geometry.format_number(bounds[2] - bounds[0])}" '
            f'height="{geometry.format_number(bounds[3] - bounds[1])}" rx="10" fill="{bg}" opacity="{opacity}"/>',
        )
    lines.append("  </g>")
    return "\n".join(lines)


def render_footer(data: Dict[str, object], style: Dict[str, object], width: float, height: float) -> str:
    layout = footer_layout(data, width, height)
    if not layout:
        return ""
    x, y, _ = layout
    text = str(data.get("footer", "")).strip()
    return f'  <text x="{x}" y="{y}" class="footnote">{normalize_text(text)}</text>'


def bounds_metadata(bounds: Bounds) -> str:
    return ",".join(geometry.format_number(value) for value in bounds)


def build_svg_with_report(template_type: str, data: Dict[str, object]) -> Tuple[str, Dict[str, object]]:
    diagram = normalize_diagram(data, template_type)
    source_data = diagram.as_dict()
    mode = diagram.mode

    # ``normalize_diagram`` resolves both the legacy ``style`` selector and
    # the v1 ``visual_theme`` selector.  Reuse that single decision here so a
    # conflicting or misspelled selector can never silently fall back to a
    # different renderer.
    style_index, style = parse_style(diagram.style_index)
    style["_style_index"] = style_index
    composition_contract = quality.resolve_contract(
        source_data.get("composition", source_data.get("quality_profile", "standard"))
    )
    if source_data.get("style_overrides"):
        style.update(source_data["style_overrides"])
    width, height = parse_template_viewbox(template_type)
    width = to_float(source_data.get("width", width))
    height = to_float(source_data.get("height", height))
    if source_data.get("viewBox"):
        match = re.match(r"0 0 ([0-9.]+) ([0-9.]+)", str(source_data["viewBox"]))
        if match:
            width = float(match.group(1))
            height = float(match.group(2))
    if not math.isfinite(width) or not math.isfinite(height) or width <= 0 or height <= 0:
        raise ValueError("canvas width and height must be finite positive numbers")

    containers = list(source_data.get("containers", []))
    nodes_data = list(source_data.get("nodes", []))
    arrows_data = list(source_data.get("arrows", []))
    legend = list(source_data.get("legend", []))

    if style_index == 9:
        rough_seed = source_data.get("rough_seed", 0)
        for node_data in nodes_data:
            node_data.setdefault("_rough_seed", rough_seed)
        for container in containers:
            container.setdefault("_rough_seed", rough_seed)
        for arrow_data in arrows_data:
            arrow_data.setdefault("_rough_seed", rough_seed)

    defs = render_defs(style_index, style)
    canvas = render_canvas(style_index, style, width, height)
    title_block, content_start_y = render_title_block(style, source_data, width)
    window_controls = render_window_controls(source_data, style_index, width)
    header_meta = render_header_meta(source_data, style, width)
    style_signature = render_style_signature(style_index, source_data, width)

    # Assign auto_place y before building node maps so arrows route correctly
    for node_data in nodes_data:
        if "y" not in node_data and node_data.get("auto_place"):
            node_data["y"] = content_start_y + to_float(node_data.get("offset_y", 0))

    normalized_nodes = [normalize_node(node, f"node-{idx}") for idx, node in enumerate(nodes_data)]
    if len({node.node_id for node in normalized_nodes}) != len(normalized_nodes):
        raise ValueError("node ids must be unique")
    node_map = {node.node_id: node for node in normalized_nodes}

    # Raw semantic IDs stay intact in data attributes and reports. SVG DOM IDs
    # are allocated separately so normalization collisions (for example
    # ``a b`` vs ``a-b`` or multiple CJK-only IDs) cannot create duplicate IDs.
    used_dom_ids = set(_RESERVED_DOM_IDS)
    for index, container in enumerate(containers):
        raw_id = str(container.get("id") or f"container-{index:03d}")
        container["_dom_id"] = allocate_dom_identifier(
            safe_identifier(raw_id, f"container-{index:03d}"), used_dom_ids, ("-header",)
        )
    for index, arrow in enumerate(arrows_data):
        raw_id = str(arrow.get("id") or f"edge-{index:03d}")
        arrow["_dom_id"] = allocate_dom_identifier(
            safe_identifier(raw_id, f"edge-{index:03d}"), used_dom_ids, _EDGE_DOM_SUFFIXES
        )
    for node, node_data in zip(normalized_nodes, nodes_data):
        node_data["_dom_id"] = allocate_dom_identifier(
            f"node-{safe_identifier(node.node_id, 'node')}", used_dom_ids
        )

    section_obstacles = [bounds for container in containers if (bounds := container_header_bounds(container, style)) is not None]
    footer_reserved = footer_layout(source_data, width, height)
    blueprint_block_svg, blueprint_block_bounds = render_blueprint_title_block(source_data, style, style_index, width, height)
    node_obstacles = [node.bounds for node in normalized_nodes]
    placement_obstacles = list(node_obstacles) + list(section_obstacles)
    if footer_reserved:
        placement_obstacles.append(footer_reserved[2])
    if blueprint_block_bounds:
        placement_obstacles.append(blueprint_block_bounds)
    legend_placement = resolve_legend_layout(
        source_data,
        legend,
        width,
        height,
        placement_obstacles,
        arrows_data,
        node_map,
    )
    legend_reserved = legend_layout(source_data, legend, width, height)

    reserved_bounds = list(section_obstacles)
    if legend_reserved:
        reserved_bounds.append(legend_reserved[2])
    if footer_reserved:
        reserved_bounds.append(footer_reserved[2])
    if blueprint_block_bounds:
        reserved_bounds.append(blueprint_block_bounds)

    route_obstacles = node_obstacles + reserved_bounds
    label_obstacles = node_obstacles + reserved_bounds
    prepared_arrows = prepare_arrows(arrows_data, node_map)
    rendered_by_index: Dict[int, ArrowRender] = {}
    existing_routes: List[Sequence[Point]] = []
    routing_order = sorted(
        range(len(prepared_arrows)),
        key=lambda index: (0 if prepared_arrows[index].get("route_points") else 1, index),
    )
    issues: List[Dict[str, object]] = []
    for index in routing_order:
        rendered = render_arrow(
            prepared_arrows[index],
            style,
            node_map,
            route_obstacles,
            label_obstacles,
            existing_routes=existing_routes,
            canvas_bounds=(0.0, 0.0, width, height),
        )
        rendered_by_index[index] = rendered
        existing_routes.append(rendered.route)
        if rendered.label_bounds:
            label_obstacles.append(rendered.label_bounds)
            route_obstacles.append(rendered.label_bounds)
        if rendered.report["bridges"]:
            issues.append(
                {
                    "severity": "info",
                    "code": "EDGE_CROSSING_BRIDGED",
                    "element": rendered.edge_id,
                    "coordinates": rendered.report["bridges"],
                }
            )

    contract_data = composition_contract.as_dict()
    visual_theme = STYLE_NAMES[style_index]
    semantic_profile = str(diagram.semantic_report.get("profile", "generic"))
    diagram_type = str(source_data.get("diagram_type", mode))
    motion_scene = str(source_data.get("motion_scene", ""))
    lines = [
        f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {int(width)} {int(height)}" '
        f'width="{int(width)}" height="{int(height)}" data-generator="fireworks-tech-graph" '
        f'data-schema-version="1" data-text-metrics="heuristic-v1" '
        f'data-style-id="{style_index}" data-visual-theme="{normalize_attribute(visual_theme)}" '
        f'data-diagram-type="{normalize_attribute(diagram_type)}" '
        f'data-motion-scene="{normalize_attribute(motion_scene)}" '
        f'data-semantic-profile="{normalize_attribute(semantic_profile)}" data-semantic-valid="true" '
        f'data-quality-profile="{normalize_attribute(composition_contract.profile)}" '
        f'data-max-bends-per-edge="{contract_data["max_bends_per_edge"]}" '
        f'data-max-total-bends="{contract_data["max_total_bends"]}" '
        f'data-max-route-stretch="{contract_data["max_route_stretch"]}" '
        f'data-max-bridged-crossings="{contract_data["max_bridged_crossings"]}" '
        f'data-min-node-gap="{contract_data["min_node_gap"]}" '
        f'data-min-container-gutter="{contract_data["min_container_gutter"]}" '
        f'data-min-label-clearance="{contract_data["min_label_clearance"]}" '
        f'data-min-segment-length="{contract_data["min_segment_length"]}">'
    ]
    lines.append(defs)
    lines.append(canvas)
    if window_controls:
        lines.append(window_controls)
    if header_meta:
        lines.append(header_meta)
    lines.append(title_block)
    if style_signature:
        lines.append(style_signature)

    for index, container in enumerate(containers):
        container_id = str(container.get("_dom_id") or safe_identifier(container.get("id"), f"container-{index:03d}"))
        container_bounds = rectangle_bounds(
            to_float(container["x"]),
            to_float(container["y"]),
            to_float(container["width"]),
            to_float(container["height"]),
        )
        lines.append(
            f'  <g id="{normalize_attribute(container_id)}" data-graph-role="container" '
            f'data-container-id="{normalize_attribute(container.get("id", ""))}" '
            f'data-semantic-role="{normalize_attribute(container.get("deployment_kind", container.get("c4_type", "boundary")))}" '
            f'data-graph-bounds="{bounds_metadata(container_bounds)}">'
        )
        lines.append(render_section(container, style))
        header_bounds = container_header_bounds(container, style)
        if header_bounds:
            lines.append(
                f'    <rect id="{normalize_attribute(container_id)}-header" data-graph-role="reserved" '
                f'data-reserved-kind="container-header" x="{geometry.format_number(header_bounds[0])}" '
                f'y="{geometry.format_number(header_bounds[1])}" width="{geometry.format_number(header_bounds[2] - header_bounds[0])}" '
                f'height="{geometry.format_number(header_bounds[3] - header_bounds[1])}" fill="none" stroke="none"/>'
            )
        lines.append("  </g>")

    # A bridge is assigned to the edge routed after an existing edge. Preserve
    # that order in the SVG paint stack so the bridge mask erases the lower
    # edge before the bridge owner is painted on top.
    lines.extend(rendered_by_index[index].path_svg for index in routing_order)

    for node, node_data in zip(normalized_nodes, nodes_data):
        node_id = str(node_data.get("_dom_id") or f"node-{safe_identifier(node.node_id, 'node')}")
        semantic_role = node_data.get(
            "c4_type",
            node_data.get("deployment_kind", node_data.get("transit_role", node_data.get("ops_role", node_data.get("kind", "node")))),
        )
        lines.append(
            f'  <g id="{normalize_attribute(node_id)}" data-graph-role="node" '
            f'data-node-id="{normalize_attribute(node.node_id)}" '
            f'data-semantic-role="{normalize_attribute(semantic_role)}" '
            f'data-motion-role="{normalize_attribute(node_data.get("motion_role", ""))}" '
            f'data-motion-stage="{normalize_attribute(node_data.get("motion_stage", ""))}" '
            f'data-motion-order="{normalize_attribute(node_data.get("motion_order", ""))}" '
            f'data-parent="{normalize_attribute(node_data.get("parent", ""))}" '
            f'data-deployment-id="{normalize_attribute(node_data.get("deployment_id", ""))}" '
            f'data-topic-id="{normalize_attribute(node_data.get("topic_id", ""))}" '
            f'data-span-id="{normalize_attribute(node_data.get("span_id", ""))}" '
            f'data-station-order="{normalize_attribute(node_data.get("station_order", ""))}" '
            f'data-status="{normalize_attribute(node_data.get("status", ""))}" '
            f'data-start-ms="{normalize_attribute(node_data.get("start_ms", ""))}" '
            f'data-duration-ms="{normalize_attribute(node_data.get("duration_ms", ""))}" '
            f'data-parent-span="{normalize_attribute(node_data.get("parent_span", ""))}" '
            f'data-graph-bounds="{bounds_metadata(node.bounds)}">'
        )
        lines.append(render_node(node_data, style))
        lines.append("  </g>")

    lines.extend(
        rendered_by_index[index].label_svg
        for index in range(len(prepared_arrows))
        if rendered_by_index[index].label_svg
    )

    legend_svg = render_legend(legend, style, width, height, source_data)
    if legend_svg:
        lines.append(legend_svg)

    if blueprint_block_svg:
        if blueprint_block_bounds:
            lines.append(
                f'  <g id="blueprint-title-block" data-graph-role="reserved" '
                f'data-graph-bounds="{bounds_metadata(blueprint_block_bounds)}">'
            )
            lines.append(blueprint_block_svg)
            lines.append("  </g>")
        else:
            lines.append(blueprint_block_svg)

    footer_svg = render_footer(source_data, style, width, height)
    if footer_svg:
        if footer_reserved:
            lines.append(
                f'  <g id="footer" data-graph-role="reserved" data-graph-bounds="{bounds_metadata(footer_reserved[2])}">'
            )
            lines.append(footer_svg)
            lines.append("  </g>")
        else:
            lines.append(footer_svg)

    lines.append("</svg>")
    composition = quality.assess_composition(
        nodes=[(node.node_id, node.bounds) for node in normalized_nodes],
        containers=[
            (
                str(container.get("id") or f"container-{index:03d}"),
                rectangle_bounds(
                    to_float(container["x"]),
                    to_float(container["y"]),
                    to_float(container["width"]),
                    to_float(container["height"]),
                ),
            )
            for index, container in enumerate(containers)
        ],
        edges=[rendered_by_index[index].report for index in range(len(prepared_arrows))],
        contract=composition_contract,
    )
    if not composition["ok"]:
        summary = "; ".join(
            f'{item["code"]}:{item["element"]}={item["actual"]}>{item["limit"]}'
            for item in composition["violations"]
        )
        raise ValueError(f"COMPOSITION_QUALITY: {summary}")

    report: Dict[str, object] = {
        "schema_version": 1,
        "input_schema": diagram.input_schema,
        "mode": mode,
        "style": {"id": style_index, "name": visual_theme},
        "semantics": dict(diagram.semantic_report),
        "ok": True,
        "canvas": {"width": round(width, 2), "height": round(height, 2)},
        "text_metrics": "heuristic-v1",
        "placements": {
            "legend": {
                key: ([round(item, 2) for item in value] if key == "bounds" else value)
                for key, value in legend_placement.items()
            }
            if legend_placement
            else None
        },
        "edges": [rendered_by_index[index].report for index in range(len(prepared_arrows))],
        "composition": composition,
        "issues": issues,
        "summary": {
            "nodes": len(nodes_data),
            "edges": len(prepared_arrows),
            "bridged_crossings": sum(len(rendered.report["bridges"]) for rendered in rendered_by_index.values()),
        },
    }
    return "\n".join(line for line in lines if line), report


def build_svg(template_type: str, data: Dict[str, object]) -> str:
    return build_svg_with_report(template_type, data)[0]


def parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("template_type")
    parser.add_argument("output_path")
    parser.add_argument("data_json", nargs="?")
    parser.add_argument("--layout-report", "--report", dest="layout_report")
    return parser.parse_args(argv)


def main(argv: Optional[Sequence[str]] = None) -> None:
    args = parse_args(argv)
    try:
        if args.data_json is not None:
            data = json.loads(args.data_json)
        else:
            data = json.load(sys.stdin)
        svg_content, report = build_svg_with_report(args.template_type, data)
        os.makedirs(os.path.dirname(os.path.abspath(args.output_path)), exist_ok=True)
        with open(args.output_path, "w", encoding="utf-8") as handle:
            handle.write(svg_content)
        if args.layout_report:
            os.makedirs(os.path.dirname(os.path.abspath(args.layout_report)), exist_ok=True)
            with open(args.layout_report, "w", encoding="utf-8") as handle:
                json.dump(report, handle, ensure_ascii=False, indent=2, sort_keys=True)
                handle.write("\n")
        print(f"✓ SVG generated: {args.output_path}")
        if args.layout_report:
            print(f"✓ Layout report: {args.layout_report}")
    except FileNotFoundError as exc:
        print(f"Error: {exc}")
        sys.exit(1)
    except json.JSONDecodeError as exc:
        print(f"Error: Invalid JSON: {exc}")
        sys.exit(1)
    except ValueError as exc:
        if args.layout_report:
            os.makedirs(os.path.dirname(os.path.abspath(args.layout_report)), exist_ok=True)
            failure_report = {
                "schema_version": 1,
                "ok": False,
                "issues": [{"severity": "error", "code": "LAYOUT_ERROR", "message": str(exc)}],
            }
            with open(args.layout_report, "w", encoding="utf-8") as handle:
                json.dump(failure_report, handle, ensure_ascii=False, indent=2, sort_keys=True)
                handle.write("\n")
        print(f"Error: {exc}")
        sys.exit(1)
    except Exception as exc:  # pragma: no cover
        print(f"Unexpected error: {exc}")
        sys.exit(1)


if __name__ == "__main__":
    main()
skills/fireworks-tech-graph/docs/releases/v1.0.3.md
# Fireworks Tech Graph v1.0.3

This patch release made the installation source explicit and separated the Agent Skill installer path from the npm distribution page.

## Version information

- Version: `v1.0.3` / npm `1.0.3`
- Version date: 2026-04-12; npm published at `2026-04-12T13:28:10.195Z`
- Tag commit: [`c3726124`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/c37261248ca674d5d94e24d9d7d85e50a88c77d1), whose versioned content matches the npm tarball
- npm registry `gitHead`: [`921fb308`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/921fb30855cd76e03e1b96ff3463028ddcd36deb)
- Previous version: [`v1.0.2`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/releases/tag/v1.0.2)
- Runtime metadata: Node.js 14 or newer
- Distribution: [npm package `1.0.3`](https://www.npmjs.com/package/@yizhiyanhua-ai/fireworks-tech-graph/v/1.0.3)

## Highlights

- Standardized installation on `npx skills add yizhiyanhua-ai/fireworks-tech-graph`.
- Clarified that `skills add` resolves the GitHub repository while npm provides package distribution and README discovery.

## Provenance

This GitHub Release was backfilled on 2026-07-17. npm `1.0.3` was published 13 seconds before the matching repository commit, so the registry retained the preceding `gitHead`. The historical tag uses the subsequent commit because its README, Chinese README, Skill instructions, and `package.json` match the published versioned content.
skills/fireworks-tech-graph/docs/releases/v1.0.2.md
# Fireworks Tech Graph v1.0.2

This release introduced the style-driven generator and a reproducible fixture-and-template workflow for the original seven styles.

## Version information

- Version: `v1.0.2` / npm `1.0.2`
- Version date: 2026-04-12; npm published at `2026-04-12T13:24:35.073Z`
- Tag commit: [`921fb308`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/921fb30855cd76e03e1b96ff3463028ddcd36deb), whose complete file set matches the npm tarball
- npm registry `gitHead`: [`fee15924`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/fee15924e5d9a273d270067dce6380f27de3d033)
- Previous version: [`v1.0.1`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/releases/tag/v1.0.1)
- Runtime metadata: Node.js 14 or newer
- Distribution: [npm package `1.0.2`](https://www.npmjs.com/package/@yizhiyanhua-ai/fireworks-tech-graph/v/1.0.2)

## Highlights

- Added a Python style-driven generator, seven scenario fixtures, and ten reusable SVG diagram templates.
- Added formal npm package metadata for repository, license, files, runtime, and discovery keywords.
- Expanded the package payload to include `fixtures/` and `templates/`.
- Refreshed all seven showcase renders and strengthened SVG validation and all-style test scripts.
- Added a style-to-diagram selection matrix for more reliable prompting.

## Provenance

This GitHub Release was backfilled on 2026-07-17. Every one of the 44 files in the published npm tarball matches commit `921fb308` byte-for-byte. The registry records the preceding `gitHead` because publication occurred about 34 seconds before the packaging commit was created; the historical tag therefore uses the content-matching commit.
skills/fireworks-tech-graph/assets/icons/cloud/manifest-v1.json
{
  "schema_version": 1,
  "version": "2026.07-neutral.1",
  "policy": "Built-in provider-neutral glyphs. Official vendor packs remain external, versioned adapters.",
  "official_sources": {
    "aws": "https://aws.amazon.com/architecture/icons/",
    "azure": "https://learn.microsoft.com/azure/architecture/icons/",
    "gcp": "https://cloud.google.com/icons"
  },
  "icons": [
    {
      "id": "generic:traffic",
      "aliases": ["global traffic", "dns", "cdn"],
      "badge": "EDGE",
      "category": "edge",
      "color": "#2563eb",
      "glyph": "globe"
    },
    {
      "id": "generic:gateway",
      "aliases": ["load balancer", "api gateway"],
      "badge": "GW",
      "category": "network",
      "color": "#7c3aed",
      "glyph": "gateway"
    },
    {
      "id": "generic:compute",
      "aliases": ["service", "container", "kubernetes workload"],
      "badge": "APP",
      "category": "compute",
      "color": "#059669",
      "glyph": "compute"
    },
    {
      "id": "generic:database",
      "aliases": ["relational database", "managed database"],
      "badge": "DB",
      "category": "data",
      "color": "#ea580c",
      "glyph": "database"
    },
    {
      "id": "generic:stream",
      "aliases": ["event stream", "message bus"],
      "badge": "BUS",
      "category": "integration",
      "color": "#db2777",
      "glyph": "stream"
    },
    {
      "id": "generic:observability",
      "aliases": ["metrics", "telemetry"],
      "badge": "OBS",
      "category": "operations",
      "color": "#0891b2",
      "glyph": "observe"
    }
  ]
}
skills/fireworks-tech-graph/docs/releases/v1.0.4.md
# Fireworks Tech Graph v1.0.4

This patch release documented a deterministic update path for existing Skill installations.

## Version information

- Version: `v1.0.4` / npm `1.0.4`
- Version date: 2026-04-12; npm published at `2026-04-12T13:32:37.231Z`
- Tag commit: [`c8668407`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/c8668407ebfa72e00719be8f4312b71ab90c3c3e), whose versioned content matches the npm tarball
- npm registry `gitHead`: [`c3726124`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/c37261248ca674d5d94e24d9d7d85e50a88c77d1)
- Previous version: [`v1.0.3`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/releases/tag/v1.0.3)
- Runtime metadata: Node.js 14 or newer
- Distribution: [npm package `1.0.4`](https://www.npmjs.com/package/@yizhiyanhua-ai/fireworks-tech-graph/v/1.0.4); this remains the npm registry's `latest` version for the legacy package line

## Highlights

- Added `npx skills add yizhiyanhua-ai/fireworks-tech-graph --force -g -y` as the documented update command.
- Synchronized the update workflow across the English README, Chinese README, and Skill instructions.

## Provenance

This GitHub Release was backfilled on 2026-07-17. npm `1.0.4` was published 11 seconds before the matching repository commit, so the registry retained the preceding `gitHead`. The historical tag uses the subsequent commit because its README, Chinese README, Skill instructions, and `package.json` match the published versioned content.
skills/fireworks-tech-graph/docs/releases/v1.0.5.md
# Fireworks Tech Graph v1.0.5

This repository release consolidated three months of diagram-quality work and made the Skill directly usable by both Codex and Claude Code.

## Version information

- Version: `v1.0.5`
- Version date: 2026-07-11
- Tag commit: [`14be3ad3`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/14be3ad3b05389a5d603562c207eb37157637127), the mainline merge of version commit [`55916c55`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/55916c552300a7475b8f8a57cc62c53e2034b016); both have the same tree
- Previous version: [`v1.0.4`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/releases/tag/v1.0.4)
- Runtime metadata: Node.js 14 or newer
- Distribution: repository version only; npm publishing stopped at `1.0.4`

## Highlights

- Added shared Codex and Claude Code metadata, instructions, compatibility tests, and installation paths.
- Added Style 8 Dark Luxury while preserving the seven original style contracts.
- Added a bounded generate-validate-repair loop, structural SVG checks, and optional visual self-review.
- Improved connector routing, label placement, overlap prevention, filter and viewport clipping guidance, and CJK font fallbacks.
- Switched the default PNG renderer to CairoSVG, hardened helper scripts, and documented renderer edge cases.
- Added the first GitHub Pages product showcase.

## Provenance

This GitHub Release was backfilled on 2026-07-17. Unlike npm versions `1.0.0` through `1.0.4`, the `v1.0.5` tag points to a committed mainline tree whose `package.json` declares `1.0.5`; no npm `1.0.5` package was published.
skills/fireworks-tech-graph/SKILL.md
---
name: fireworks-tech-graph
description: >-
  Create technical diagrams such as software architecture, data flow,
  flowcharts, sequence diagrams, C4 reviews, cloud deployments, event streams,
  observability investigations, agent/memory systems, UML, ER, network
  topology, timelines, and technical concept maps, then export SVG, PNG,
  focused semantic SVG-to-GIF motion, or offline interactive HTML. Treat direct
  requests such as "Generate a GIF", "生成 GIF", or "制作 GIF" as motion requests,
  and use this skill when the user asks to visualize a system or engineering
  concept. Do not use for photos, raster artwork, or quantitative data charts.
---

# Fireworks Tech Graph

Generate geometry-checked SVG technical diagrams, high-resolution PNG, validated SVG-to-GIF semantic motion, and sanitized offline interactive HTML.

## Runtime Compatibility

Use this repository unchanged in both Codex and Claude Code. It follows the Agent Skills layout: `SKILL.md` is the shared entry point, bundled resources use relative paths, and `agents/openai.yaml` adds optional Codex UI metadata without affecting Claude Code.

Before reading a reference or running a script, resolve the directory containing this `SKILL.md` as `SKILL_ROOT`. Do not assume the current working directory is the skill directory, and do not assume a variable set in one shell call persists into the next.

- In Claude Code, use `${CLAUDE_SKILL_DIR}`.
- In Codex, use the absolute skill directory shown in the loaded skill metadata.

Every command block below sets `SKILL_ROOT` itself. In Codex, replace `/absolute/path/from-codex-skill-metadata` with the absolute skill directory before running the command.

## Helper Scripts (Recommended)

The unified `scripts/fireworks.py` CLI and compatibility helpers provide stable rendering, geometry validation, inspection, animation, and export:

### 1. `generate-diagram.sh` - Validate SVG + export PNG
```bash
SKILL_ROOT="${CLAUDE_SKILL_DIR:-/absolute/path/from-codex-skill-metadata}"
"$SKILL_ROOT/scripts/generate-diagram.sh" -t architecture -s 1 -o ./output/arch.svg
```
- Validates an existing SVG file
- Exports PNG after validation
- Example: `"$SKILL_ROOT/scripts/generate-diagram.sh" -t architecture -s 1 -o ./output/arch.svg`

### 2. `generate-from-template.py` - Create starter SVG from template
```bash
SKILL_ROOT="${CLAUDE_SKILL_DIR:-/absolute/path/from-codex-skill-metadata}"
mkdir -p ./output
python3 "$SKILL_ROOT/scripts/generate-from-template.py" architecture ./output/arch.svg '{"title":"My Diagram","nodes":[],"arrows":[]}'
```
- Loads a built-in SVG template
- Renders nodes, arrows, and legend entries from JSON input
- Escapes text content to keep output XML-valid

### 3. `validate-svg.sh` - Validate SVG syntax
```bash
SKILL_ROOT="${CLAUDE_SKILL_DIR:-/absolute/path/from-codex-skill-metadata}"
"$SKILL_ROOT/scripts/validate-svg.sh" <svg-file>
```
- Checks XML syntax
- Verifies tag balance
- Validates marker references
- Checks attribute completeness
- Validates path data

### 4. `test-all-styles.sh` - Batch test all styles
```bash
SKILL_ROOT="${CLAUDE_SKILL_DIR:-/absolute/path/from-codex-skill-metadata}"
"$SKILL_ROOT/scripts/test-all-styles.sh"
```
- Tests multiple diagram sizes
- Validates all generated SVGs
- Generates test report

**When to use scripts:**
- Use scripts when generating complex SVGs to avoid syntax errors
- Scripts provide automatic validation and error reporting
- Recommended for production diagrams

## Workflow (Always Follow This Order)

1. **Classify** the diagram type (see Diagram Types below)
2. **Extract structure** — identify layers, nodes, edges, flows, and semantic groups from user description
3. **Plan layout** — load `$SKILL_ROOT/references/composition-quality-contract.md`, then apply the diagram-type layout rules
4. **Load style reference** — always load `$SKILL_ROOT/references/style-1-flat-icon.md` unless user specifies another; load the matching `$SKILL_ROOT/references/style-N-*.md` for exact color tokens and SVG patterns
5. **Select semantic contract** — Styles 9–12 default to `c4-review`, `cloud-fabric`, `event-transit`, and `ops-pulse`; use `scripts/fireworks.py validate` before layout so missing or contradictory engineering facts fail closed
6. **Map nodes to shapes** — use Shape Vocabulary below
7. **Check icon needs** — load `$SKILL_ROOT/references/icons.md` for known products
8. **Write SVG** with adaptive strategy (see SVG Generation Strategy below)
9. **Validate**: Run `"$SKILL_ROOT/scripts/validate-svg.sh" file.svg` to check XML, markers, geometry, composition budgets, and renderability
10. **Export PNG**: Use `cairosvg` (recommended). Load `$SKILL_ROOT/references/png-export.md` when choosing another renderer
11. **Animate on request** — `让这张图动起来` / `生成 GIF` / `制作 GIF` / `Animate this diagram` / `Generate a GIF` means the latest generated semantic SVG to one GIF with `auto`, 5.75s, 20fps, and 960px width when that SVG satisfies one of the 12 approved motion contracts. Exact source bytes are not pinned, but role/stage/order coverage, route directions, required colors, and geometry fail closed; do not claim arbitrary same-style topologies are supported. Load `$SKILL_ROOT/references/motion-effects.md`, run `fireworks.py animate`, and report SVG/GIF/report paths. GIF is the only motion media format, while the default command also emits `<output>.motion.json`. Styles 1–12 are enabled, and their contracts plus the shared `+2s-settled-flow` timing revision are user-approved; the default keeps frames 38–109 at full opacity and resets on frames 110–114. Every scene begins connector-free and advances its moving primitives toward each target. The 75-vs-115 compatibility gate counts binary-exact frames first, decoded-RGBA-exact frames second, and permits a guarded antialias equivalent only when AE ≤ 128, normalized RMSE ≤ 0.001, every difference component is at most 2px wide or high, and all differences remain on edge or node borders; DOM and signature geometry stay strict-exact. Reject raster animation inputs and non-GIF motion outputs. Explicit 3.75s/75-frame and 2.75s/55-frame timelines remain supported
12. **Visual review gate** — if your runtime can read images, load the exported PNG back and inspect it. Syntactic validity does not guarantee visual correctness: arrows may cross through component interiors, labels may collide with lifelines or other labels, boxes may overlap, alt-frame text may sit on top of a message, or a legend may cover content. If you see any of these, revise the SVG and re-export, with at most two focused correction passes. Common fixes:
    - Route arrows through gaps between boxes, not through box interiors
    - Move arrow labels 6-8px away from the arrow line (offset-first); add background rects only when offset is insufficient
    - Widen inter-row/inter-column gutters so same-layer arrows have clear corridors
    - Collapse repeated cross-layer arrows into a single "delegates down" rail outside the content area
    - Move legend/notes out of any region where arrows or labels land
    - Increase viewBox height/width rather than packing elements tighter
    - If a filtered element (drop-shadow, blur) is missing one side of its border, move it ≥30px away from that viewBox edge, or remove the filter and rely on color/contrast for visual separation
  Report `visual_review: passed` after inspection. If image reading is unavailable, report `visual_review: skipped (image reader unavailable)` — do not guess or claim visual correctness.

## Rule Precedence

Use this order when instructions disagree:

1. The user's explicit content and style request
2. The selected `$SKILL_ROOT/references/style-N-*.md` visual tokens (palette, typography, corner radius, shadow treatment)
3. Diagram-type layout rules and semantic flow requirements in this file
4. Universal defaults and examples

Geometry and validation gates always remain active: style guidance cannot justify unreadable text, missing marker definitions, or arrows crossing component interiors. Tables in this file define semantic defaults; a selected style may override their colors and stroke treatment while preserving the meaning and direction of each flow.

## Diagram Types & Layout Rules

### Architecture Diagram
Nodes = services/components. Group into **horizontal layers** (top→bottom or left→right).
- Typical layers: Client → Gateway/LB → Services → Data/Storage
- Use `<rect>` dashed containers to group related services in the same layer
- Arrow direction follows data/request flow
- ViewBox: `0 0 960 600` standard, `0 0 960 800` for tall stacks

### Data Flow Diagram
Emphasizes **what data moves where**. Focus on data transformation.
- Label every arrow with the data type (e.g., "embeddings", "query", "context")
- Use wider arrows (`stroke-width: 2.5`) for primary data paths
- Dashed arrows for control/trigger flows
- Color arrows by data category (not just Agent/RAG — use semantics)

### Flowchart / Process Flow
Sequential decision/process steps.
- Top-to-bottom preferred; left-to-right for wide flows
- Diamond shapes for decisions, rounded rects for processes, parallelograms for I/O
- Keep node labels short (≤3 words); put detail in sub-labels
- Align nodes on a grid: x positions snap to 120px intervals, y to 80px

### Agent Architecture Diagram
Shows how an AI agent reasons, uses tools, and manages memory.
Key conceptual layers to always consider:
- **Input layer**: User, query, trigger
- **Agent core**: LLM, reasoning loop, planner
- **Memory layer**: Short-term (context window), Long-term (vector/graph DB), Episodic
- **Tool layer**: Tool calls, APIs, search, code execution
- **Output layer**: Response, action, side-effects
Use cyclic arrows (loop arcs) to show iterative reasoning. Separate memory types visually.

### Memory Architecture Diagram (Mem0, MemGPT-style)
Specialized agent diagram focused on memory operations.
- Show memory **write path** and **read path** separately (different arrow colors)
- Memory tiers: Working Memory → Short-term → Long-term → External Store
- Label memory operations: `store()`, `retrieve()`, `forget()`, `consolidate()`
- Use stacked rects or layered cylinders for storage tiers

### Sequence Diagram
Time-ordered message exchanges between participants.
- Participants as vertical **lifelines** (top labels + vertical dashed lines)
- Messages as horizontal arrows between lifelines, top-to-bottom time order
- Activation boxes (thin filled rects on lifeline) show active processing
- Group with `<rect>` loop/alt frames with label in top-left corner
- ViewBox height = 80 + (num_messages × 50)

### Comparison / Feature Matrix
Side-by-side comparison of approaches, systems, or components.
- Column headers = systems, row headers = attributes
- Row height: 40px; column width: min 120px; header row height: 50px
- Checked cell: tinted background (e.g. `#dcfce7`) + `✓` checkmark; unsupported: `#f9fafb` fill
- Alternating row fills (`#f9fafb` / `#ffffff`) for readability
- Max readable columns: 5; beyond that, split into two diagrams

### Timeline / Gantt
Horizontal time axis showing durations, phases, and milestones.
- X-axis = time (weeks/months/quarters); Y-axis = items/tasks/phases
- Bars: rounded rects, colored by category, labeled inside or beside
- Milestone markers: diamond or filled circle at specific x position with label above
- ViewBox: `0 0 960 400` typical; wider for many time periods: `0 0 1200 400`

### Mind Map / Concept Map
Radial layout from central concept.
- Central node at `cx=480, cy=280`
- First-level branches: evenly distributed around center (360/N degrees)
- Second-level branches: branch off first-level at 30-45° offset
- Use curved `<path>` with cubic bezier for branches, not straight lines

### Class Diagram (UML)
Static structure showing classes, attributes, methods, and relationships.
- **Class box**: 3-compartment rect (name / attributes / methods), min width 160px
  - Top compartment: class name, bold, centered (abstract = *italic*)
  - Middle: attributes with visibility (`+` public, `-` private, `#` protected)
  - Bottom: method signatures, same visibility notation
- **Relationships**:
  - Inheritance (extends): solid line + hollow triangle arrowhead, child → parent
  - Implementation (interface): dashed line + hollow triangle, class → interface
  - Association: solid line + open arrowhead, label with multiplicity (1, 0..*, 1..*)
  - Aggregation: solid line + hollow diamond on container side
  - Composition: solid line + filled diamond on container side
  - Dependency: dashed line + open arrowhead
- **Interface**: `<<interface>>` stereotype above name, or circle/lollipop notation
- **Enum**: compartment rect with `<<enumeration>>` stereotype, values in bottom
- Layout: parent classes top, children below; interfaces to the left/right of implementors
- ViewBox: `0 0 960 600` standard; `0 0 960 800` for deep hierarchies

### Use Case Diagram (UML)
System functionality from user perspective.
- **Actor**: stick figure (circle head + body line) placed outside system boundary
  - Label below figure, 13-14px
  - Primary actors on left, secondary/supporting on right
- **Use case**: ellipse with label centered inside, min 140×60px
  - Keep names verb phrases: "Create Order", "Process Payment"
- **System boundary**: large rect with dashed border + system name in top-left
- **Relationships**:
  - Include: dashed arrow `<<include>>` from base to included use case
  - Extend: dashed arrow `<<extend>>` from extension to base use case
  - Generalization: solid line + hollow triangle (specialized → general)
- Layout: system boundary centered, actors outside, use cases inside
- ViewBox: `0 0 960 600` standard

### State Machine Diagram (UML)
Lifecycle states and transitions of an entity.
- **State**: rounded rect with state name, min 120×50px
  - Internal activities: small text `entry/ action`, `exit/ action`, `do/ activity`
  - **Initial state**: filled black circle (r=8), one outgoing arrow
  - **Final state**: filled circle (r=8) inside hollow circle (r=12)
  - **Choice**: small hollow diamond, guard labels on outgoing arrows `[condition]`
- **Transition**: arrow with optional label `event [guard] / action`
  - Guard conditions in square brackets
  - Actions after `/`
- **Composite/nested state**: larger rect containing sub-states, with name tab
- **Fork/join**: thick horizontal or vertical black bar (synchronization)
- Layout: initial state top-left, final state bottom-right, flow top-to-bottom
- ViewBox: `0 0 960 600` standard

### ER Diagram (Entity-Relationship)
Database schema and data relationships.
- **Entity**: rect with entity name in header (bold), attributes below
  - Primary key attribute: underlined
  - Foreign key: italic or marked with (FK)
  - Min width: 160px; attribute font-size: 12px
- **Relationship**: diamond shape on connecting line
  - Label inside diamond: "has", "belongs to", "enrolls in"
  - Cardinality labels near entity: `1`, `N`, `0..1`, `0..*`, `1..*`
- **Weak entity**: double-bordered rect with double diamond relationship
- **Associative entity**: diamond + rect hybrid (rect with diamond inside)
- Line style: solid for identifying relationships, dashed for non-identifying
- Layout: entities in 2-3 rows, relationships between related entities
- ViewBox: `0 0 960 600` standard; wider `0 0 1200 600` for many entities

### Network Topology
Physical or logical network infrastructure.
- **Devices**: icon-like rects or rounded rects
  - Router: circle with cross arrows
  - Switch: rect with arrow grid
  - Server: stacked rect (rack icon)
  - Firewall: brick-pattern rect or shield shape
  - Load Balancer: horizontal split rect with arrows
  - Cloud: cloud path (overlapping arcs)
- **Connections**: lines between device centers
  - Ethernet/wired: solid line, label bandwidth
  - Wireless: dashed line with WiFi symbol
  - VPN: dashed line with lock icon
- **Subnets/Zones**: dashed rect containers with zone label (DMZ, Internal, External)
- **Labels**: device hostname + IP below, 12-13px
- Layout: tiered top-to-bottom (Internet → Edge → Core → Access → Endpoints)
- ViewBox: `0 0 960 600` standard

## UML Coverage Map

Full mapping of UML 14 diagram types to supported diagram types:

| UML Diagram | Supported As | Notes |
|-------------|-------------|-------|
| Class | Class Diagram | Full UML notation |
| Component | Architecture Diagram | Use colored fills per component type |
| Deployment | Architecture Diagram | Add node/instance labels |
| Package | Architecture Diagram | Use dashed grouping containers |
| Composite Structure | Architecture Diagram | Nested rects within components |
| Object | Class Diagram | Instance boxes with underlined name |
| Use Case | Use Case Diagram | Full actor/ellipse/relationship |
| Activity | Flowchart / Process Flow | Add fork/join bars |
| State Machine | State Machine Diagram | Full UML notation |
| Sequence | Sequence Diagram | Add alt/opt/loop frames |
| Communication | — | Approximate with Sequence (swap axes) |
| Timing | Timeline | Adapt time axis |
| Interaction Overview | Flowchart | Combine activity + sequence fragments |
| ER Diagram | ER Diagram | Chen/Crow's foot notation |

## Shape Vocabulary

Map semantic concepts to consistent shapes across all diagram types:

| Concept | Shape | Notes |
|---------|-------|-------|
| User / Human | Circle + body path | Stick figure or avatar |
| LLM / Model | Rounded rect with brain/spark icon or gradient fill | Use accent color |
| Agent / Orchestrator | Hexagon or rounded rect with double border | Signals "active controller" |
| Memory (short-term) | Rounded rect, dashed border | Ephemeral = dashed |
| Memory (long-term) | Cylinder (database shape) | Persistent = solid cylinder |
| Vector Store | Cylinder with grid lines inside | Add 3 horizontal lines |
| Graph DB | Circle cluster (3 overlapping circles) | |
| Tool / Function | Gear-like rect or rect with wrench icon | |
| API / Gateway | Hexagon (single border) | |
| Queue / Stream | Horizontal tube (pipe shape) | |
| File / Document | Folded-corner rect | |
| Browser / UI | Rect with 3-dot titlebar | |
| Decision | Diamond | Flowcharts only |
| Process / Step | Rounded rect | Standard box |
| External Service | Rect with cloud icon or dashed border | |
| Data / Artifact | Parallelogram | I/O in flowcharts |

## Arrow Semantics

Always assign arrow meaning, not just color. The values below are defaults; the selected style reference overrides colors and stroke weights while preserving flow semantics:

| Flow Type | Color | Stroke | Dash | Meaning |
|-----------|-------|--------|------|---------|
| Primary data flow | blue `#2563eb` | 2px solid | none | Main request/response path |
| Control / trigger | orange `#ea580c` | 1.5px solid | none | One system triggering another |
| Memory read | green `#059669` | 1.5px solid | none | Retrieval from store |
| Memory write | green `#059669` | 1.5px | `5,3` | Write/store operation |
| Async / event | gray `#6b7280` | 1.5px | `4,2` | Non-blocking, event-driven |
| Embedding / transform | purple `#7c3aed` | 1px solid | none | Data transformation |
| Feedback / loop | purple `#7c3aed` | 1.5px curved | none | Iterative reasoning loop |

Always include a **legend** when 2+ arrow types are used.

## Layout Rules & Validation

**Spacing**:
- Same-layer nodes: 80px horizontal, 120px vertical between layers
- Canvas margins: 40px minimum, 60px between node edges
- Snap to 8px grid: horizontal 120px intervals, vertical 120px intervals

**Arrow Labels** (CRITICAL):
- **Offset-first** (default): place label 6-8px above horizontal arrows, or 8px left/right of vertical arrows — do not overlap the arrow line
- **Background fallback**: add `<rect fill="canvas_bg" opacity="0.95"/>` only when the offset label still crosses another visual element (another arrow, a node edge, etc.)
- Place mid-arrow, ≤3 words, stagger by 15-20px when multiple arrows converge
- Maintain 10px safety distance from nodes

**Arrow Routing**:
- Prefer orthogonal (L-shaped) paths to minimize crossings
- Anchor arrows on component edges, not geometric centers
- Route around dense node clusters, use different y-offsets for parallel arrows
- Jump-over arcs (5px radius) for unavoidable crossings
- Compress equivalent bidirectional traffic only when both directions share the same semantics and styling: use one corridor with `marker-start` + `marker-end`, or two visibly offset paths in that corridor
- Keep read/write, request/response, sync/async, or differently labeled directions as separate arrows; remove redundant bends and duplicate rails without erasing direction or meaning

**Post-Generation Arrow Optimization**:

When a user asks to "优化箭头" / "fix arrow routing" / "optimize the diagram" on an already-generated diagram, preserve all nodes, containers, styles, and layout — only modify the `arrows` entries in the JSON data, then re-render with `generate-from-template.py`.

Available arrow override fields (in recommended order of use):

| Field | Type | When to Use |
|-------|------|-------------|
| `source_port` / `target_port` | `"left"` / `"right"` / `"top"` / `"bottom"` | Arrow exits/enters from the wrong edge |
| `corridor_x` | `[x, ...]` | Hint vertical segments toward this x lane (soft preference) |
| `corridor_y` | `[y, ...]` | Hint horizontal segments toward this y lane (soft preference) |
| `route_points` | `[[x1,y1], [x2,y2], ...]` | Exact ordered waypoints; each leg is routed orthogonally and unsafe points are rejected |
| `routing_padding` | number (default: 24) | *(Advanced)* Adjust obstacle clearance for this arrow |
| `port_clearance` | number | *(Advanced)* Adjust first-segment offset from node edge |
| `label_style` | `"badge"` / `"offset"` | Choose `"offset"` when badge backgrounds create visual clutter; keep `"badge"` (default) for legacy/high-contrast labels |

For JSON/template rendering, the default remains `"badge"` for backward compatibility. Set `"label_style": "offset"` on individual arrows when you want offset-first labels without background rects.

Optimization steps:
1. Read the existing SVG — identify which arrows overlap, cross nodes, or look misaligned
2. Find those arrows in the JSON data by `source` / `target` pair
3. Add `source_port` / `target_port` if the exit/entry direction is wrong; add `corridor_x` / `corridor_y` to space parallel arrows apart; use `route_points` only when hints alone cannot resolve the path
4. Re-run `generate-from-template.py` with the updated JSON and validate with `validate-svg.sh`

Example — spacing two overlapping arrows into separate corridors:
```json
{ "source": "nodeA", "target": "nodeB", "corridor_y": [280] }
{ "source": "nodeC", "target": "nodeD", "corridor_y": [320] }
```

**Line Overlap Prevention** (CRITICAL - common in AI-generated diagrams):
When two arrows must cross each other, ALWAYS use jump-over arcs to prevent visual overlap:
- Crossing horizontal arrows: add a small semicircle arc (radius 5px, stroke same color as arrow, fill none) that "jumps over" the other line
- SVG pattern for jump-over: use a white/matching-background arc on the lower layer, then draw the upper arc on top
- Multiple crossings: stagger arc radii (5px, 7px, 9px) so arcs don't overlap each other
- Never let two arrows' straight-line segments cross without a jump-over arc

**Validation Checklist** (run before finalizing):
1. **Arrow-Component Collision**: Arrows MUST NOT pass through component interiors (route around with orthogonal paths)
2. **Text Overflow**: All text MUST fit with 8px padding (estimate: `text.length × 7px ≤ shape_width - 16px`)
3. **Arrow-Text Alignment**: Arrow endpoints MUST connect to shape edges (not floating); arrow labels should not overlap arrow lines (use offset positioning or background rects)
4. **Container Discipline**: Prefer arrows entering and leaving section containers through open gaps between components, not through inner component bodies
5. **Filter Boundary Safety**: For every element with `filter="url(...)"`, verify `(element_x + element_width + filter_extension) ≤ viewBox_width` AND `element_x ≥ filter_extension`. The default filter region extends 10-20% beyond bbox; staying near viewBox edges causes Chrome/cairosvg to clip the element's edge-side stroke (one side of the border vanishes while other sides render correctly)
6. **Arrow-Title Collision**: Arrows MUST NOT cross through section/container title text or region labels (font-size ≥ 13px). For smaller annotations (< 13px), prefer routing around but tolerate if layout constraints require it. *(Visual self-review check — not covered by `validate-svg.sh` automated checks)*
7. **Frame Label–Arrow Alignment** (sequence diagrams): Section/frame label badges MUST be vertically centered with their first message arrow. Compute `badge_y = first_arrow_y - (badge_height / 2)`. When appending new sections to an existing diagram, verify alignment matches the existing sections — this is the most common regression when adding content incrementally. Use variables in Python list generation to enforce the constraint: `sec_y = 840; badge_y = sec_y - 9  # for height=18 badge`
8. **Marker Integrity**: Every `marker-start`, `marker-mid`, and `marker-end` URL MUST resolve to a `<marker id="...">` definition
9. **Visual Review Status**: Report whether the exported PNG was visually inspected; automated validation does not cover every text, legend, or arrow-arrow collision

## SVG Technical Rules

- ViewBox: `0 0 960 600` default; `0 0 960 800` tall; `0 0 1200 600` wide
- Fonts: embed via `<style>font-family: ...</style>` — no external `@import` (cairosvg / rsvg-convert cannot fetch external URLs)
- `<defs>`: arrow markers, gradients, filters, clip paths
- Text: minimum 12px, prefer 13-14px labels, 11px sub-labels, 16-18px titles
- All arrows: `<marker>` with `markerEnd`, sized `markerWidth="10" markerHeight="7"`
- Drop shadows: `<feDropShadow>` in `<filter>`, apply sparingly (key nodes only)
- Curved paths: use `M x1,y1 C cx1,cy1 cx2,cy2 x2,y2` cubic bezier for loops/feedback arrows
- Clip content: use `<clipPath>` if text might overflow a node box
- Z-order (drawing order): SVG uses painter's model — later elements cover earlier ones. Recommended layer order (bottom → top): ① canvas background ② dashed containers / region backgrounds ③ arrows and connection lines ④ node shapes (rects, circles) ⑤ text labels and annotations ⑥ legends and overlays. When arrows pass near text, draw arrows BEFORE text so text stays readable. Adjust per diagram needs — this is guidance, not rigid.

## SVG Generation & Error Prevention

**MANDATORY: Python List Method** (ALWAYS use this):
```python
python3 << 'EOF'
lines = []
lines.append('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 700">')
lines.append('  <defs>')
# ... each line separately
lines.append('</svg>')

with open('/path/to/output.svg', 'w') as f:
    f.write('\n'.join(lines))
print("SVG generated successfully")
EOF
```

**Why mandatory**: Prevents character truncation, typos, and syntax errors. Each line is independent and easy to verify.

**Pre-Tool-Call Checklist** (CRITICAL - use EVERY time):
1. ✅ Can I write out the COMPLETE command/content right now?
2. ✅ Do I have ALL required parameters ready?
3. ✅ Have I checked for syntax errors in my prepared content?

**If ANY answer is NO**: STOP. Do NOT call the tool. Prepare the content first.

**Error Recovery Protocol**:
- **First error**: Analyze root cause, apply targeted fix
- **Second error**: Switch method entirely (Python list → chunked generation)
- **Third error**: STOP and report to user - do NOT loop endlessly
- **Never**: Retry the same failing command or call tools with empty parameters

**Validation** (run after generation):
```bash
python3 -c "import xml.etree.ElementTree as ET; ET.parse('file.svg')" && echo "✓ Valid XML"
# Or use cairosvg as a render-time check:
python3 -c "import cairosvg; cairosvg.svg2png(url='file.svg', write_to='/tmp/test.png')" && echo "✓ Renders" && rm /tmp/test.png
```

**If using `generate-from-template.py`**:
- Prefer `source` / `target` node ids in arrow JSON so the generator can snap to node edges
- Keep `x1,y1,x2,y2` as hints or fallback coordinates, not the main routing primitive
- Let the generator choose orthogonal routes; avoid hardcoding center-to-center straight lines unless the path is guaranteed clear

**Common Syntax Errors to Avoid**:
- ❌ `yt-anchor` → ✅ `y="60" text-anchor="middle"`
- ❌ `x="390` (missing y) → ✅ `x="390" y="250"`
- ❌ `fill=#fff` → ✅ `fill="#ffffff"`
- ❌ `marker-end=` → ✅ `marker-end="url(#arrow)"`
- ❌ `L 29450` → ✅ `L 290,220`
- ❌ Missing `</svg>` at end
- ❌ Element with `filter` near viewBox edge — filter region extends 20% (default) or more beyond bbox; if that region exceeds viewBox, Chrome/cairosvg clip the filter rendering AND can drop the element's own stroke on that side. Keep filtered elements at least `max(20% of element size, shadow blur radius × 3)` away from viewBox edges, or omit the filter.

## Output

- **Default**: `./[derived-name].svg` and `./[derived-name].png` in current directory
- **Custom**: user specifies path with `--output /path/` or `输出到 /path/`
- **PNG / motion export**: see **SVG → PNG Conversion** below and `$SKILL_ROOT/references/motion-effects.md`

## SVG → PNG Conversion

Use `$SKILL_ROOT/scripts/generate-diagram.sh` by default. Load `$SKILL_ROOT/references/png-export.md` only when selecting a renderer manually, handling CJK/emoji fallback, converting browser-generated SVG, or using the bundled Puppeteer converter.

## Styles

| # | Name | Background | Best For |
|---|------|-----------|----------|
| 1 | **Flat Icon** (default) | White | Blogs, docs, presentations |
| 2 | **Dark Terminal** | `#0f0f1a` | GitHub, dev articles |
| 3 | **Blueprint** | `#0a1628` | Architecture docs |
| 4 | **Notion Clean** | White, minimal | Notion, Confluence, wikis |
| 5 | **Glassmorphism** | Dark gradient | Product sites, keynotes |
| 6 | **Claude Official** | Warm cream `#f8f6f3` | Anthropic-style diagrams |
| 7 | **OpenAI Official** | Pure white `#ffffff` | OpenAI-style diagrams |
| 8 | **Dark Luxury** *(AI-authored)* | `#0a0a0a` deep black | Architecture docs, premium editorial — hand-craft SVG from `$SKILL_ROOT/references/style-8-dark-luxury.md` |
| 9 | **C4 Review Canvas** | Warm paper `#f7f2e8` | C4 reviews and ADRs; enforces one abstraction level |
| 10 | **Cloud Fabric** | Cloud blue `#edf5fb` | Region/network/workload deployment ownership |
| 11 | **Event Transit** | Transit paper `#fbf7ee` | Topics, processors, consumer groups, DLQ, state |
| 12 | **Ops Pulse** | Ops navy `#07111f` | Golden signals, critical paths, correlated traces |

Load the matching `$SKILL_ROOT/references/style-N-*.md` for exact color tokens and SVG patterns.

## Style Selection

**Default**: Style 1 (Flat Icon) for most diagrams. Load `$SKILL_ROOT/references/style-diagram-matrix.md` for detailed style-to-diagram-type recommendations.

Prompt fingerprints: `C4评审画布/C4 review board` → 9; `多区域云部署/deployment topology` → 10; `事件地铁图/event metro map` → 11; `可靠性脉冲/golden signals trace` → 12; `让这张图动起来/生成 GIF/制作 GIF/animate this diagram/Generate a GIF` → auto motion.
Auto-select these only with matching domain evidence; otherwise use Styles 1–7 or `semantic_profile: "generic"`, and split mixed C4/deployment/event/ops views.

These patterns appear frequently — internalize them:

**RAG Pipeline**: Query → Embed → VectorSearch → Retrieve → Augment → LLM → Response
**Agentic RAG**: adds Agent loop with Tool use between Query and LLM
**Agentic Search**: Query → Planner → [Search Tool / Calculator / Code] → Synthesizer → Response
**Mem0 / Memory Layer**: Input → Memory Manager → [Write: VectorDB + GraphDB] / [Read: Retrieve+Rank] → Context
**Agent Memory Types**: Sensory (raw input) → Working (context window) → Episodic (past interactions) → Semantic (facts) → Procedural (skills)
**Multi-Agent**: Orchestrator → [SubAgent A / SubAgent B / SubAgent C] → Aggregator → Output
**Tool Call Flow**: LLM → Tool Selector → Tool Execution → Result Parser → LLM (loop)
skills/fireworks-tech-graph/docs/releases/v1.1.0.md
# Fireworks Tech Graph v1.1.0

This release turns the project into an engineering-grade diagram system while preserving the distinct scenarios and visual identity of all 12 styles.

## Version information

- Version: `v1.1.0`
- Version date: 2026-07-15
- Tag commit: [`1c7ba0fe`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/1c7ba0fe69ef5b166821eefb6fe447a0e5d70be9)
- Previous version: [`v1.0.5`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/releases/tag/v1.0.5)
- Release channel: GitHub stable release; the npm registry remains on the legacy `1.0.4` package line.

## Highlights

- Adds Style 9 C4 Review Canvas, Style 10 Cloud Fabric, Style 11 Event Transit, and Style 12 Ops Pulse.
- Enforces semantic contracts for C4 abstraction, deployment ownership, event rails, Golden Signals, observation windows, critical paths, and correlated traces.
- Adds geometry-safe orthogonal routing, port fan-out, waypoint validation, label and reserved-region checks, bridge masks, and fail-closed composition gates.
- Adds a unified CLI, versioned Diagram IR, deterministic layout reports, and safe offline interactive HTML export.
- Refreshes the official gallery with 12 distinct engineering scenarios and fixed-width 1920px regression renders.
- Ships a complete nested Agent Skill payload for Codex and Claude Code, plus reproducible tgz/zip archives and `SHA256SUMS`.

## Compatibility

- Node.js 18 or newer is required.
- Python source compatibility is checked against Python 3.9 grammar; CI runs the test suite on Python 3.9 and 3.12.
- The npm registry is a separate distribution channel and may lag this GitHub Release. Install the current Skill from `yizhiyanhua-ai/fireworks-tech-graph/skills/fireworks-tech-graph`.

See the [v1.1.0 changelog](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/blob/v1.1.0/CHANGELOG.md) for the full change list and the release assets below for checksum-verified archives.
skills/fireworks-tech-graph/docs/releases/v1.2.0.md
# Fireworks Tech Graph v1.2.0

This release adds a focused, production-validated SVG-to-GIF workflow while preserving the twelve distinct diagram styles and their engineering scenarios.

## Version information

- Version: `v1.2.0`
- Version date: 2026-07-17
- Tag commit: [`ea534c2d`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/ea534c2d2817d43e4c02fe0ea9f67dfced6dece3)
- Previous version: [`v1.1.0`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/releases/tag/v1.1.0)
- Release channel: latest GitHub stable release; the npm registry remains on the legacy `1.0.4` package line.

## Highlights

- Adds user-approved semantic motion contracts for all 12 styles: connectors start absent, build in semantic order, then keep the completed topology alive with style-specific data flow.
- Makes the 5.75-second / 115-frame settled-flow timeline the default for natural-language GIF requests, while retaining explicit 3.75-second and 2.75-second compatibility timelines.
- Replaces every README showcase image with an approved GIF, including a synchronized 3×4 overview optimized below 2 MiB. Static 1920px PNGs remain available as regression baselines.
- Refreshes the GitHub Pages product site with the animated overview and all twelve per-style GIFs, while retaining static Open Graph media and reduced-motion fallbacks.
- Adds source-DOM immutability guards, bounded runtime probes, secure Puppeteer resolution, atomic GIF/report installation, FFprobe media checks, binary infinite-loop readback, and motion reports.
- Validates all 12 installed Skill styles in CI and before tag releases, including 852 Chromium compatibility comparisons across the extended timeline.
- Keeps motion reusable for valid title and content changes: the semantic topology, route roles, directions, stages, geometry, and style signature are pinned; the exact source SVG bytes are not.
- Includes the post-v1.1.0 SVG validator fixes for multi-subpath geometry and transformed bounds.

## Compatibility

- Python 3.9 or newer is required.
- Node.js 18 or newer is required for the optional Chromium motion renderer.
- GIF generation requires FFmpeg and `puppeteer-core@25.3.0`; static SVG/PNG/HTML workflows continue to work without them.
- The npm registry is a separate distribution channel and may lag the GitHub Release. Install the current Skill from `yizhiyanhua-ai/fireworks-tech-graph/skills/fireworks-tech-graph`.

See the [v1.2.0 changelog](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/blob/v1.2.0/CHANGELOG.md) for the full change list and the release assets below for checksum-verified archives.
skills/fireworks-tech-graph/README.zh.md
[English](README.md) | [中文](README.zh.md)

[版本历史](docs/releases/README.md) · [更新日志](CHANGELOG.md)

# fireworks-tech-graph

> 不用手画图了。用中文描述你的系统,直接得到通过几何门禁的 SVG、PNG、聚焦的 SVG 转 GIF 动效与离线交互技术图。

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![GitHub Release](https://img.shields.io/github/v/release/yizhiyanhua-ai/fireworks-tech-graph)](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/releases)
[![Codex Skill](https://img.shields.io/badge/Codex-Skill-10a37f)](https://learn.chatgpt.com/docs/build-skills)
[![Claude Code Skill](https://img.shields.io/badge/Claude%20Code-Skill-d97757)](https://code.claude.com/docs/zh-CN/skills)
[![12 种视觉风格](https://img.shields.io/badge/风格-12种-purple)]()
[![14 种图类型](https://img.shields.io/badge/图类型-14种-green)]()
[![UML 支持](https://img.shields.io/badge/UML-完整支持-orange)]()

## 概述

`fireworks-tech-graph` 是一份可由 **Codex 和 Claude Code 共用**的 Agent Skill。它将自然语言描述转化为经过几何校验的 SVG、高分辨率 PNG、经过媒体探测验证的 SVG 转 GIF 语义动效与离线交互 HTML。聚焦后的动效链路只接收生成器产出的语义 SVG,只输出一个紧凑、可验证的 GIF。项目内置 **11 种生成器风格** + **1 种 AI 手绘风格(Dark Luxury)**;新增的四种工程风格分别为 C4 评审、云部署、事件流和可靠性排查提供可执行语义契约,同时保留 AI/Agent Pattern 与全部 14 种 UML 图类型。

```
用户: "画一张 Mem0 的架构图,暗黑风格"
  → Skill 识别:Memory Architecture Diagram,Style 2
  → 生成含泳道、圆柱体、语义箭头的 SVG
  → 导出 1920px PNG
  → 输出路径:mem0-architecture.svg / mem0-architecture.png
```

---

## 效果展示

> 动态样例统一使用已验收的 5.75 秒 settled-flow 时间线:先逐步绘制线路,再让最终拓扑中的数据流额外持续 2 秒。每张完整 GIF 为 960px 宽、20fps / 115 帧;3×4 总览压缩为 1200px 动态预览。`assets/samples/` 中仍保留 1920px 无损 PNG,作为静态回归基线。

![动态 12 风格总览 — 每种风格使用独立工程场景](assets/samples/showcase-12-styles.gif)

上方 v1.2.0 总览与下方每张完整动态样例均来自已验收回归集。12 种风格保留各自独立场景,同时统一通过几何、文字适配、线束路由和语义动效质量门禁。

### 风格 1 — 扁平图标风(默认)
*Mem0 Memory Architecture — 个人记忆抽取、冲突消解、存储与检索*
![风格 1 — 扁平图标风](assets/samples/sample-style1-flat.gif)

### 风格 2 — 暗黑极客风
*Tool Call Flow — 暗黑终端执行、来源 Grounding、检索与回答合成*
![风格 2 — 暗黑极客风](assets/samples/sample-style2-dark.gif)

### 风格 3 — 工程蓝图风
*Microservices Architecture — 工程网格、领域服务、数据存储、事件与遥测*
![风格 3 — 工程蓝图风](assets/samples/sample-style3-blueprint.gif)

### 风格 4 — Notion 极简风
*Agent Memory Types — 从感知和工作上下文到长期记忆的极简层级*
![风格 4 — Notion 极简风](assets/samples/sample-style4-notion.gif)

### 风格 5 — 玻璃态卡片风
*Multi-Agent Collaboration — 协调器、专家 Agent、共享状态、评审与合成*
![风格 5 — 玻璃态卡片风](assets/samples/sample-style5-glass.gif)

### 风格 6 — Claude 官方风格
*System Architecture — 温暖的界面、Runtime、安全、记忆、工具和运维分层*
![风格 6 — Claude 官方风格](assets/samples/sample-style6-claude.gif)

### 风格 7 — OpenAI 官方风格
*API Integration Flow — 清晰的 SDK、Prompt、Model、Tool、交付与发布阶段*
![风格 7 — OpenAI 官方风格](assets/samples/sample-style7-openai.gif)

### 风格 8 — 暗黑奢华风 *(AI 手绘)*
*Agent Runtime Architecture — 控制平面、执行与状态分层,香槟金结构线和语义色桶*
![风格 8 — 暗黑奢华风](assets/samples/sample-style8-dark-luxury.gif)

### 风格 9 — C4 评审画布
*Checkout Container Review — 单一抽象层级、明确职责、技术栈与协议*
![风格 9 — C4 评审画布](assets/samples/sample-style9-c4-review-canvas.gif)

### 风格 10 — Cloud Fabric
*Active–Active Checkout Deployment — 全局入口、Region、VPC 归属与跨区复制*
![风格 10 — Cloud Fabric](assets/samples/sample-style10-cloud-fabric.gif)

### 风格 11 — Event Transit
*Checkout Event Line — Topic 轨道、处理站点、显式 Junction、DLQ 与状态投影*
![风格 11 — Event Transit](assets/samples/sample-style11-event-transit.gif)

### 风格 12 — Ops Pulse
*Checkout Reliability Pulse — Golden Signals、关键路径、OTel 导出与关联 Trace*
![风格 12 — Ops Pulse](assets/samples/sample-style12-ops-pulse.gif)

---

## 稳定输出提示词

公开展示保留 12 个互不重复的领域场景;它们通过同一套可执行构图契约保证质量可比。同拓扑回归样例仅保留在 `fixtures/quality-baseline/` 内部使用。

```text
按 style N 对应的场景出图:
1 Mem0 Memory Architecture;2 Tool Call Flow;3 Microservices Architecture;
4 Agent Memory Types;5 Multi-Agent Collaboration;6 System Architecture;
7 API Integration Flow;8 Agent Runtime Architecture;9 C4 Checkout Review;
10 Active–Active Cloud Deployment;11 Checkout Event Line;12 Checkout Reliability Pulse。
保留该场景自己的节点、分区和阅读方向。
应用 showcase 构图质量契约:零交叉、零跨线桥、每条线最多 2 个折点、
全图最多 8 个折点、节点间距至少 40px、容器内边距至少 20px,
正交线段保持简短,标签避开节点、线路和分区标题。
保留所选风格的字体、配色、卡片材质和品牌化细节。
```

新增的四种工程风格可以直接使用下面的“提示词指纹”,让路由同时选中
对应的视觉语言与领域语义契约:

```text
风格 9 · C4 评审画布:只展示一个 C4 层级,包含职责、技术栈、评审状态,以及“动作 + 协议”关系标签。
风格 10 · 多区域云部署图:展示全局入口、Region/VPC 归属、中立云图标、部署模式,以及具名的跨边界机制。
风格 11 · 事件地铁图:使用细 Topic 轨道、编号处理站、显式 Junction、Consumer Group、DLQ 与状态投影。
风格 12 · 可靠性脉冲:固定观察窗口,每个服务展示四个 Golden Signals、编号关键跳、遥测导出和一条关联 Trace。
```

把 `N` 替换为 `1`–`12`。Style 8 仍由 AI 读取 `references/style-8-dark-luxury.md` 手工绘制;Style 9–12 还会执行对应的工程语义契约;所有风格同时加载 `references/composition-quality-contract.md`。

---

## 功能特性

- **12 种视觉风格** — 11 种生成器驱动 + 1 种 AI 手绘(Dark Luxury)
- **工程语义契约** — C4 抽象层级、Deployment 归属、事件轨道拓扑、精确 Golden Signals 在渲染前 fail closed
- **可执行风格系统** — 风格约束不仅写在文档里,也真正进入生成器逻辑
- **几何安全布线** — 确定性的正交路径、强制 waypoint、端口分流、图例自动避让、标签画布约束,以及带遮罩验证的跨线桥
- **版本化 Diagram IR** — 旧 JSON 会归一化为 schema v1;重复 ID、悬空引用、非法 waypoint 和非有限坐标在渲染前直接失败
- **统一 CLI 与交互导出** — 支持 render、validate、inspect,并可导出单文件离线 HTML,包含平移缩放、主题切换、复制和 1×–4× SVG/PNG/JPEG/WebP 导出
- **14 种图类型** — 完整支持全部 UML 图类型(类图、组件图、部署图、包图、复合结构图、对象图、用例图、活动图、状态机图、序列图、通信图、时序图、交互概览图、ER 图)以及 AI/Agent 领域图
- **AI/Agent 领域内建知识** — RAG、Agentic Search、Mem0、Multi-Agent、Tool Call 等常见 Pattern 开箱即用
- **语义形状词汇表** — LLM = 双边框圆角矩形,Agent = 六边形,Vector Store = 带内环圆柱
- **语义箭头系统** — 颜色 + 虚线样式编码含义(写入/读取/异步/循环)
- **结构化 SVG 校验** — XML 解析、`marker-start/mid/end` 完整性,以及 `M/L/H/V/Q/C/S/T` 路径的箭头穿框检测
- **视觉复核门禁** — 交付前回读 PNG,检查裁切、重叠、标签位置和走线回归
- **产品图标库** — 40+ 产品品牌色:OpenAI、Anthropic、Pinecone、Weaviate、Kafka、PostgreSQL……
- **泳道分组** — 自动为复杂架构添加层级标签
- **SVG + PNG 双输出** — SVG 可编辑,1920px PNG 可直接嵌入文章
- **聚焦的语义 GIF 动效** — 只支持生成 SVG 输入和 GIF 输出;连接线从无到有并按语义顺序绘制。Style 1–12 的数据包头、终端证据流、Blueprint bead、14×10 Notion memory card、玻璃任务胶囊、治理印章、API token train、宝石 tracer、评审 cursor、双活区域流、事件列车和运维瀑布 scanner 均已验收;共享 `+2s-settled-flow` 时间修订也已成为正式默认
- **渲染器友好** — 纯内联 SVG,不依赖外部字体;在 cairosvg、rsvg-convert、headless Chrome 下都能稳定渲染

---

## Loop Engineering 设计理念

首轮渲染会被视为候选结果,交付前还要经过一条由 Agent 驱动、轮次受限的 validation feedback loop:

```text
Prompt
  → Diagram Contract
  → Semantic IR
  → Style Spec
  → Route Planner
  → SVG Build
  → Structural Validation
  → PNG Visual Readback
  → Targeted Revision
  → Verified SVG + PNG
```

这条闭环遵循五项原则:

1. **Evaluate, don't assert** — 完成状态必须有 validator 和实际渲染证据,不能只依赖模型对结果的主观判断。
2. **先确定性校验** — 依次检查 XML 结构、marker 引用、路径几何、箭头穿框和渲染可用性,再进入视觉判断。
3. **再做感知验证** — 回读导出的 PNG,检查语法工具无法识别的裁切、标签碰撞、视觉层级、留白和走线质量。
4. **定向修正** — 每轮只修改已诊断的标签、坐标、corridor 或间距,随后重新运行 validator 和 render check。
5. **有界收敛** — 默认最多执行两轮 focused correction,避免进入无上限的自我修改循环。

最终状态会明确报告闭环结果:

```text
validation: passed
visual_review: passed
```

当运行环境无法读取图片时,Skill 会明确报告 `visual_review: skipped (image reader unavailable)`。整个流程保持可观察、可审计,也不会在缺少图片证据时宣称已经完成视觉验证。

---

## 安装

### 推荐:一次安装到 Codex 与 Claude Code

必须使用真正的嵌套 Skill 路径。末尾的 `/skills/fireworks-tech-graph` 不能省略;当前版本的 `skills` CLI 在裸仓库路径下可能只选择根目录的 `SKILL.md`。

```bash
npx -y skills@1.5.17 add \
  yizhiyanhua-ai/fireworks-tech-graph/skills/fireworks-tech-graph \
  --agent codex claude-code -g -y --copy
```

命令会把完整 Skill 分别复制到 Codex 的 `~/.agents/skills/fireworks-tech-graph` 与 Claude Code 的 `~/.claude/skills/fireworks-tech-graph`,其中包含脚本、schema、fixture、模板、测试、参考资料与元数据。

### Codex 的可编辑 Git 安装

```bash
mkdir -p ~/.agents/skills
git clone https://github.com/yizhiyanhua-ai/fireworks-tech-graph.git ~/.agents/skills/fireworks-tech-graph
```

Codex 从 `~/.agents/skills` 发现个人 Skill,并会读取仓库中的可选元数据 `agents/openai.yaml`。

### Claude Code 的可编辑 Git 安装

```bash
mkdir -p ~/.claude/skills
git clone https://github.com/yizhiyanhua-ai/fireworks-tech-graph.git ~/.claude/skills/fireworks-tech-graph
```

Claude Code 从 `~/.claude/skills` 发现个人 Skill,会忽略只供 Codex 使用的 UI 元数据。

### Codex 与 Claude Code 共用一份可编辑仓库

首次安装且 Claude Code 版本不低于 2.1.203 时,可以只保留一份仓库,再把两个发现路径链接到它。创建链接前,先把已有目标目录移开。

```bash
mkdir -p ~/.local/share/agent-skills ~/.agents/skills ~/.claude/skills
git clone https://github.com/yizhiyanhua-ai/fireworks-tech-graph.git ~/.local/share/agent-skills/fireworks-tech-graph
ln -s ~/.local/share/agent-skills/fireworks-tech-graph ~/.agents/skills/fireworks-tech-graph
ln -s ~/.local/share/agent-skills/fireworks-tech-graph ~/.claude/skills/fireworks-tech-graph
```

这样 `SKILL.md`、参考资料、脚本、模板和后续更新在两端始终一致。npm registry 是独立分发渠道,版本可能晚于 GitHub Release。当前 Skill 版本请使用上面的 GitHub 嵌套路径;npm 页面继续用于查看包元数据:

```text
https://www.npmjs.com/package/@yizhiyanhua-ai/fireworks-tech-graph
```

## 更新

通过 `skills` CLI 安装时,重新执行上面的嵌套路径命令。通过 Git 安装时,更新实际安装的那一份仓库:

```bash
git -C ~/.agents/skills/fireworks-tech-graph pull
# 或
git -C ~/.claude/skills/fireworks-tech-graph pull
# 或:共享仓库方式
git -C ~/.local/share/agent-skills/fireworks-tech-graph pull
```

首次安装后重启 Codex 和 Claude Code,让两端重新发现 Skill。后续修改 `SKILL.md` 会自动生效;如果改的是脚本或参考资料而运行时没有看到更新,重启对应运行时。

以上 Shell 命令适用于 macOS、Linux、WSL 和 Git Bash。原生 Windows 请使用 `%USERPROFILE%\.agents\skills` 与 `%USERPROFILE%\.claude\skills` 对应路径。运行脚本需要 Python 3.9+;可选的 Puppeteer 路径需要 Node.js 18+。

---

## 统一 CLI

```bash
SKILL_ROOT="${CLAUDE_SKILL_DIR:-$HOME/.agents/skills/fireworks-tech-graph}"

python3 "$SKILL_ROOT/scripts/fireworks.py" doctor
python3 "$SKILL_ROOT/scripts/fireworks.py" validate architecture "$SKILL_ROOT/fixtures/api-flow-style7.json"
python3 "$SKILL_ROOT/scripts/fireworks.py" render architecture "$SKILL_ROOT/fixtures/api-flow-style7.json" diagram.svg --report layout.json
python3 "$SKILL_ROOT/scripts/fireworks.py" check diagram.svg
python3 "$SKILL_ROOT/scripts/fireworks.py" export-html diagram.svg diagram.html --title "API Integration Flow"
python3 "$SKILL_ROOT/scripts/fireworks.py" animate diagram.svg diagram.gif
```

HTML 导出是单文件离线产物。导出器会清洗 SVG,并提供平移、缩放、复位、明暗主题、复制 SVG 源码,以及 1×–4× 的 SVG/PNG/JPEG/WebP 下载。

动效可以直接说 **“生成 GIF”**、**“制作 GIF”** 或 **“让这张图动起来”**。命令只接收带有 12 套已验收动效契约之一的生成器语义 SVG。它不锁定源文件的精确字节,因此同一受支持拓扑下通过校验的标题和内容变化可以正常使用;缺失或改变 role/stage/order 覆盖、线路方向、必要颜色或几何时会 fail closed。动效媒体格式只允许 GIF,默认还会写出 `<output>.motion.json` 验证报告。正式默认参数为 960px、5.75 秒、20fps、115 个帧中心采样:第 1–36 帧完成线路构建,第 36–38 帧淡入运行流,第 38–109 帧保持完整稳定数据流,第 110–114 帧执行五档 reset。Style 1–12 的 signature、速度、路径、几何、构建节奏均已验收,包括 packet head、terminal evidence trace、Blueprint bead、14×10 Notion memory card 和八种场景专属 signature;共享的 `+2s-settled-flow` 时间修订也已验收,默认新包的相关状态统一记录为 `user-approved`。75 帧及以下继续要求全部帧唯一;更长时间线允许完整不透明区间内出现非相邻重复。frame 110 因 reset opacity 精确保留 1.00,成为唯一边界例外,并显式分类为 `intentional_reset_boundary_repeat`;frame 111–114 必须全局不同。长时间线至少保留 75 个唯一 raster,并禁止相邻重复。全样式 75-vs-115 gate 分开统计 binary-exact 与 decoded-RGBA-exact;仅当 AE ≤ 128、normalized RMSE ≤ 0.001、差异 component 宽或高不超过 2px 且只落在 edge/node border 时,才接受 compositor 抗锯齿等价,DOM 与 signature geometry 始终 strict-exact。显式 3.75 秒/75 帧与 2.75 秒/55 帧调用继续兼容。详见 [聚焦的 SVG 转 GIF 动效](references/motion-effects.md)。

| Style | Preset | 运行态 signature |
|---:|---|---|
| 5 | `agent-orchestration` | 玻璃任务胶囊 + coordinator halo |
| 6 | `governed-runtime` | governance thread + policy seal |
| 7 | `token-stream` | API rail + 三格 token train |
| 8 | `golden-circuit` | luxury circuit rail + gem tracer |
| 9 | `review-trace` | review rail + moving review cursor |
| 10 | `cloud-flow` | region chevrons + replication capsule |
| 11 | `event-transit` | event train + exception/projection cars |
| 12 | `ops-pulse` | ECG/export heads + trace reveal + waterfall scanner |

---

## 安装依赖

仓库自带的 SVG/PNG 校验与导出脚本需要 **cairosvg**(推荐)或 `rsvg-convert`。可选的 SVG 转 GIF 动效导出需要 FFmpeg/FFprobe、Chrome/Chromium,以及 `puppeteer` 或 `puppeteer-core`。

```bash
# 推荐:cairosvg(CSS 支持最好)
python3 -m pip install cairosvg

# 备选:rsvg-convert(系统包,可能丢失 CSS / <foreignObject>)
brew install librsvg                   # macOS
sudo apt install librsvg2-bin          # Ubuntu/Debian

# 可选:语义动效导出。依赖分别安装在两份 Skill 旁边,因为渲染器会
# 刻意忽略调用者当前目录中的同名 Node 模块。
brew install ffmpeg                    # macOS;其他平台使用系统包管理器
for SKILL_ROOT in \
  "$HOME/.agents/skills/fireworks-tech-graph" \
  "$HOME/.claude/skills/fireworks-tech-graph"
do
  [ -d "$SKILL_ROOT" ] || continue
  npm install --prefix "$SKILL_ROOT" --ignore-scripts --no-save --package-lock=false puppeteer-core@25.3.0
  python3 "$SKILL_ROOT/scripts/fireworks.py" doctor
done

# 验证脚本支持的任一渲染器
python3 -c "import cairosvg; print(cairosvg.__version__)"
rsvg-convert --version
```

| 渲染器 | 渲染质量 | 安装成本 | 适用场景 |
|--------|---------|---------|---------|
| **cairosvg** | ✅ 好 | 一行 `python3 -m pip install` | 默认推荐,平衡最佳 |
| rsvg-convert | ⚠️ 一般 | 系统包 | 没有 Python 环境,简单图形够用 |
| puppeteer | ✅✅ 最佳 | Node + Chromium | D3、Mermaid 或像素级还原的手动浏览器渲染方案 |

> 渲染器对比和 Puppeteer 用法见 [references/png-export.md](references/png-export.md),浏览器导出脚本位于 `scripts/svg2png.js`。

---

## 使用方式

### 触发词

以下关键词会自动触发 Skill:

```
画图 / 帮我画 / 生成图 / 做个图 / 架构图 / 流程图 / 可视化一下 / 出图
generate diagram / draw diagram / create chart / visualize
生成 GIF / 制作 GIF / 让这张图动起来 / 把刚才的 SVG 转成 GIF / Generate a GIF / animate this diagram / animate this SVG as a GIF
```

### 基本用法

```
画一张 RAG 流程图
```

```
生成一张 Agentic Search 架构图
```

### 指定风格

```
画一张微服务架构图,风格2(暗黑极客风)
```

```
生成 Multi-Agent 协作图,玻璃态风格
```

### 指定输出路径

```
生成 Mem0 架构图,输出到 ~/Desktop/
```

```
画一张 Tool Call 流程图 --output /tmp/diagrams/
```

---

## 场景示例集

### AI/Agent 系统

```
画一张 Agentic RAG 和普通 RAG 的对比图,用 Notion 极简风
```
→ 功能矩阵对比:检索策略、Agent 循环、工具调用、延迟、成本

```
生成一张 Mem0 记忆架构图,包含向量库、图数据库、KV 存储和记忆管理器
```
→ 分泳道记忆架构:Input → Memory Manager → 存储层 → 检索输出

```
画一张 Multi-Agent 协作图:Orchestrator 调度 3 个 SubAgent(搜索/计算/代码执行),汇聚到 Aggregator
```
→ Agent 架构,六边形节点 + 工具层 + 结果聚合

```
可视化一下 Tool Call 的执行流程:LLM → Tool Selector → Execution → Parser → 回到 LLM
```
→ 含决策循环的流程图,展示工具调用的完整生命周期

```
画一张 Agent 的 5 种记忆类型图:感知记忆、工作记忆、情景记忆、语义记忆、程序记忆
```
→ 思维导图或分层架构,从感官输入到程序技能的记忆层级

### 基础设施与云架构

```
帮我画一张微服务架构图:Client → API Gateway → [用户服务 / 订单服务 / 支付服务] → PostgreSQL + Redis
```
→ 水平分层架构,每个服务集群一个泳道

```
生成一张数据管道图:Kafka 消费数据 → Spark 处理 → 写入 S3 → Athena 查询
```
→ 数据流图,每条箭头标注数据类型(stream / batch / query)

```
画一张 Kubernetes 部署架构:Ingress → Service → [Pod × 3] → ConfigMap + PersistentVolume
```
→ 架构图,Namespace 用虚线框,流量用实线箭头

### API 与时序流程

```
画一张 OAuth2 授权码流程的序列图:用户 → 客户端 → 授权服务器 → 资源服务器
```
→ 序列图,垂直生命线 + 激活框

```
帮我画一张 ChatGPT Plugin 的调用时序图
```
→ 时序:User → ChatGPT → Plugin Manifest → API → 响应链

### 决策与流程图

```
画一张 AI 应用上线前的质检流程图:代码审查 → 安全扫描 → 性能测试 → 人工审核 → 发布
```
→ 流程图,含菱形决策节点和并行分支

```
生成一张 RAG vs Fine-tuning vs Prompt Engineering 的功能对比图
```
→ 功能矩阵,对比成本、延迟、准确率、灵活性

### 概念图与知识图谱

```
帮我可视化一下 LLM 应用的技术栈:从底层模型到 SDK 到应用框架到部署层
```
→ 分层架构图或思维导图,从模型层到产品层

```
画一张 AI Agent 的核心能力地图:感知 / 记忆 / 推理 / 行动 / 学习
```
→ 以"AI Agent"为中心的放射状思维导图,5 个核心能力分支

---

## 12 种风格

| # | 名称 | 背景色 | 字体 | 适用场景 |
|---|------|--------|------|----------|
| 1 | **扁平图标风** *(默认)* | `#ffffff` | Helvetica | 博客、幻灯片、技术文档 |
| 2 | **暗黑极客风** | `#0f0f1a` | SF Mono / Fira Code | GitHub README、开发者文章 |
| 3 | **工程蓝图风** | `#0a1628` | Courier New | 架构设计文档、工程规范 |
| 4 | **Notion 极简风** | `#ffffff` | system-ui | Notion、Confluence、内部 Wiki |
| 5 | **玻璃态卡片风** | `#0d1117` 渐变 | Inter | 产品官网、演讲 Keynote |
| 6 | **Claude 官方风格** | `#f8f6f3` | system-ui | Anthropic 风格图表,温暖专业美学 |
| 7 | **OpenAI 官方风格** | `#ffffff` | system-ui | OpenAI 风格图表,简洁现代设计 |
| 8 | **暗黑奢华风** *(AI 手绘)* | `#0a0a0a` | Georgia + system-ui | 高级文档、README Hero、技术演讲 |
| 9 | **C4 评审画布** | `#f7f2e8` | Avenir / system-ui | C4 设计评审、ADR、职责边界 |
| 10 | **Cloud Fabric** | `#edf5fb` | Inter / system-ui | 多 Region 部署、VPC/网络归属 |
| 11 | **Event Transit** | `#fbf7ee` | Avenir / system-ui | Kafka/Event Stream、Consumer Group、DLQ |
| 12 | **Ops Pulse** | `#07111f` | SF Mono / Fira Code | SRE 评审、Golden Signals、关键 Trace |

每种风格在 `references/` 目录下都有专属参考文件,包含精确的颜色 Token 和 SVG Pattern。风格 1–7 与 9–12 由生成器驱动;风格 8 使用 AI 手绘构图和静态回归 fixture。
生成器会直接消费 `containers`、语义化 `nodes[].kind`、`arrows[].flow` 以及显式端口锚点;Style 9–12 还会在布局前校验领域字段。

几个很有用的增强字段:
- `style_overrides`:在不复制整套 style 的前提下微调标题对齐或配色 token
- `containers[].header_prefix` / `containers[].header_text`:用于 style 3 这种 `01 // EDGE` 的工程编号分区标题
- `containers[].side_label`:用于 style 6 这类左侧 Layer Label
- `window_controls`、`meta_left`、`meta_center`、`meta_right`:用于终端 / 文档风格的顶部 chrome
- `blueprint_title_block`:用于 style 3 的蓝图标题信息框

### 风格选择指南

**UML 图类型:**
- **类图/组件图/包图**:风格 1(扁平图标风)或风格 4(Notion 极简风)— 结构清晰,易于阅读
- **序列图/时序图**:风格 2(暗黑极客风)— 等宽字体有助于对齐
- **状态机图/活动图**:风格 3(工程蓝图风)— 工程美学适合流程图
- **用例图/交互图**:风格 1(扁平图标风)— 彩色,易于理解

**AI/Agent 图类型:**
- **RAG/Agentic Search**:风格 2(暗黑极客风)或风格 5(玻璃态卡片风)— 科技感强
- **记忆架构**:风格 3(工程蓝图风)— 强调分层存储结构
- **Multi-Agent**:风格 5(玻璃态卡片风)— 磨砂卡片区分 Agent 边界

**文档类型:**
- **内部文档**:风格 4(Notion 极简风)— 极简,适合 Wiki
- **技术博客**:风格 1(扁平图标风)— 彩色,吸引眼球
- **GitHub README**:风格 2(暗黑极客风)— 匹配暗色主题
- **演示文稿**:风格 5(玻璃态卡片风)或风格 6(Claude 官方风格)— 精致专业

**品牌特定:**
- **Anthropic/Claude 项目**:风格 6(Claude 官方风格)— 温暖奶油色背景,品牌感强且克制
- **OpenAI 项目**:风格 7(OpenAI 官方风格)— 简洁白色,OpenAI 配色
- **高级编辑感技术图**:风格 8(暗黑奢华风)— 深黑画布、香槟金层级和语义色桶

**工程评审:**
- **C4/ADR 评审**:风格 9(C4 评审画布)— 单一抽象层级、职责与协议明确
- **云部署评审**:风格 10(Cloud Fabric)— Region/Network 归属与跨边界机制明确
- **事件驱动系统**:风格 11(Event Transit)— Topic、Processor、Consumer Group、状态与 DLQ
- **可靠性/事故复盘**:风格 12(Ops Pulse)— Golden Signals、唯一关键路径与关联 Trace

---

## 支持的图类型

| 类型 | 描述 | 关键布局规则 |
|------|------|-------------|
| **架构图** | 服务、组件、云基础设施 | 水平分层,自上而下 |
| **数据流图** | 数据在系统中的流向 | 每条箭头标注数据类型 |
| **流程图** | 决策树、流程步骤 | 菱形 = 决策,自上而下 |
| **Agent 架构图** | LLM + 工具 + 记忆 | 五层模型:输入/Agent/记忆/工具/输出 |
| **记忆架构图** | Mem0、MemGPT 风格 | 读/写路径分离,记忆层级分明 |
| **序列图** | API 调用链、时序交互 | 垂直生命线,水平消息箭头 |
| **对比图** | 功能矩阵、方案比较 | 列 = 系统,行 = 属性 |
| **思维导图** | 概念地图、发散思维 | 中心节点,贝塞尔曲线分支 |

### UML 图类型支持(14 种)

| UML 类型 | 描述 | 推荐风格 |
|----------|------|----------|
| **类图** | 类、属性、方法、关系 | 风格 1, 4 |
| **组件图** | 软件组件和依赖关系 | 风格 1, 3 |
| **部署图** | 硬件节点和软件部署 | 风格 3 |
| **包图** | 包组织和依赖关系 | 风格 1, 4 |
| **复合结构图** | 类/组件的内部结构 | 风格 1, 3 |
| **对象图** | 对象实例和关系 | 风格 1, 4 |
| **用例图** | 参与者、用例、系统边界 | 风格 1 |
| **活动图** | 工作流、并行流程 | 风格 3 |
| **状态机图** | 状态转换和事件 | 风格 2, 3 |
| **序列图** | 时间顺序的消息交换 | 风格 2 |
| **通信图** | 对象交互和消息 | 风格 1, 2 |
| **时序图** | 状态随时间的变化 | 风格 2 |
| **交互概览图** | 高层交互流程 | 风格 1, 2 |
| **ER 图** | 实体关系数据模型 | 风格 1, 3 |

---

## AI/Agent 领域内建 Pattern

Skill 内置以下领域知识,可直接描述场景生成:

```
RAG Pipeline         → Query → Embed → VectorSearch → Retrieve → LLM → Response
Agentic RAG          → 在 RAG 基础上加入 Agent 循环 + 工具调用
Agentic Search       → Query → Planner → [Search/Calc/Code] → Synthesizer
Mem0 记忆层          → Input → Memory Manager → [VectorDB + GraphDB] → Context
Agent 记忆类型       → 感知记忆 → 工作记忆 → 情景记忆 → 语义记忆 → 程序记忆
Multi-Agent          → Orchestrator → [SubAgent×N] → Aggregator → Output
Tool Call 流程       → LLM → Tool Selector → Execution → Parser → LLM (循环)
```

---

## 形状词汇表

形状在所有风格中保持一致的语义:

| 概念 | 形状 |
|------|------|
| 用户 / 人类 | 圆形 + 身体路径 |
| LLM / 模型 | 圆角矩形,双边框,⚡ |
| Agent / 编排器 | 六边形 |
| 短期记忆 | 虚线边框圆角矩形 |
| 长期记忆 | 实线圆柱体 |
| Vector Store | 带内环圆柱 |
| Graph DB | 三圆簇 |
| 工具 / 函数 | 带 ⚙ 的矩形 |
| API / 网关 | 六边形(单边框) |
| 消息队列 / 流 | 横向管道 |
| 文档 / 文件 | 折角矩形 |
| 浏览器 / UI | 带三点标题栏的矩形 |
| 决策节点 | 菱形 |
| 外部服务 | 虚线边框矩形 |

---

## 箭头语义

| 流类型 | 线宽 | 虚线 | 含义 |
|--------|------|------|------|
| 主数据流 | 2px 实线 | — | 主要请求/响应路径 |
| 控制 / 触发 | 1.5px 实线 | — | 系统 A 触发 B |
| 记忆读取 | 1.5px 实线 | — | 从存储检索 |
| 记忆写入 | 1.5px | `5,3` | 写入/存储操作 |
| 异步 / 事件 | 1.5px | `4,2` | 非阻塞 |
| 反馈 / 循环 | 1.5px 曲线 | — | 迭代推理 |

---

## 文件结构

```
fireworks-tech-graph/
├── SKILL.md                      # 主 Skill 文件 — 图类型、布局规则、形状词汇
├── README.md                     # 英文文档
├── README.zh.md                  # 本文件(中文)
├── references/
│   ├── style-1-flat-icon.md      # 白底风格 — 彩色强调色
│   ├── style-2-dark-terminal.md  # 暗黑风格 — Neon 配色,等宽字体
│   ├── style-3-blueprint.md      # 蓝图风格 — 网格底纹,青色线条
│   ├── style-4-notion-clean.md   # 极简风格 — 白底,单色箭头
│   ├── style-5-glassmorphism.md  # 玻璃态风格 — 深色渐变,磨砂卡片
│   ├── style-6-claude-official.md # Claude 官方风格 — 温暖奶油色,Anthropic 品牌
│   ├── style-7-openai.md         # OpenAI 官方风格 — 简洁白色,OpenAI 品牌配色
│   ├── style-8-dark-luxury.md    # 深黑画布、香槟金、AI 手绘布局
│   ├── style-9-c4-review-canvas.md # C4 评审语义 + 确定性手绘纹理
│   ├── style-10-cloud-fabric.md  # Deployment 归属 + 中性 Cloud Glyph
│   ├── style-11-event-transit.md # Topic 轨道、站点、Junction 与 DLQ
│   ├── style-12-ops-pulse.md     # Golden Signals、关键路径与 Trace Waterfall
│   ├── png-export.md             # 渲染器选择与手动导出方案
│   └── icons.md                  # 40+ 产品图标 + 语义形状模板
├── agents/
│   └── openai.yaml              # Codex 可选 UI 元数据
├── schemas/                      # 版本化 Diagram JSON Schema
├── docs/                         # 能力契约与路线图
├── examples/
│   └── interactive-architecture.html # 离线平移/缩放/导出演示
├── fixtures/
│   ├── mem0-style1.json          # Style 1 · Mem0 记忆场景
│   ├── tool-call-style2.json     # Style 2 · Tool Call 场景
│   ├── microservices-style3.json # Style 3 · 微服务蓝图
│   ├── agent-memory-types-style4.json # Style 4 · 记忆层级
│   ├── multi-agent-style5.json   # Style 5 · 多 Agent 协作
│   ├── system-architecture-style6.json # Style 6 · 系统分层
│   ├── api-flow-style7.json      # Style 7 · API 集成
│   ├── dark-luxury-style8.svg    # Style 8 · AI 手绘 Runtime 场景
│   ├── c4-review-canvas-style9.json # Style 9 · Checkout C4 评审
│   ├── cloud-fabric-style10.json # Style 10 · Active-Active Deployment
│   ├── event-transit-style11.json # Style 11 · Checkout Event Line
│   ├── ops-pulse-style12.json    # Style 12 · Reliability Pulse
│   └── quality-baseline/         # 内部同拓扑质量回归基线
├── scripts/
│   ├── fireworks.py              # 统一 validate/render/check/animate/export CLI
│   ├── diagram_ir.py             # Schema v1 类型化归一层
│   ├── fireworks_geometry.py     # 路由与碰撞共享语义
│   ├── interactive_html.py       # 安全的离线 HTML 导出器
│   ├── generate-diagram.sh       # SVG 校验与 PNG 导出
│   ├── generate-from-template.py # 基于模板生成 SVG 起始文件
│   ├── motion.py                 # SVG 转 GIF 校验、编码与原子报告
│   ├── svg2gif.js                # 手动时间轴 Chromium 逐帧渲染器
│   ├── svg2png.js                 # Puppeteer 高保真导出脚本
│   ├── validate-svg.sh           # 校验与渲染检查入口
│   ├── validate_svg.py           # XML、marker、transform 与路径碰撞检测
│   └── test-all-styles.sh        # 批量测试所有风格
├── tests/
│   ├── test_geometry_contracts.py # 路由与产物几何门禁
│   └── ...                       # IR、CLI、导出器、安装兼容测试
├── tools/                         # 分发、项目一致性、安装 canary
├── skills/fireworks-tech-graph/  # 完整的 npx 兼容物理镜像
├── assets/
│   └── samples/                  # 示例图 PNG
├── templates/
│   ├── architecture.svg         # 架构图模板
│   ├── data-flow.svg            # 数据流模板
│   └── ...                      # 其他图类型模板
└── agentloop-core.svg           # 仓库自带示例 SVG
```

---

## 产品图标覆盖范围

**AI/ML 模型:** OpenAI、Anthropic/Claude、Google Gemini、Meta LLaMA、Mistral、Cohere、Groq、Hugging Face

**AI 框架:** Mem0、LangChain、LlamaIndex、LangGraph、CrewAI、AutoGen、DSPy、Haystack

**向量数据库:** Pinecone、Weaviate、Qdrant、Chroma、Milvus、pgvector、Faiss

**关系型/NoSQL 数据库:** PostgreSQL、MySQL、MongoDB、Redis、Elasticsearch、Neo4j、Cassandra

**消息队列:** Kafka、RabbitMQ、NATS、Pulsar

**云服务 & 基础设施:** AWS、GCP、Azure、Cloudflare、Vercel、Docker、Kubernetes

**可观测性:** Grafana、Prometheus、Datadog、LangSmith、Langfuse、Arize

---

## 故障排查

| 现象 | 原因 | 处理方式 |
|------|------|----------|
| `npx skills add` 后只有 `SKILL.md` | 使用了裸仓库路径,CLI 选中了根 Skill | 使用 `yizhiyanhua-ai/fireworks-tech-graph/skills/fireworks-tech-graph` 嵌套路径重新安装 |
| SVG 校验报告 `edge_node` / `edge_reserved` | 业务线穿过节点、图例或标题区 | 调整端口、`corridor_x/y` 或 waypoint,再重新渲染;不要手工删除 geometry gate |
| SVG 校验报告 `edge_overlap` | 两条业务线共用同一段轨道 | 给边分配独立 corridor 或 waypoint;跨线桥不能修复共线重叠 |
| PNG 为空或纯黑 | SVG 中含外部字体 `@import url()` | 移除 `@import`,使用系统字体栈 |
| PNG 未生成 | 未安装 raster renderer | 推荐 `python3 -m pip install cairosvg`,也可安装 `rsvg-convert` |
| 交互 HTML 导出拒绝 SVG | SVG 含脚本、事件属性、外部链接或 `foreignObject` | 将资源内联并移除 active content 后重新导出 |
| 图底部被截断 | `viewBox` 高度不足 | 增加画布 `height`,再执行严格 geometry check |

---

## License

MIT © 2025 fireworks-tech-graph contributors
skills/fireworks-tech-graph/examples/README.md
# Interactive example

Open `interactive-architecture.html` directly in a browser. It is a single offline file generated from `fixtures/api-flow-style7.json` and contains no remote runtime dependencies.

Regenerate it with:

```bash
tmp_dir="$(mktemp -d)"
trap 'rm -rf "$tmp_dir"' EXIT
tmp_svg="$tmp_dir/api-flow.svg"
python3 scripts/fireworks.py render architecture fixtures/api-flow-style7.json "$tmp_svg"
python3 scripts/fireworks.py export-html "$tmp_svg" examples/interactive-architecture.html --title "API Integration Flow" --slug api-integration-flow
```
skills/fireworks-tech-graph/fixtures/agent-memory-types-style4.json
{
  "schema_version": 1,
  "mode": "memory",
  "template_type": "memory",
  "style": 4,
  "motion_scene": "memory-lifecycle",
  "quality_profile": "showcase",
  "width": 960,
  "height": 620,
  "title": "Agent Memory Types",
  "subtitle": "from active perception to durable knowledge and reusable procedures",
  "containers": [
    { "id": "active-context", "x": 40, "y": 120, "width": 880, "height": 165, "label": "Active Context" },
    { "id": "long-term", "x": 40, "y": 335, "width": 880, "height": 185, "label": "Long-term Memory" }
  ],
  "nodes": [
    {
      "id": "sensory", "kind": "rect", "x": 60, "y": 165, "width": 160, "height": 90,
      "type_label": "SENSORY MEMORY", "label": "Raw Input",
      "fill": "#ffffff", "stroke": "#e5e7eb", "flat": true,
      "tags": [{ "label": "RAM / context", "fill": "#eff6ff", "stroke": "#bfdbfe", "text_fill": "#3b82f6" }]
    },
    {
      "id": "working", "kind": "rect", "x": 270, "y": 165, "width": 160, "height": 90,
      "type_label": "WORKING MEMORY", "label": "Active Context",
      "fill": "#ffffff", "stroke": "#e5e7eb", "flat": true,
      "tags": [{ "label": "Context Window", "fill": "#eff6ff", "stroke": "#bfdbfe", "text_fill": "#3b82f6" }]
    },
    {
      "id": "agent", "kind": "hexagon", "x": 490, "y": 165, "width": 160, "height": 90,
      "type_label": "AGENT CORE", "label": "LLM + Planner", "sublabel": "reason + decide",
      "fill": "#ffffff", "stroke": "#3b82f6", "stroke_width": 1.6, "flat": true
    },
    {
      "id": "procedural", "kind": "rect", "x": 700, "y": 165, "width": 180, "height": 90,
      "type_label": "PROCEDURAL MEMORY", "label": "Skills + How-to",
      "fill": "#ffffff", "stroke": "#e5e7eb", "flat": true,
      "tags": [{ "label": "Code / Skills", "fill": "#f5f3ff", "stroke": "#ddd6fe", "text_fill": "#7c3aed" }]
    },
    {
      "id": "episodic", "kind": "rect", "x": 270, "y": 400, "width": 160, "height": 90,
      "type_label": "EPISODIC MEMORY", "label": "Past Interactions",
      "fill": "#ffffff", "stroke": "#e5e7eb", "flat": true,
      "tags": [{ "label": "Vector DB + time", "fill": "#ecfdf5", "stroke": "#a7f3d0", "text_fill": "#059669" }]
    },
    {
      "id": "semantic", "kind": "rect", "x": 490, "y": 400, "width": 160, "height": 90,
      "type_label": "SEMANTIC MEMORY", "label": "Facts + Knowledge",
      "fill": "#ffffff", "stroke": "#e5e7eb", "flat": true,
      "tags": [{ "label": "Vector + Graph", "fill": "#fff7ed", "stroke": "#fed7aa", "text_fill": "#ea580c" }]
    }
  ],
  "arrows": [
    { "id": "sample", "source": "sensory", "target": "working", "source_port": "right", "target_port": "left", "flow": "read", "motion_role": "sample", "motion_stage": 1, "motion_order": 0 },
    { "id": "attend", "source": "working", "target": "agent", "source_port": "right", "target_port": "left", "flow": "control", "motion_role": "attend", "motion_stage": 2, "motion_order": 0 },
    { "id": "invoke", "source": "agent", "target": "procedural", "source_port": "right", "target_port": "left", "flow": "control", "motion_role": "invoke", "motion_stage": 3, "motion_order": 0 },
    { "id": "remember", "source": "working", "target": "episodic", "source_port": "bottom", "target_port": "top", "flow": "write", "motion_role": "remember", "motion_stage": 4, "motion_order": 0, "label": "remember" },
    { "id": "recall", "source": "semantic", "target": "agent", "source_port": "top", "target_port": "bottom", "flow": "read", "motion_role": "recall", "motion_stage": 6, "motion_order": 0, "label": "recall" },
    { "id": "consolidate", "source": "episodic", "target": "semantic", "source_port": "right", "target_port": "left", "flow": "write", "motion_role": "consolidate", "motion_stage": 5, "motion_order": 0 }
  ],
  "footer": "Style 4 · Notion Clean · five complementary memory systems",
  "footer_x": 48,
  "footer_y": 590
}
skills/fireworks-tech-graph/fixtures/api-flow-style7.json
{
  "schema_version": 1,
  "mode": "architecture",
  "template_type": "architecture",
  "style": 7,
  "motion_scene": "token-stream",
  "quality_profile": "showcase",
  "width": 960,
  "height": 700,
  "title": "API Integration Flow",
  "subtitle": "clean application integration from SDK input to governed delivery",
  "containers": [
    { "id": "entry", "x": 40, "y": 120, "width": 880, "height": 110, "label": "Integration", "stroke": "#e2e8f0", "fill": "none" },
    { "id": "runtime", "x": 40, "y": 280, "width": 880, "height": 130, "label": "Model + Tools", "stroke": "#e2e8f0", "fill": "none" },
    { "id": "delivery", "x": 40, "y": 450, "width": 880, "height": 140, "label": "Delivery", "stroke": "#e2e8f0", "fill": "none" }
  ],
  "nodes": [
    { "id": "app", "kind": "rect", "x": 80, "y": 156, "width": 180, "height": 54, "type_label": "CLIENT", "label": "Application", "fill": "#ffffff", "stroke": "#dce5e3", "flat": true },
    { "id": "sdk", "kind": "double_rect", "x": 370, "y": 156, "width": 220, "height": 54, "type_label": "SDK", "label": "OpenAI SDK Layer", "fill": "#ffffff", "stroke": "#10a37f" },
    { "id": "prompt", "kind": "rect", "x": 80, "y": 340, "width": 180, "height": 50, "type_label": "INPUT", "label": "Prompt Builder", "fill": "#ffffff", "stroke": "#dce5e3", "flat": true },
    { "id": "model", "kind": "double_rect", "x": 370, "y": 340, "width": 220, "height": 50, "type_label": "REASONING", "label": "Model Runtime", "fill": "#ffffff", "stroke": "#10a37f" },
    { "id": "tools", "kind": "rect", "x": 700, "y": 340, "width": 180, "height": 50, "type_label": "ACTIONS", "label": "Tool Calls", "fill": "#ffffff", "stroke": "#dce5e3", "flat": true },
    { "id": "formatter", "kind": "rect", "x": 80, "y": 510, "width": 180, "height": 60, "type_label": "OUTPUT", "label": "Response Formatter", "fill": "#ffffff", "stroke": "#dce5e3", "flat": true },
    { "id": "observability", "kind": "rect", "x": 390, "y": 510, "width": 180, "height": 60, "type_label": "METRICS", "label": "Observability", "fill": "#ffffff", "stroke": "#dce5e3", "flat": true },
    { "id": "release", "kind": "rect", "x": 700, "y": 510, "width": 180, "height": 60, "type_label": "CONFIG", "label": "Release Control", "fill": "#ffffff", "stroke": "#dce5e3", "flat": true }
  ],
  "arrows": [
    { "id": "connect", "source": "app", "target": "sdk", "source_port": "right", "target_port": "left", "flow": "control", "motion_role": "connect", "motion_stage": 1, "motion_order": 0, "label": "connect" },
    { "id": "prepare", "source": "sdk", "target": "prompt", "source_port": "bottom", "target_port": "top", "flow": "read", "motion_role": "prepare", "motion_stage": 2, "motion_order": 0, "label": "prepare" },
    { "id": "invoke", "source": "prompt", "target": "model", "source_port": "right", "target_port": "left", "flow": "control", "motion_role": "invoke", "motion_stage": 3, "motion_order": 0, "label": "invoke" },
    { "id": "tool-call", "source": "model", "target": "tools", "source_port": "right", "target_port": "left", "flow": "read", "motion_role": "tool-call", "motion_stage": 4, "motion_order": 0, "label": "tool call" },
    { "id": "stream", "source": "model", "target": "formatter", "source_port": "bottom", "target_port": "top", "flow": "control", "motion_role": "token-stream", "motion_stage": 4, "motion_order": 1, "label": "stream" },
    { "id": "measure", "source": "formatter", "target": "observability", "source_port": "right", "target_port": "left", "flow": "feedback", "motion_role": "measure", "motion_stage": 5, "motion_order": 1, "label": "measure" },
    { "id": "govern", "source": "tools", "target": "release", "source_port": "bottom", "target_port": "top", "flow": "feedback", "motion_role": "govern", "motion_stage": 5, "motion_order": 0, "label": "govern" },
    { "id": "promote", "source": "observability", "target": "release", "source_port": "right", "target_port": "left", "flow": "feedback", "motion_role": "promote", "motion_stage": 6, "motion_order": 0, "label": "promote" }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 625,
  "legend_locked": true,
  "legend": [
    { "flow": "control", "label": "primary API path" },
    { "flow": "read", "label": "prompt / tools" },
    { "flow": "feedback", "label": "governance" }
  ],
  "footer": "Style 7 · OpenAI Official · precise integration stages",
  "footer_x": 48,
  "footer_y": 680
}
skills/fireworks-tech-graph/fixtures/c4-review-canvas-style9.json
{
  "schema_version": 1,
  "mode": "architecture",
  "template_type": "architecture",
  "style": 9,
  "motion_scene": "review-trace",
  "semantic_profile": "c4-review",
  "diagram_type": "c4",
  "c4_level": "container",
  "review_state": "ADR READY",
  "rough_seed": 20260715,
  "scope": "Checkout Platform",
  "quality_profile": "showcase",
  "width": 1100,
  "height": 700,
  "title": "Checkout Platform · Container Review",
  "subtitle": "one abstraction level, explicit responsibilities, review-ready relationships",
  "containers": [
    {
      "id": "checkout-boundary",
      "x": 260,
      "y": 150,
      "width": 600,
      "height": 420,
      "label": "Checkout Platform",
      "subtitle": "C4 container boundary"
    }
  ],
  "nodes": [
    {
      "id": "shopper",
      "kind": "review_card",
      "c4_type": "person",
      "x": 50,
      "y": 220,
      "width": 150,
      "height": 76,
      "label": "Shopper",
      "description": "places an order",
      "stroke": "#356a8a"
    },
    {
      "id": "web-app",
      "kind": "review_card",
      "c4_type": "container",
      "parent": "checkout-boundary",
      "x": 300,
      "y": 210,
      "width": 200,
      "height": 96,
      "label": "Checkout Web",
      "description": "collects cart + payment intent",
      "technology": "Next.js"
    },
    {
      "id": "checkout-api",
      "kind": "review_card",
      "c4_type": "container",
      "parent": "checkout-boundary",
      "x": 600,
      "y": 210,
      "width": 200,
      "height": 96,
      "label": "Checkout API",
      "description": "coordinates the purchase flow",
      "technology": "Go + gRPC",
      "stroke": "#a44a3f"
    },
    {
      "id": "order-store",
      "kind": "review_card",
      "c4_type": "container",
      "parent": "checkout-boundary",
      "x": 300,
      "y": 400,
      "width": 200,
      "height": 96,
      "label": "Order Store",
      "description": "persists order state",
      "technology": "PostgreSQL",
      "stroke": "#356a8a"
    },
    {
      "id": "order-worker",
      "kind": "review_card",
      "c4_type": "container",
      "parent": "checkout-boundary",
      "x": 600,
      "y": 400,
      "width": 200,
      "height": 96,
      "label": "Order Worker",
      "description": "confirms and fulfils orders",
      "technology": "Kotlin",
      "stroke": "#c06b35"
    },
    {
      "id": "payment-provider",
      "kind": "review_card",
      "c4_type": "external_system",
      "x": 900,
      "y": 410,
      "width": 150,
      "height": 76,
      "label": "Payment PSP",
      "description": "authorizes cards",
      "stroke": "#7a5c99"
    }
  ],
  "arrows": [
    {
      "id": "browse-checkout",
      "source": "shopper",
      "target": "web-app",
      "source_port": "right",
      "target_port": "left",
      "flow": "read",
      "motion_role": "review-entry",
      "motion_stage": 1,
      "motion_order": 0,
      "label": "start",
      "protocol": "HTTPS"
    },
    {
      "id": "submit-order",
      "source": "web-app",
      "target": "checkout-api",
      "source_port": "right",
      "target_port": "left",
      "flow": "control",
      "motion_role": "review-request",
      "motion_stage": 2,
      "motion_order": 0,
      "label": "order",
      "protocol": "JSON/HTTPS"
    },
    {
      "id": "enqueue-order",
      "source": "checkout-api",
      "target": "order-worker",
      "source_port": "bottom",
      "target_port": "top",
      "flow": "async",
      "motion_role": "review-async",
      "motion_stage": 3,
      "motion_order": 0,
      "label": "enqueue",
      "protocol": "Kafka"
    },
    {
      "id": "persist-order",
      "source": "order-worker",
      "target": "order-store",
      "source_port": "left",
      "target_port": "right",
      "flow": "write",
      "motion_role": "review-state",
      "motion_stage": 4,
      "motion_order": 0,
      "label": "save",
      "protocol": "SQL"
    },
    {
      "id": "authorize-payment",
      "source": "order-worker",
      "target": "payment-provider",
      "source_port": "right",
      "target_port": "left",
      "flow": "control",
      "motion_role": "review-external",
      "motion_stage": 4,
      "motion_order": 1,
      "label": "authorize",
      "protocol": "HTTPS"
    }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 620,
  "legend_locked": true,
  "legend": [
    { "flow": "control", "label": "request" },
    { "flow": "write", "label": "state change" },
    { "flow": "async", "label": "asynchronous" }
  ],
  "footer": "Style 9 · C4 Review Canvas · deterministic review marks · container-level semantics",
  "footer_x": 48,
  "footer_y": 680
}
skills/fireworks-tech-graph/fixtures/event-transit-style11.json
{
  "schema_version": 1,
  "mode": "flow",
  "template_type": "flow",
  "style": 11,
  "semantic_profile": "event-transit",
  "diagram_type": "event_stream",
  "line_code": "LINE A · EVENT METRO",
  "quality_profile": "showcase",
  "width": 1172,
  "height": 720,
  "title": "Checkout Event Line",
  "subtitle": "topics as rails, processors as stations, branches only at declared junctions",
  "topics": [
    { "id": "checkout.events", "color": "#e4475b" },
    { "id": "checkout.dlq", "color": "#c62828" }
  ],
  "containers": [
    {
      "id": "checkout-stream",
      "x": 35,
      "y": 140,
      "width": 1102,
      "height": 410,
      "label": "checkout.events",
      "subtitle": "12 partitions · retention 72 h",
      "header_prefix": "LINE A",
      "header_separator": " · "
    }
  ],
  "nodes": [
    {
      "id": "checkout-api",
      "kind": "transit_terminal",
      "transit_role": "producer",
      "topic_id": "checkout.events",
      "station_order": 0,
      "x": 60,
      "y": 258,
      "width": 150,
      "height": 78,
      "label": "Checkout API",
      "sublabel": "producer",
      "rail_color": "#e4475b",
      "badge": "P12"
    },
    {
      "id": "schema-gate",
      "kind": "transit_station",
      "transit_role": "station",
      "topic_id": "checkout.events",
      "station_order": 1,
      "operation": "validate envelope",
      "x": 278,
      "y": 258,
      "width": 150,
      "height": 78,
      "label": "Schema Gate",
      "sublabel": "validate",
      "rail_color": "#e4475b",
      "badge": "v3"
    },
    {
      "id": "fraud-score",
      "kind": "transit_station",
      "transit_role": "station",
      "topic_id": "checkout.events",
      "station_order": 2,
      "operation": "score risk",
      "x": 496,
      "y": 258,
      "width": 150,
      "height": 78,
      "label": "Fraud Score",
      "sublabel": "enrich",
      "rail_color": "#e4475b",
      "badge": "p95 32ms"
    },
    {
      "id": "order-router",
      "kind": "transit_junction",
      "transit_role": "junction",
      "topic_id": "checkout.events",
      "station_order": 3,
      "operation": "route accepted orders",
      "x": 714,
      "y": 258,
      "width": 150,
      "height": 78,
      "label": "Order Router",
      "sublabel": "junction",
      "rail_color": "#e4475b",
      "badge": "fan-out"
    },
    {
      "id": "fulfilment",
      "kind": "transit_terminal",
      "transit_role": "consumer",
      "topic_id": "checkout.events",
      "station_order": 4,
      "consumer_group": "fulfilment-v2",
      "x": 932,
      "y": 258,
      "width": 180,
      "height": 78,
      "label": "Fulfilment",
      "sublabel": "consumer group",
      "rail_color": "#e4475b",
      "badge": "lag 14"
    },
    {
      "id": "checkout-dlq",
      "kind": "transit_terminal",
      "transit_role": "dlq",
      "topic_id": "checkout.dlq",
      "x": 496,
      "y": 430,
      "width": 150,
      "height": 78,
      "label": "Checkout DLQ",
      "sublabel": "manual replay",
      "stroke": "#c62828",
      "badge": "7 events"
    },
    {
      "id": "order-state",
      "kind": "transit_terminal",
      "transit_role": "state_store",
      "topic_id": "checkout.events",
      "x": 714,
      "y": 430,
      "width": 150,
      "height": 78,
      "label": "Order State",
      "sublabel": "materialized view",
      "rail_color": "#00897b",
      "badge": "RocksDB"
    }
  ],
  "arrows": [
    { "id": "rail-01", "source": "checkout-api", "target": "schema-gate", "source_port": "right", "target_port": "left", "flow": "control", "transit_type": "rail", "topic_id": "checkout.events", "motion_role": "topic-rail", "motion_stage": 1, "motion_order": 0 },
    { "id": "rail-12", "source": "schema-gate", "target": "fraud-score", "source_port": "right", "target_port": "left", "flow": "control", "transit_type": "rail", "topic_id": "checkout.events", "motion_role": "topic-rail", "motion_stage": 2, "motion_order": 0 },
    { "id": "rail-23", "source": "fraud-score", "target": "order-router", "source_port": "right", "target_port": "left", "flow": "control", "transit_type": "rail", "topic_id": "checkout.events", "motion_role": "topic-rail", "motion_stage": 3, "motion_order": 0 },
    { "id": "rail-34", "source": "order-router", "target": "fulfilment", "source_port": "right", "target_port": "left", "flow": "control", "transit_type": "rail", "topic_id": "checkout.events", "motion_role": "topic-rail", "motion_stage": 4, "motion_order": 0 },
    { "id": "to-dlq", "source": "fraud-score", "target": "checkout-dlq", "source_port": "bottom", "target_port": "top", "flow": "feedback", "transit_type": "dead_letter", "topic_id": "checkout.dlq", "motion_role": "dead-letter", "motion_stage": 5, "motion_order": 0, "dashed": true },
    { "id": "materialize", "source": "order-router", "target": "order-state", "source_port": "bottom", "target_port": "top", "flow": "write", "transit_type": "state", "topic_id": "checkout.events", "motion_role": "state-project", "motion_stage": 5, "motion_order": 1 }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 610,
  "legend_locked": true,
  "legend": [
    { "flow": "control", "label": "topic rail" },
    { "flow": "write", "label": "state projection" },
    { "flow": "feedback", "label": "dead-letter route" }
  ],
  "footer": "Style 11 · Event Transit · ordinary edges remain the semantic rails",
  "footer_x": 48,
  "footer_y": 695
}
skills/fireworks-tech-graph/fixtures/microservices-style3.json
{
  "schema_version": 1,
  "mode": "architecture",
  "template_type": "architecture",
  "style": 3,
  "motion_scene": "service-blueprint",
  "quality_profile": "showcase",
  "width": 960,
  "height": 720,
  "title": "Microservices Architecture",
  "subtitle": "edge routing, domain services, data stores, events, and observability",
  "containers": [
    { "id": "edge", "x": 40, "y": 110, "width": 880, "height": 110, "label": "Edge", "header_prefix": "01" },
    { "id": "services", "x": 40, "y": 270, "width": 880, "height": 130, "label": "Application Services", "header_prefix": "02" },
    { "id": "data", "x": 40, "y": 420, "width": 680, "height": 130, "label": "Data" },
    { "id": "observability", "x": 740, "y": 420, "width": 180, "height": 130, "label": "Obs" }
  ],
  "nodes": [
    { "id": "clients", "kind": "rect", "x": 60, "y": 146, "width": 160, "height": 54, "label": "Client Apps", "sublabel": "web · mobile", "fill": "#0b3b5e", "stroke": "#94a3b8", "flat": true },
    { "id": "gateway", "kind": "double_rect", "x": 300, "y": 146, "width": 160, "height": 54, "label": "API Gateway", "sublabel": "route + throttle", "fill": "#0a4b78", "stroke": "#38bdf8", "flat": true },
    { "id": "auth", "kind": "hexagon", "x": 570, "y": 146, "width": 160, "height": 54, "label": "Auth / Policy", "sublabel": "identity gate", "fill": "#0b3b5e", "stroke": "#f59e0b", "flat": true },
    { "id": "order", "kind": "rect", "x": 80, "y": 330, "width": 140, "height": 50, "label": "Order Service", "sublabel": "transactions", "fill": "#0a4b78", "stroke": "#38bdf8", "flat": true },
    { "id": "catalog", "kind": "rect", "x": 310, "y": 330, "width": 140, "height": 50, "label": "Catalog Service", "sublabel": "inventory", "fill": "#0a4b78", "stroke": "#38bdf8", "flat": true },
    { "id": "billing", "kind": "rect", "x": 540, "y": 330, "width": 140, "height": 50, "label": "Billing Service", "sublabel": "payments", "fill": "#0a4b78", "stroke": "#38bdf8", "flat": true },
    { "id": "events", "kind": "hexagon", "x": 760, "y": 330, "width": 140, "height": 50, "label": "Event Router", "sublabel": "async bus", "fill": "#123c5a", "stroke": "#a855f7", "flat": true },
    { "id": "postgres", "kind": "cylinder", "x": 80, "y": 480, "width": 140, "height": 50, "label": "Postgres", "sublabel": "orders", "fill": "#082f49", "stroke": "#38bdf8" },
    { "id": "redis", "kind": "cylinder", "x": 310, "y": 480, "width": 140, "height": 50, "label": "Redis", "sublabel": "catalog cache", "fill": "#082f49", "stroke": "#38bdf8" },
    { "id": "warehouse", "kind": "cylinder", "x": 540, "y": 480, "width": 140, "height": 50, "label": "Warehouse", "sublabel": "billing facts", "fill": "#082f49", "stroke": "#38bdf8" },
    { "id": "metrics", "kind": "rect", "x": 765, "y": 480, "width": 130, "height": 50, "label": "Metrics", "sublabel": "traces + SLOs", "fill": "#123c5a", "stroke": "#10b981", "flat": true }
  ],
  "arrows": [
    { "id": "client-request", "source": "clients", "target": "gateway", "source_port": "right", "target_port": "left", "flow": "read", "motion_role": "ingress", "motion_stage": 1, "motion_order": 0, "label": "HTTPS" },
    { "id": "policy-check", "source": "gateway", "target": "auth", "source_port": "right", "target_port": "left", "flow": "control", "motion_role": "policy", "motion_stage": 2, "motion_order": 0, "label": "policy" },
    { "id": "a-route-order", "source": "gateway", "target": "order", "source_port": "bottom", "target_port": "top", "flow": "read", "motion_role": "fanout", "motion_stage": 3, "motion_order": 0 },
    { "id": "b-route-catalog", "source": "gateway", "target": "catalog", "source_port": "bottom", "target_port": "top", "flow": "read", "motion_role": "fanout", "motion_stage": 3, "motion_order": 1 },
    { "id": "c-route-billing", "source": "gateway", "target": "billing", "source_port": "bottom", "target_port": "top", "flow": "read", "motion_role": "fanout", "motion_stage": 3, "motion_order": 2 },
    { "id": "order-store", "source": "order", "target": "postgres", "source_port": "bottom", "target_port": "top", "flow": "data", "motion_role": "data-write", "motion_stage": 4, "motion_order": 0, "label": "persist" },
    { "id": "catalog-cache", "source": "catalog", "target": "redis", "source_port": "bottom", "target_port": "top", "flow": "data", "motion_role": "data-write", "motion_stage": 4, "motion_order": 1, "label": "cache" },
    { "id": "billing-store", "source": "billing", "target": "warehouse", "source_port": "bottom", "target_port": "top", "flow": "data", "motion_role": "data-write", "motion_stage": 4, "motion_order": 2, "label": "facts" },
    { "id": "publish-event", "source": "billing", "target": "events", "source_port": "right", "target_port": "left", "flow": "feedback", "motion_role": "event", "motion_stage": 5, "motion_order": 0, "label": "events" },
    { "id": "observe", "source": "events", "target": "metrics", "source_port": "bottom", "target_port": "top", "flow": "feedback", "motion_role": "telemetry", "motion_stage": 6, "motion_order": 0, "label": "telemetry" }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 600,
  "legend_locked": true,
  "legend": [
    { "flow": "read", "label": "request" },
    { "flow": "control", "label": "policy" },
    { "flow": "data", "label": "data path" },
    { "flow": "feedback", "label": "events" }
  ],
  "blueprint_title_block": {
    "title": "AI MICROSERVICES",
    "subtitle": "SYSTEM ARCHITECTURE",
    "center_caption": "STYLE 3",
    "left_caption": "REV: 1.1",
    "right_caption": "DWG: ARCH-003",
    "width": 220,
    "height": 76,
    "x": 700,
    "y": 620
  },
  "footer": "Style 3 · Blueprint · domain services and data planes",
  "footer_x": 48,
  "footer_y": 700
}
skills/fireworks-tech-graph/docs/releases/v1.0.0.md
# Fireworks Tech Graph v1.0.0

The first public npm release established Fireworks Tech Graph as a reusable Agent Skill for generating polished technical diagrams from natural-language requirements.

## Version information

- Version: `v1.0.0` / npm `1.0.0`
- Version date: 2026-04-12; npm published at `2026-04-12T00:09:17.387Z`
- Tag commit: [`27f41d98`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/27f41d98883113affbb28a2ebfff4b566e3cc819)
- npm registry `gitHead`: [`27f41d98`](https://github.com/yizhiyanhua-ai/fireworks-tech-graph/commit/27f41d98883113affbb28a2ebfff4b566e3cc819)
- Previous version: initial public release
- Runtime metadata: Node.js 14 or newer
- Distribution: [npm package `1.0.0`](https://www.npmjs.com/package/@yizhiyanhua-ai/fireworks-tech-graph/v/1.0.0)

## Highlights

- Shipped seven built-in visual styles for architecture, flow, UML, ER, sequence, and related technical diagrams.
- Included SVG generation, high-resolution PNG export through `rsvg-convert`, validation scripts, and bilingual documentation.
- Included style references and the first seven-style showcase.

## Provenance

This GitHub Release was backfilled on 2026-07-17. Of the 26 files in the published npm tarball, all 25 Git-backed files match the registry-recorded `gitHead` byte-for-byte; `package.json` was publication metadata that had not yet been committed. The tag is therefore the closest source provenance anchor, but GitHub's generated source archive is not byte-identical to the npm artifact.
skills/fireworks-tech-graph/fixtures/multi-agent-style5.json
{
  "schema_version": 1,
  "mode": "agent",
  "template_type": "agent",
  "style": 5,
  "motion_scene": "agent-orchestration",
  "quality_profile": "showcase",
  "width": 960,
  "height": 700,
  "title": "Multi-Agent Collaboration",
  "subtitle": "coordinator-led delegation, shared state, quality review, and synthesis",
  "containers": [
    { "id": "mission", "x": 40, "y": 110, "width": 880, "height": 110, "label": "Mission Control", "stroke": "rgba(255,255,255,0.20)", "fill": "rgba(255,255,255,0.05)" },
    { "id": "specialists", "x": 40, "y": 270, "width": 880, "height": 140, "label": "Specialist Agents", "stroke": "rgba(255,255,255,0.20)", "fill": "rgba(255,255,255,0.05)" },
    { "id": "delivery", "x": 40, "y": 450, "width": 880, "height": 140, "label": "Delivery", "stroke": "rgba(255,255,255,0.20)", "fill": "rgba(255,255,255,0.05)" }
  ],
  "nodes": [
    { "id": "request", "kind": "speech", "x": 60, "y": 146, "width": 160, "height": 54, "label": "User Brief", "sublabel": "goal + constraints", "fill": "rgba(255,255,255,0.12)", "stroke": "rgba(255,255,255,0.35)", "flat": true },
    { "id": "coordinator", "kind": "double_rect", "x": 390, "y": 146, "width": 200, "height": 54, "type_label": "ORCHESTRATOR", "label": "Coordinator Agent", "fill": "rgba(255,255,255,0.12)", "stroke": "#c084fc" },
    { "id": "research", "kind": "rect", "x": 80, "y": 330, "width": 180, "height": 60, "type_label": "SPECIALIST", "label": "Research Agent", "fill": "rgba(255,255,255,0.12)", "stroke": "#60a5fa" },
    { "id": "coding", "kind": "terminal", "x": 390, "y": 330, "width": 200, "height": 60, "label": "Coding Agent", "fill": "#0f172a", "stroke": "rgba(255,255,255,0.24)", "header_fill": "rgba(255,255,255,0.12)" },
    { "id": "review", "kind": "rect", "x": 700, "y": 330, "width": 180, "height": 60, "type_label": "QUALITY GATE", "label": "Review Agent", "fill": "rgba(255,255,255,0.12)", "stroke": "#34d399" },
    { "id": "memory", "kind": "cylinder", "x": 80, "y": 510, "width": 180, "height": 60, "label": "Shared Memory", "sublabel": "facts + plans", "fill": "rgba(255,255,255,0.08)", "stroke": "#34d399" },
    { "id": "synthesis", "kind": "double_rect", "x": 390, "y": 510, "width": 200, "height": 60, "type_label": "MERGE", "label": "Synthesis Engine", "fill": "rgba(255,255,255,0.12)", "stroke": "#c084fc" },
    { "id": "response", "kind": "speech", "x": 700, "y": 510, "width": 180, "height": 60, "label": "Final Response", "sublabel": "reviewed result", "fill": "rgba(255,255,255,0.12)", "stroke": "#f59e0b", "flat": true }
  ],
  "arrows": [
    { "id": "brief", "source": "request", "target": "coordinator", "source_port": "right", "target_port": "left", "flow": "read", "motion_role": "ingress", "motion_stage": 1, "motion_order": 0, "label": "brief" },
    { "id": "a-delegate-research", "source": "coordinator", "target": "research", "source_port": "bottom", "target_port": "top", "flow": "control", "motion_role": "delegate", "motion_stage": 2, "motion_order": 0, "label": "research" },
    { "id": "b-delegate-coding", "source": "coordinator", "target": "coding", "source_port": "bottom", "target_port": "top", "flow": "control", "motion_role": "delegate", "motion_stage": 2, "motion_order": 1, "label": "build" },
    { "id": "c-delegate-review", "source": "coordinator", "target": "review", "source_port": "bottom", "target_port": "top", "flow": "control", "motion_role": "delegate", "motion_stage": 2, "motion_order": 2, "label": "review" },
    { "id": "research-write", "source": "research", "target": "memory", "source_port": "bottom", "target_port": "top", "flow": "write", "motion_role": "evidence", "motion_stage": 3, "motion_order": 0, "label": "evidence" },
    { "id": "memory-merge", "source": "memory", "target": "synthesis", "source_port": "right", "target_port": "left", "flow": "read", "motion_role": "context", "motion_stage": 4, "motion_order": 0, "label": "context" },
    { "id": "coding-merge", "source": "coding", "target": "synthesis", "source_port": "bottom", "target_port": "top", "flow": "write", "motion_role": "artifact", "motion_stage": 3, "motion_order": 1, "label": "artifact" },
    { "id": "review-output", "source": "review", "target": "response", "source_port": "bottom", "target_port": "top", "flow": "feedback", "motion_role": "approval", "motion_stage": 5, "motion_order": 1, "label": "approval" },
    { "id": "deliver", "source": "synthesis", "target": "response", "source_port": "right", "target_port": "left", "flow": "feedback", "motion_role": "deliver", "motion_stage": 5, "motion_order": 0, "label": "deliver" }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 625,
  "legend_locked": true,
  "legend": [
    { "flow": "read", "label": "input / context" },
    { "flow": "control", "label": "delegation" },
    { "flow": "write", "label": "shared work" },
    { "flow": "feedback", "label": "reviewed output" }
  ],
  "footer": "Style 5 · Glassmorphism · coordinated specialist workflow",
  "footer_x": 48,
  "footer_y": 680
}
skills/fireworks-tech-graph/fixtures/mem0-style1.json
{
  "schema_version": 1,
  "mode": "memory",
  "template_type": "memory",
  "style": 1,
  "motion_scene": "memory-weave",
  "quality_profile": "showcase",
  "width": 960,
  "height": 680,
  "title": "Mem0 Memory Architecture",
  "subtitle": "personal memory extraction, conflict resolution, storage, and retrieval",
  "containers": [
    { "id": "input-layer", "x": 40, "y": 110, "width": 880, "height": 110, "label": "Input Layer" },
    { "id": "memory-core", "x": 40, "y": 260, "width": 880, "height": 140, "label": "Memory Manager" },
    { "id": "storage-output", "x": 40, "y": 440, "width": 880, "height": 140, "label": "Storage and Retrieval" }
  ],
  "nodes": [
    { "id": "user", "kind": "user_avatar", "x": 60, "y": 145, "width": 120, "height": 54, "label": "User", "fill": "#eff6ff", "stroke": "#3b82f6", "flat": true },
    { "id": "app", "kind": "rect", "x": 280, "y": 145, "width": 160, "height": 54, "label": "AI App / Agent", "sublabel": "conversation input", "fill": "#ffffff", "stroke": "#cbd5e1", "flat": true },
    { "id": "llm", "kind": "double_rect", "x": 560, "y": 145, "width": 140, "height": 54, "label": "LLM", "sublabel": "reason + extract", "fill": "#faf5ff", "stroke": "#8b5cf6", "flat": true },
    { "id": "extractor", "kind": "rect", "x": 100, "y": 320, "width": 170, "height": 60, "label": "Fact Extractor", "sublabel": "salient memories", "fill": "#fff7ed", "stroke": "#f97316", "flat": true },
    { "id": "manager", "kind": "double_rect", "x": 390, "y": 320, "width": 180, "height": 60, "label": "Memory Manager", "sublabel": "store · update · forget", "fill": "#faf5ff", "stroke": "#8b5cf6", "flat": true },
    { "id": "resolver", "kind": "rect", "x": 700, "y": 320, "width": 160, "height": 60, "label": "Conflict Resolver", "sublabel": "merge + supersede", "fill": "#fff7ed", "stroke": "#f97316", "flat": true },
    { "id": "vector", "kind": "cylinder", "x": 100, "y": 500, "width": 160, "height": 60, "label": "Vector Store", "sublabel": "semantic recall", "fill": "#ecfdf5", "stroke": "#10b981" },
    { "id": "graph", "kind": "rect", "x": 400, "y": 500, "width": 160, "height": 60, "label": "Graph Memory", "sublabel": "relations + history", "fill": "#eff6ff", "stroke": "#3b82f6", "flat": true },
    { "id": "response", "kind": "speech", "x": 700, "y": 500, "width": 160, "height": 60, "label": "Personalized Reply", "sublabel": "ranked context", "fill": "#faf5ff", "stroke": "#8b5cf6", "flat": true }
  ],
  "arrows": [
    { "id": "request", "source": "user", "target": "app", "source_port": "right", "target_port": "left", "flow": "control", "motion_role": "ingress", "motion_stage": 1, "motion_order": 0, "label": "message" },
    { "id": "reason", "source": "app", "target": "llm", "source_port": "right", "target_port": "left", "flow": "control", "motion_role": "reason", "motion_stage": 2, "motion_order": 0, "label": "reason" },
    { "id": "extract", "source": "app", "target": "extractor", "source_port": "bottom", "target_port": "top", "flow": "data", "motion_role": "extract", "motion_stage": 3, "motion_order": 0, "label": "extract", "corridor_y": [240] },
    { "id": "facts", "source": "extractor", "target": "manager", "source_port": "right", "target_port": "left", "flow": "data", "motion_role": "transform", "motion_stage": 4, "motion_order": 0, "label": "facts" },
    { "id": "resolved", "source": "resolver", "target": "manager", "source_port": "left", "target_port": "right", "flow": "feedback", "motion_role": "resolve", "motion_stage": 4, "motion_order": 1, "label": "resolved" },
    { "id": "write", "source": "manager", "target": "vector", "source_port": "bottom", "target_port": "top", "flow": "write", "motion_role": "memory-write", "motion_stage": 5, "motion_order": 0, "label": "write", "corridor_y": [420] },
    { "id": "relate", "source": "vector", "target": "graph", "source_port": "right", "target_port": "left", "flow": "read", "motion_role": "memory-read", "motion_stage": 6, "motion_order": 0, "label": "retrieve" },
    { "id": "context", "source": "graph", "target": "response", "source_port": "right", "target_port": "left", "flow": "control", "motion_role": "response-context", "motion_stage": 7, "motion_order": 0, "label": "context" }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 600,
  "legend_locked": true,
  "legend": [
    { "flow": "control", "label": "control" },
    { "flow": "write", "label": "memory write" },
    { "flow": "read", "label": "memory read" },
    { "flow": "data", "label": "data transform" }
  ],
  "footer": "Style 1 · Flat Icon · Mem0 memory lifecycle",
  "footer_x": 48,
  "footer_y": 650
}
skills/fireworks-tech-graph/fixtures/ops-pulse-style12.json
{
  "schema_version": 1,
  "mode": "architecture",
  "template_type": "architecture",
  "style": 12,
  "semantic_profile": "ops-pulse",
  "diagram_type": "observability",
  "observation_window": "5m",
  "quality_profile": "showcase",
  "width": 960,
  "height": 860,
  "title": "Checkout Reliability Pulse",
  "subtitle": "golden signals, critical request path, and one correlated trace waterfall",
  "critical_path_id": "checkout-request",
  "critical_path": ["edge-gateway-api", "edge-api-checkout", "edge-checkout-payment"],
  "containers": [
    {
      "id": "service-health",
      "x": 20,
      "y": 120,
      "width": 920,
      "height": 310,
      "label": "Service Health",
      "subtitle": "rolling 5 minute window"
    },
    {
      "id": "trace-waterfall",
      "x": 40,
      "y": 450,
      "width": 880,
      "height": 330,
      "label": "Trace 7F3A · checkout-request",
      "subtitle": "420 ms end-to-end · sampled on elevated latency"
    }
  ],
  "nodes": [
    {
      "id": "edge-gateway",
      "kind": "ops_service",
      "ops_role": "service",
      "x": 40,
      "y": 170,
      "width": 180,
      "height": 108,
      "label": "Edge Gateway",
      "status": "ok",
      "status_label": "HEALTHY",
      "signals": {
        "latency": { "value": "18", "unit": "ms", "window": "5m", "status": "ok" },
        "traffic": { "value": "8.2k", "unit": "rps", "window": "5m", "status": "ok" },
        "errors": { "value": "0.08", "unit": "%", "window": "5m", "status": "ok" },
        "saturation": { "value": "42", "unit": "%", "window": "5m", "status": "ok" }
      }
    },
    {
      "id": "api-gateway",
      "kind": "ops_service",
      "ops_role": "service",
      "x": 270,
      "y": 170,
      "width": 180,
      "height": 108,
      "label": "API Gateway",
      "status": "warn",
      "status_label": "WATCH",
      "signals": {
        "latency": { "value": "61", "unit": "ms", "window": "5m", "status": "warn" },
        "traffic": { "value": "7.9k", "unit": "rps", "window": "5m", "status": "ok" },
        "errors": { "value": "0.15", "unit": "%", "window": "5m", "status": "ok" },
        "saturation": { "value": "68", "unit": "%", "window": "5m", "status": "warn" }
      }
    },
    {
      "id": "checkout-service",
      "kind": "ops_service",
      "ops_role": "service",
      "x": 500,
      "y": 170,
      "width": 180,
      "height": 108,
      "label": "Checkout",
      "status": "critical",
      "status_label": "DEGRADED",
      "signals": {
        "latency": { "value": "286", "unit": "ms", "window": "5m", "status": "critical" },
        "traffic": { "value": "3.1k", "unit": "rps", "window": "5m", "status": "ok" },
        "errors": { "value": "2.6", "unit": "%", "window": "5m", "status": "critical" },
        "saturation": { "value": "84", "unit": "%", "window": "5m", "status": "warn" }
      }
    },
    {
      "id": "payment-service",
      "kind": "ops_service",
      "ops_role": "service",
      "x": 730,
      "y": 170,
      "width": 180,
      "height": 108,
      "label": "Payment",
      "status": "warn",
      "status_label": "WATCH",
      "signals": {
        "latency": { "value": "94", "unit": "ms", "window": "5m", "status": "warn" },
        "traffic": { "value": "2.8k", "unit": "rps", "window": "5m", "status": "ok" },
        "errors": { "value": "0.9", "unit": "%", "window": "5m", "status": "warn" },
        "saturation": { "value": "57", "unit": "%", "window": "5m", "status": "ok" }
      }
    },
    {
      "id": "otel-collector",
      "kind": "otel_collector",
      "ops_role": "collector",
      "x": 500,
      "y": 350,
      "width": 180,
      "height": 60,
      "label": "OTel Collector"
    },
    {
      "id": "span-root",
      "kind": "trace_span",
      "ops_role": "trace_span",
      "span_id": "span-root",
      "start_ms": 0,
      "duration_ms": 420,
      "x": 100,
      "y": 520,
      "width": 760,
      "height": 28,
      "label": "edge-gateway / checkout-request",
      "status": "warn"
    },
    {
      "id": "span-api",
      "kind": "trace_span",
      "ops_role": "trace_span",
      "span_id": "span-api",
      "parent_span": "span-root",
      "start_ms": 8,
      "duration_ms": 380,
      "x": 114.5,
      "y": 588,
      "width": 687.6,
      "height": 28,
      "label": "api-gateway / authorize",
      "status": "ok"
    },
    {
      "id": "span-checkout",
      "kind": "trace_span",
      "ops_role": "trace_span",
      "span_id": "span-checkout",
      "parent_span": "span-api",
      "start_ms": 42,
      "duration_ms": 286,
      "x": 176,
      "y": 656,
      "width": 517.5,
      "height": 28,
      "label": "checkout / commit-order",
      "status": "critical"
    },
    {
      "id": "span-payment",
      "kind": "trace_span",
      "ops_role": "trace_span",
      "span_id": "span-payment",
      "parent_span": "span-checkout",
      "start_ms": 85,
      "duration_ms": 94,
      "x": 253.8,
      "y": 724,
      "width": 170.1,
      "height": 28,
      "label": "payment / auth",
      "status": "warn"
    }
  ],
  "arrows": [
    { "id": "edge-gateway-api", "source": "edge-gateway", "target": "api-gateway", "source_port": "right", "target_port": "left", "flow": "control", "edge_kind": "business", "protocol": "HTTP/2", "motion_role": "critical-request", "motion_stage": 1, "motion_order": 0 },
    { "id": "edge-api-checkout", "source": "api-gateway", "target": "checkout-service", "source_port": "right", "target_port": "left", "flow": "control", "edge_kind": "business", "protocol": "gRPC", "motion_role": "critical-request", "motion_stage": 2, "motion_order": 0 },
    { "id": "edge-checkout-payment", "source": "checkout-service", "target": "payment-service", "source_port": "right", "target_port": "left", "flow": "control", "edge_kind": "business", "protocol": "gRPC", "motion_role": "critical-request", "motion_stage": 3, "motion_order": 0 },
    { "id": "export-telemetry", "source": "checkout-service", "target": "otel-collector", "source_port": "bottom", "target_port": "top", "flow": "async", "edge_kind": "telemetry", "motion_role": "telemetry-export", "motion_stage": 4, "motion_order": 0, "dashed": true }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 798,
  "legend_locked": true,
  "legend": [
    { "flow": "control", "label": "request" },
    { "flow": "async", "label": "telemetry" },
    { "flow": "feedback", "label": "degraded" }
  ],
  "footer": "Style 12 · Ops Pulse · exact golden signals · one critical path · correlated trace",
  "footer_x": 48,
  "footer_y": 848
}
skills/fireworks-tech-graph/fixtures/quality-baseline/agent-runtime-style3.json
{
  "schema_version": 1,
  "mode": "architecture",
  "template_type": "architecture",
  "style": 3,
  "quality_profile": "showcase",
  "width": 960,
  "height": 600,
  "title": "Agent Runtime Architecture",
  "subtitle": "orchestration, memory, tools, and evaluation",
  "containers": [
    { "id": "control-plane", "x": 40, "y": 120, "width": 880, "height": 116, "label": "Control Plane", "header_prefix": "01" },
    { "id": "execution-state", "x": 40, "y": 304, "width": 880, "height": 136, "label": "Execution and State", "header_prefix": "02" }
  ],
  "nodes": [
    { "id": "client", "kind": "rect", "x": 60, "y": 156, "width": 160, "height": 60, "label": "Client", "sublabel": "request + policy", "fill": "#0b3b5e", "stroke": "#94a3b8", "flat": true },
    { "id": "gateway", "kind": "double_rect", "x": 300, "y": 156, "width": 180, "height": 60, "label": "Gateway", "sublabel": "auth + routing", "fill": "#0b3b5e", "stroke": "#fde047" },
    { "id": "agent", "kind": "double_rect", "x": 560, "y": 156, "width": 180, "height": 60, "label": "Agent Runtime", "sublabel": "plan + inference", "fill": "#0b3b5e", "stroke": "#c084fc" },
    { "id": "memory", "kind": "rect", "x": 300, "y": 344, "width": 180, "height": 60, "label": "Vector Memory", "sublabel": "retrieve + persist", "fill": "#0b3b5e", "stroke": "#38bdf8", "flat": true },
    { "id": "tools", "kind": "rect", "x": 560, "y": 344, "width": 180, "height": 60, "label": "Tool Runtime", "sublabel": "MCP + functions", "fill": "#0b3b5e", "stroke": "#34d399", "flat": true },
    { "id": "eval", "kind": "rect", "x": 790, "y": 344, "width": 110, "height": 60, "label": "Trace + Eval", "sublabel": "quality gate", "fill": "#0b3b5e", "stroke": "#fb7185", "flat": true }
  ],
  "arrows": [
    { "id": "request", "source": "client", "target": "gateway", "source_port": "right", "target_port": "left", "flow": "control", "label": "request" },
    { "id": "dispatch", "source": "gateway", "target": "agent", "source_port": "right", "target_port": "left", "flow": "control", "label": "dispatch" },
    { "id": "context", "source": "agent", "target": "memory", "source_port": "bottom", "target_port": "top", "flow": "read", "label": "context", "route_points": [[632, 270], [390, 270]] },
    { "id": "tool-call", "source": "agent", "target": "tools", "source_port": "bottom", "target_port": "top", "flow": "async", "label": "tool call", "dashed": true },
    { "id": "augment", "source": "memory", "target": "tools", "source_port": "right", "target_port": "left", "flow": "write", "label": "augment" },
    { "id": "trace", "source": "tools", "target": "eval", "source_port": "right", "target_port": "left", "flow": "data", "label": "trace" },
    { "id": "z-feedback", "source": "eval", "target": "agent", "source_port": "top", "target_port": "bottom", "flow": "feedback", "label": "feedback", "dashed": true, "route_points": [[845, 270], [668, 270]] }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 510,
  "legend_locked": true,
  "legend": [
    { "flow": "control", "label": "primary" },
    { "flow": "write", "label": "data" },
    { "flow": "async", "label": "dependency" }
  ],
  "blueprint_title_block": {
    "title": "AGENT RUNTIME",
    "subtitle": "SYSTEM ARCHITECTURE",
    "center_caption": "STYLE 3",
    "left_caption": "REV: 1.1",
    "right_caption": "DWG: AGT-001",
    "width": 220,
    "height": 76,
    "x": 700,
    "y": 500
  },
  "footer": "Style 3 · Blueprint · showcase composition contract",
  "footer_x": 48,
  "footer_y": 570
}
skills/fireworks-tech-graph/fixtures/quality-baseline/agent-runtime-style12.json
{
  "schema_version": 1,
  "mode": "architecture",
  "template_type": "architecture",
  "style": 12,
  "semantic_profile": "generic",
  "quality_profile": "showcase",
  "width": 960,
  "height": 600,
  "title": "Agent Runtime Architecture",
  "subtitle": "orchestration, memory, tools, and evaluation",
  "containers": [
    { "id": "control-plane", "x": 40, "y": 120, "width": 880, "height": 116, "label": "Control Plane" },
    { "id": "execution-state", "x": 40, "y": 304, "width": 880, "height": 136, "label": "Execution and State" }
  ],
  "nodes": [
    { "id": "client", "kind": "rect", "x": 60, "y": 156, "width": 160, "height": 60, "label": "Client", "sublabel": "request + policy", "fill": "#f8fafc", "stroke": "#94a3b8", "flat": true },
    { "id": "gateway", "kind": "double_rect", "x": 300, "y": 156, "width": 180, "height": 60, "label": "Gateway", "sublabel": "auth + routing", "fill": "#fffbeb", "stroke": "#f59e0b", "flat": true },
    { "id": "agent", "kind": "double_rect", "x": 560, "y": 156, "width": 180, "height": 60, "label": "Agent Runtime", "sublabel": "plan + inference", "fill": "#faf5ff", "stroke": "#8b5cf6", "flat": true },
    { "id": "memory", "kind": "rect", "x": 300, "y": 344, "width": 180, "height": 60, "label": "Vector Memory", "sublabel": "retrieve + persist", "fill": "#f0f9ff", "stroke": "#38bdf8", "flat": true },
    { "id": "tools", "kind": "rect", "x": 560, "y": 344, "width": 180, "height": 60, "label": "Tool Runtime", "sublabel": "MCP + functions", "fill": "#f0fdf4", "stroke": "#22c55e", "flat": true },
    { "id": "eval", "kind": "rect", "x": 790, "y": 344, "width": 110, "height": 60, "label": "Trace + Eval", "sublabel": "quality gate", "fill": "#fff1f2", "stroke": "#fb7185", "flat": true }
  ],
  "arrows": [
    { "id": "request", "source": "client", "target": "gateway", "source_port": "right", "target_port": "left", "flow": "control", "label": "request" },
    { "id": "dispatch", "source": "gateway", "target": "agent", "source_port": "right", "target_port": "left", "flow": "control", "label": "dispatch" },
    { "id": "context", "source": "agent", "target": "memory", "source_port": "bottom", "target_port": "top", "flow": "read", "label": "context", "route_points": [[632, 270], [390, 270]] },
    { "id": "tool-call", "source": "agent", "target": "tools", "source_port": "bottom", "target_port": "top", "flow": "async", "label": "tool call", "dashed": true },
    { "id": "augment", "source": "memory", "target": "tools", "source_port": "right", "target_port": "left", "flow": "write", "label": "augment" },
    { "id": "trace", "source": "tools", "target": "eval", "source_port": "right", "target_port": "left", "flow": "data", "label": "trace" },
    { "id": "z-feedback", "source": "eval", "target": "agent", "source_port": "top", "target_port": "bottom", "flow": "feedback", "label": "feedback", "dashed": true, "route_points": [[845, 270], [668, 270]] }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 510,
  "legend_locked": true,
  "legend": [
    { "flow": "control", "label": "primary" },
    { "flow": "write", "label": "data" },
    { "flow": "async", "label": "dependency" }
  ],
  "footer": "Style 12 · Ops Pulse · showcase composition contract",
  "footer_x": 48,
  "footer_y": 570
}
skills/fireworks-tech-graph/fixtures/quality-baseline/agent-runtime-style11.json
{
  "schema_version": 1,
  "mode": "architecture",
  "template_type": "architecture",
  "style": 11,
  "semantic_profile": "generic",
  "quality_profile": "showcase",
  "width": 960,
  "height": 600,
  "title": "Agent Runtime Architecture",
  "subtitle": "orchestration, memory, tools, and evaluation",
  "containers": [
    { "id": "control-plane", "x": 40, "y": 120, "width": 880, "height": 116, "label": "Control Plane" },
    { "id": "execution-state", "x": 40, "y": 304, "width": 880, "height": 136, "label": "Execution and State" }
  ],
  "nodes": [
    { "id": "client", "kind": "rect", "x": 60, "y": 156, "width": 160, "height": 60, "label": "Client", "sublabel": "request + policy", "fill": "#f8fafc", "stroke": "#94a3b8", "flat": true },
    { "id": "gateway", "kind": "double_rect", "x": 300, "y": 156, "width": 180, "height": 60, "label": "Gateway", "sublabel": "auth + routing", "fill": "#fffbeb", "stroke": "#f59e0b", "flat": true },
    { "id": "agent", "kind": "double_rect", "x": 560, "y": 156, "width": 180, "height": 60, "label": "Agent Runtime", "sublabel": "plan + inference", "fill": "#faf5ff", "stroke": "#8b5cf6", "flat": true },
    { "id": "memory", "kind": "rect", "x": 300, "y": 344, "width": 180, "height": 60, "label": "Vector Memory", "sublabel": "retrieve + persist", "fill": "#f0f9ff", "stroke": "#38bdf8", "flat": true },
    { "id": "tools", "kind": "rect", "x": 560, "y": 344, "width": 180, "height": 60, "label": "Tool Runtime", "sublabel": "MCP + functions", "fill": "#f0fdf4", "stroke": "#22c55e", "flat": true },
    { "id": "eval", "kind": "rect", "x": 790, "y": 344, "width": 110, "height": 60, "label": "Trace + Eval", "sublabel": "quality gate", "fill": "#fff1f2", "stroke": "#fb7185", "flat": true }
  ],
  "arrows": [
    { "id": "request", "source": "client", "target": "gateway", "source_port": "right", "target_port": "left", "flow": "control", "label": "request" },
    { "id": "dispatch", "source": "gateway", "target": "agent", "source_port": "right", "target_port": "left", "flow": "control", "label": "dispatch" },
    { "id": "context", "source": "agent", "target": "memory", "source_port": "bottom", "target_port": "top", "flow": "read", "label": "context", "route_points": [[632, 270], [390, 270]] },
    { "id": "tool-call", "source": "agent", "target": "tools", "source_port": "bottom", "target_port": "top", "flow": "async", "label": "tool call", "dashed": true },
    { "id": "augment", "source": "memory", "target": "tools", "source_port": "right", "target_port": "left", "flow": "write", "label": "augment" },
    { "id": "trace", "source": "tools", "target": "eval", "source_port": "right", "target_port": "left", "flow": "data", "label": "trace" },
    { "id": "z-feedback", "source": "eval", "target": "agent", "source_port": "top", "target_port": "bottom", "flow": "feedback", "label": "feedback", "dashed": true, "route_points": [[845, 270], [668, 270]] }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 510,
  "legend_locked": true,
  "legend": [
    { "flow": "control", "label": "primary" },
    { "flow": "write", "label": "data" },
    { "flow": "async", "label": "dependency" }
  ],
  "footer": "Style 11 · Event Transit · showcase composition contract",
  "footer_x": 48,
  "footer_y": 570
}
skills/fireworks-tech-graph/fixtures/quality-baseline/agent-runtime-style4.json
{
  "schema_version": 1,
  "mode": "architecture",
  "template_type": "architecture",
  "style": 4,
  "quality_profile": "showcase",
  "width": 960,
  "height": 600,
  "title": "Agent Runtime Architecture",
  "subtitle": "orchestration, memory, tools, and evaluation",
  "containers": [
    { "id": "control-plane", "x": 40, "y": 120, "width": 880, "height": 116, "label": "Control Plane" },
    { "id": "execution-state", "x": 40, "y": 304, "width": 880, "height": 136, "label": "Execution and State" }
  ],
  "nodes": [
    { "id": "client", "kind": "rect", "x": 60, "y": 156, "width": 160, "height": 60, "label": "Client", "sublabel": "request + policy", "fill": "#f9fafb", "stroke": "#d1d5db", "flat": true },
    { "id": "gateway", "kind": "rect", "x": 300, "y": 156, "width": 180, "height": 60, "label": "Gateway", "sublabel": "auth + routing", "fill": "#f9fafb", "stroke": "#3b82f6", "flat": true },
    { "id": "agent", "kind": "rect", "x": 560, "y": 156, "width": 180, "height": 60, "label": "Agent Runtime", "sublabel": "plan + inference", "fill": "#eff6ff", "stroke": "#3b82f6", "flat": true },
    { "id": "memory", "kind": "rect", "x": 300, "y": 344, "width": 180, "height": 60, "label": "Vector Memory", "sublabel": "retrieve + persist", "fill": "#f9fafb", "stroke": "#d1d5db", "flat": true },
    { "id": "tools", "kind": "rect", "x": 560, "y": 344, "width": 180, "height": 60, "label": "Tool Runtime", "sublabel": "MCP + functions", "fill": "#f9fafb", "stroke": "#d1d5db", "flat": true },
    { "id": "eval", "kind": "rect", "x": 790, "y": 344, "width": 110, "height": 60, "label": "Trace + Eval", "sublabel": "quality gate", "fill": "#f9fafb", "stroke": "#d1d5db", "flat": true }
  ],
  "arrows": [
    { "id": "request", "source": "client", "target": "gateway", "source_port": "right", "target_port": "left", "flow": "control", "label": "request" },
    { "id": "dispatch", "source": "gateway", "target": "agent", "source_port": "right", "target_port": "left", "flow": "control", "label": "dispatch" },
    { "id": "context", "source": "agent", "target": "memory", "source_port": "bottom", "target_port": "top", "flow": "read", "label": "context", "route_points": [[632, 270], [390, 270]] },
    { "id": "tool-call", "source": "agent", "target": "tools", "source_port": "bottom", "target_port": "top", "flow": "async", "label": "tool call", "dashed": true },
    { "id": "augment", "source": "memory", "target": "tools", "source_port": "right", "target_port": "left", "flow": "write", "label": "augment" },
    { "id": "trace", "source": "tools", "target": "eval", "source_port": "right", "target_port": "left", "flow": "data", "label": "trace" },
    { "id": "z-feedback", "source": "eval", "target": "agent", "source_port": "top", "target_port": "bottom", "flow": "feedback", "label": "feedback", "dashed": true, "route_points": [[845, 270], [668, 270]] }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 510,
  "legend_locked": true,
  "legend": [
    { "flow": "control", "label": "primary" },
    { "flow": "write", "label": "data" },
    { "flow": "async", "label": "dependency" }
  ],
  "footer": "Style 4 · Notion Clean · showcase composition contract",
  "footer_x": 48,
  "footer_y": 570
}
skills/fireworks-tech-graph/fixtures/cloud-fabric-style10.json
{
  "schema_version": 1,
  "mode": "architecture",
  "template_type": "architecture",
  "style": 10,
  "semantic_profile": "cloud-fabric",
  "diagram_type": "deployment",
  "platform_profile": "aws",
  "deployment_mode": "ACTIVE–ACTIVE",
  "icon_manifest_version": "2026.07-neutral.1",
  "quality_profile": "showcase",
  "width": 960,
  "height": 760,
  "title": "Active–Active Checkout Deployment",
  "subtitle": "global ingress, explicit region/VPC ownership, and inter-region data flow",
  "containers": [
    {
      "id": "global-edge",
      "deployment_kind": "global",
      "x": 30,
      "y": 120,
      "width": 900,
      "height": 100,
      "label": "Global Edge"
    },
    {
      "id": "region-a",
      "deployment_kind": "region",
      "x": 30,
      "y": 260,
      "width": 420,
      "height": 350,
      "label": "us-east-1"
    },
    {
      "id": "region-b",
      "deployment_kind": "region",
      "x": 510,
      "y": 260,
      "width": 420,
      "height": 350,
      "label": "eu-west-1"
    },
    {
      "id": "vpc-a",
      "deployment_kind": "network",
      "parent": "region-a",
      "x": 55,
      "y": 310,
      "width": 370,
      "height": 260,
      "label": "VPC A"
    },
    {
      "id": "vpc-b",
      "deployment_kind": "network",
      "parent": "region-b",
      "x": 535,
      "y": 310,
      "width": 370,
      "height": 260,
      "label": "VPC B"
    }
  ],
  "nodes": [
    {
      "id": "edge-a",
      "kind": "cloud_service",
      "deployment_kind": "edge",
      "deployment_id": "global-edge",
      "icon_id": "generic:traffic",
      "provider": "AWS",
      "x": 182,
      "y": 142,
      "width": 176,
      "height": 56,
      "label": "NA Edge",
      "sublabel": "health routing"
    },
    {
      "id": "edge-b",
      "kind": "cloud_service",
      "deployment_kind": "edge",
      "deployment_id": "global-edge",
      "icon_id": "generic:traffic",
      "provider": "AWS",
      "x": 662,
      "y": 142,
      "width": 176,
      "height": 56,
      "label": "EU Edge",
      "sublabel": "health routing"
    },
    {
      "id": "app-a",
      "kind": "cloud_service",
      "deployment_kind": "compute",
      "deployment_id": "vpc-a",
      "icon_id": "generic:compute",
      "provider": "AWS",
      "x": 182,
      "y": 350,
      "width": 176,
      "height": 72,
      "label": "App A",
      "sublabel": "checkout service"
    },
    {
      "id": "db-a",
      "kind": "cloud_service",
      "deployment_kind": "database",
      "deployment_id": "vpc-a",
      "icon_id": "generic:database",
      "provider": "AWS",
      "x": 182,
      "y": 470,
      "width": 176,
      "height": 72,
      "label": "Orders A",
      "sublabel": "regional primary"
    },
    {
      "id": "app-b",
      "kind": "cloud_service",
      "deployment_kind": "compute",
      "deployment_id": "vpc-b",
      "icon_id": "generic:compute",
      "provider": "AWS",
      "x": 662,
      "y": 350,
      "width": 176,
      "height": 72,
      "label": "App B",
      "sublabel": "checkout service"
    },
    {
      "id": "db-b",
      "kind": "cloud_service",
      "deployment_kind": "database",
      "deployment_id": "vpc-b",
      "icon_id": "generic:database",
      "provider": "AWS",
      "x": 662,
      "y": 470,
      "width": 176,
      "height": 72,
      "label": "Orders B",
      "sublabel": "regional primary"
    }
  ],
  "arrows": [
    {
      "id": "route-a",
      "source": "edge-a",
      "target": "app-a",
      "source_port": "bottom",
      "target_port": "top",
      "flow": "read",
      "motion_role": "global-route",
      "motion_stage": 1,
      "motion_order": 0,
      "via": "Route 53 health policy",
      "label_dy": -34
    },
    {
      "id": "route-b",
      "source": "edge-b",
      "target": "app-b",
      "source_port": "bottom",
      "target_port": "top",
      "flow": "read",
      "motion_role": "global-route",
      "motion_stage": 1,
      "motion_order": 1,
      "via": "Route 53 health policy",
      "label_dy": -34
    },
    {
      "id": "write-a",
      "source": "app-a",
      "target": "db-a",
      "source_port": "bottom",
      "target_port": "top",
      "flow": "write",
      "motion_role": "regional-write",
      "motion_stage": 2,
      "motion_order": 0
    },
    {
      "id": "write-b",
      "source": "app-b",
      "target": "db-b",
      "source_port": "bottom",
      "target_port": "top",
      "flow": "write",
      "motion_role": "regional-write",
      "motion_stage": 2,
      "motion_order": 1
    },
    {
      "id": "replicate-orders",
      "source": "db-a",
      "target": "db-b",
      "source_port": "right",
      "target_port": "left",
      "flow": "async",
      "motion_role": "cross-region",
      "motion_stage": 3,
      "motion_order": 0,
      "via": "inter-region peering",
      "dashed": true
    }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 660,
  "legend_locked": true,
  "legend": [
    { "flow": "read", "label": "global routing" },
    { "flow": "write", "label": "regional write" },
    { "flow": "async", "label": "cross-region replication" }
  ],
  "footer": "Style 10 · Cloud Fabric · provider-neutral glyphs · explicit deployment ownership",
  "footer_x": 48,
  "footer_y": 735
}
skills/fireworks-tech-graph/fixtures/quality-baseline/agent-runtime-style9.json
{
  "schema_version": 1,
  "mode": "architecture",
  "template_type": "architecture",
  "style": 9,
  "semantic_profile": "generic",
  "quality_profile": "showcase",
  "width": 960,
  "height": 600,
  "title": "Agent Runtime Architecture",
  "subtitle": "orchestration, memory, tools, and evaluation",
  "containers": [
    { "id": "control-plane", "x": 40, "y": 120, "width": 880, "height": 116, "label": "Control Plane" },
    { "id": "execution-state", "x": 40, "y": 304, "width": 880, "height": 136, "label": "Execution and State" }
  ],
  "nodes": [
    { "id": "client", "kind": "rect", "x": 60, "y": 156, "width": 160, "height": 60, "label": "Client", "sublabel": "request + policy", "fill": "#f8fafc", "stroke": "#94a3b8", "flat": true },
    { "id": "gateway", "kind": "double_rect", "x": 300, "y": 156, "width": 180, "height": 60, "label": "Gateway", "sublabel": "auth + routing", "fill": "#fffbeb", "stroke": "#f59e0b", "flat": true },
    { "id": "agent", "kind": "double_rect", "x": 560, "y": 156, "width": 180, "height": 60, "label": "Agent Runtime", "sublabel": "plan + inference", "fill": "#faf5ff", "stroke": "#8b5cf6", "flat": true },
    { "id": "memory", "kind": "rect", "x": 300, "y": 344, "width": 180, "height": 60, "label": "Vector Memory", "sublabel": "retrieve + persist", "fill": "#f0f9ff", "stroke": "#38bdf8", "flat": true },
    { "id": "tools", "kind": "rect", "x": 560, "y": 344, "width": 180, "height": 60, "label": "Tool Runtime", "sublabel": "MCP + functions", "fill": "#f0fdf4", "stroke": "#22c55e", "flat": true },
    { "id": "eval", "kind": "rect", "x": 790, "y": 344, "width": 110, "height": 60, "label": "Trace + Eval", "sublabel": "quality gate", "fill": "#fff1f2", "stroke": "#fb7185", "flat": true }
  ],
  "arrows": [
    { "id": "request", "source": "client", "target": "gateway", "source_port": "right", "target_port": "left", "flow": "control", "label": "request" },
    { "id": "dispatch", "source": "gateway", "target": "agent", "source_port": "right", "target_port": "left", "flow": "control", "label": "dispatch" },
    { "id": "context", "source": "agent", "target": "memory", "source_port": "bottom", "target_port": "top", "flow": "read", "label": "context", "route_points": [[632, 270], [390, 270]] },
    { "id": "tool-call", "source": "agent", "target": "tools", "source_port": "bottom", "target_port": "top", "flow": "async", "label": "tool call", "dashed": true },
    { "id": "augment", "source": "memory", "target": "tools", "source_port": "right", "target_port": "left", "flow": "write", "label": "augment" },
    { "id": "trace", "source": "tools", "target": "eval", "source_port": "right", "target_port": "left", "flow": "data", "label": "trace" },
    { "id": "z-feedback", "source": "eval", "target": "agent", "source_port": "top", "target_port": "bottom", "flow": "feedback", "label": "feedback", "dashed": true, "route_points": [[845, 270], [668, 270]] }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 510,
  "legend_locked": true,
  "legend": [
    { "flow": "control", "label": "primary" },
    { "flow": "write", "label": "data" },
    { "flow": "async", "label": "dependency" }
  ],
  "footer": "Style 9 · C4 Review Canvas · showcase composition contract",
  "footer_x": 48,
  "footer_y": 570
}
skills/fireworks-tech-graph/fixtures/quality-baseline/agent-runtime-style6.json
{
  "schema_version": 1,
  "mode": "architecture",
  "template_type": "architecture",
  "style": 6,
  "quality_profile": "showcase",
  "width": 960,
  "height": 600,
  "title": "Agent Runtime Architecture",
  "subtitle": "orchestration, memory, tools, and evaluation",
  "containers": [
    { "id": "control-plane", "x": 40, "y": 120, "width": 880, "height": 116, "label": "Control Plane" },
    { "id": "execution-state", "x": 40, "y": 304, "width": 880, "height": 136, "label": "Execution and State" }
  ],
  "nodes": [
    { "id": "client", "kind": "rect", "x": 60, "y": 156, "width": 160, "height": 60, "label": "Client", "sublabel": "request + policy", "fill": "#efe8de", "stroke": "#a29a8f", "flat": true },
    { "id": "gateway", "kind": "double_rect", "x": 300, "y": 156, "width": 180, "height": 60, "label": "Gateway", "sublabel": "auth + routing", "fill": "#f7ecda", "stroke": "#d97757" },
    { "id": "agent", "kind": "double_rect", "x": 560, "y": 156, "width": 180, "height": 60, "label": "Agent Runtime", "sublabel": "plan + inference", "fill": "#c8e4db", "stroke": "#8c6f5a" },
    { "id": "memory", "kind": "rect", "x": 300, "y": 344, "width": 180, "height": 60, "label": "Vector Memory", "sublabel": "retrieve + persist", "fill": "#e5e8df", "stroke": "#7b8b5c", "flat": true },
    { "id": "tools", "kind": "rect", "x": 560, "y": 344, "width": 180, "height": 60, "label": "Tool Runtime", "sublabel": "MCP + functions", "fill": "#f7ecda", "stroke": "#d0b893", "flat": true },
    { "id": "eval", "kind": "rect", "x": 790, "y": 344, "width": 110, "height": 60, "label": "Trace + Eval", "sublabel": "quality gate", "fill": "#f3e3dc", "stroke": "#d97757", "flat": true }
  ],
  "arrows": [
    { "id": "request", "source": "client", "target": "gateway", "source_port": "right", "target_port": "left", "flow": "control", "label": "request" },
    { "id": "dispatch", "source": "gateway", "target": "agent", "source_port": "right", "target_port": "left", "flow": "control", "label": "dispatch" },
    { "id": "context", "source": "agent", "target": "memory", "source_port": "bottom", "target_port": "top", "flow": "read", "label": "context", "route_points": [[632, 270], [390, 270]] },
    { "id": "tool-call", "source": "agent", "target": "tools", "source_port": "bottom", "target_port": "top", "flow": "async", "label": "tool call", "dashed": true },
    { "id": "augment", "source": "memory", "target": "tools", "source_port": "right", "target_port": "left", "flow": "write", "label": "augment" },
    { "id": "trace", "source": "tools", "target": "eval", "source_port": "right", "target_port": "left", "flow": "data", "label": "trace" },
    { "id": "z-feedback", "source": "eval", "target": "agent", "source_port": "top", "target_port": "bottom", "flow": "feedback", "label": "feedback", "dashed": true, "route_points": [[845, 270], [668, 270]] }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 510,
  "legend_locked": true,
  "legend": [
    { "flow": "control", "label": "primary" },
    { "flow": "write", "label": "data" },
    { "flow": "async", "label": "dependency" }
  ],
  "footer": "Style 6 · Claude Official · showcase composition contract",
  "footer_x": 48,
  "footer_y": 570
}
skills/fireworks-tech-graph/examples/interactive-architecture.html
<!doctype html>
<html lang="en" data-theme="dark">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; img-src data: blob:">
  <title>API Integration Flow</title>
  <style>
    :root { color-scheme: dark; --page:#0f0f1a; --panel:#0f172a; --border:#334155; --text:#e2e8f0; --muted:#94a3b8; --accent:#a855f7; }
    html[data-theme="light"] { color-scheme:light; --page:#f8fafc; --panel:#fff; --border:#cbd5e1; --text:#0f172a; --muted:#475569; --accent:#7c3aed; }
    * { box-sizing:border-box; }
    html,body { width:100%; height:100%; margin:0; overflow:hidden; background:linear-gradient(135deg,var(--page),#1a1a2e); color:var(--text); font-family:'SF Mono','Fira Code',ui-monospace,monospace; }
    body { display:grid; grid-template-rows:auto 1fr; }
    .toolbar { display:flex; flex-wrap:wrap; align-items:center; gap:8px; padding:10px 14px; background:color-mix(in srgb,var(--panel) 92%,transparent); border-bottom:1px solid var(--border); z-index:2; }
    .title { margin-right:auto; font-size:13px; font-weight:700; }
    button,select { min-height:34px; padding:6px 10px; color:var(--text); background:var(--panel); border:1px solid var(--border); border-radius:8px; font:inherit; cursor:pointer; }
    button:hover,button:focus-visible,select:focus-visible { border-color:var(--accent); outline:2px solid color-mix(in srgb,var(--accent) 35%,transparent); outline-offset:1px; }
    #stage { position:relative; overflow:hidden; touch-action:none; cursor:grab; }
    #stage.dragging { cursor:grabbing; }
    #canvas { width:100%; height:100%; display:grid; place-items:center; transform-origin:0 0; will-change:transform; }
    #canvas svg { max-width:calc(100vw - 40px); max-height:calc(100vh - 92px); width:auto; height:auto; filter:drop-shadow(0 22px 60px rgba(0,0,0,.28)); user-select:none; }
    #status { min-width:76px; color:var(--muted); font-size:12px; text-align:center; }
    .sr-only { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; }
    @media (prefers-reduced-motion:reduce) { * { scroll-behavior:auto!important; transition:none!important; } }
  </style>
</head>
<body>
  <header class="toolbar" aria-label="Diagram controls">
    <span class="title">API Integration Flow</span>
    <button type="button" data-action="zoom-out" aria-label="Zoom out">−</button>
    <button type="button" data-action="reset" aria-label="Reset view">Reset</button>
    <button type="button" data-action="zoom-in" aria-label="Zoom in">+</button>
    <span id="status" aria-live="polite">100%</span>
    <button type="button" data-action="theme" aria-label="Toggle theme">Theme</button>
    <button type="button" data-action="copy" aria-label="Copy SVG source">Copy SVG</button>
    <select id="scale" aria-label="Raster export scale"><option value="1">1×</option><option value="2" selected>2×</option><option value="3">3×</option><option value="4">4×</option></select>
    <select id="format" aria-label="Export format"><option>SVG</option><option>PNG</option><option>JPEG</option><option>WebP</option></select>
    <button type="button" data-action="download">Export</button>
  </header>
  <main id="stage" tabindex="0" aria-label="Interactive diagram. Drag to pan; use plus and minus to zoom.">
    <div id="canvas"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 700" width="960" height="700" data-generator="fireworks-tech-graph" data-schema-version="1" data-text-metrics="heuristic-v1" data-style-id="7" data-visual-theme="OpenAI" data-diagram-type="architecture" data-motion-scene="token-stream" data-semantic-profile="generic" data-semantic-valid="true" data-quality-profile="showcase" data-max-bends-per-edge="2" data-max-total-bends="8" data-max-route-stretch="1.35" data-max-bridged-crossings="0" data-min-node-gap="40.0" data-min-container-gutter="20.0" data-min-label-clearance="4.0" data-min-segment-length="16.0" role="img" focusable="false">
  <defs>
    <marker id="arrowA" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
      <polygon points="0 0, 10 3.5, 0 7" fill="#10a37f" />
    </marker>
    <marker id="arrowB" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
      <polygon points="0 0, 10 3.5, 0 7" fill="#0f766e" />
    </marker>
    <marker id="arrowC" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
      <polygon points="0 0, 10 3.5, 0 7" fill="#0891b2" />
    </marker>
    <marker id="arrowE" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
      <polygon points="0 0, 10 3.5, 0 7" fill="#f59e0b" />
    </marker>
    <marker id="arrowF" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
      <polygon points="0 0, 10 3.5, 0 7" fill="#64748b" />
    </marker>
    <marker id="arrowG" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
      <polygon points="0 0, 10 3.5, 0 7" fill="#475569" />
    </marker>
    <marker id="arrowH" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
      <polygon points="0 0, 10 3.5, 0 7" fill="#94a3b8" />
    </marker>
    <style>
    text { font-family: 'Helvetica Neue', Helvetica, Arial, 'PingFang SC', 'Microsoft YaHei', 'Microsoft JhengHei', 'SimHei', sans-serif; }
    .title { font-size: 24px; font-weight: 700; fill: #0f172a; }
    .subtitle { font-size: 13px; font-weight: 500; fill: #64748b; }
    .section { font-size: 13px; font-weight: 700; fill: #10a37f; letter-spacing: 1.4px; }
    .section-sub { font-size: 12px; font-weight: 500; fill: #94a3b8; }
    .node-title { font-weight: 700; fill: #0f172a; }
    .node-sub { font-size: 12px; font-weight: 500; fill: #475569; }
    .node-type { font-size: 11px; font-weight: 700; fill: #94a3b8; letter-spacing: 0.08em; }
    .arrow-label { font-size: 12px; font-weight: 600; fill: #475569; }
    .legend { font-size: 12px; font-weight: 500; fill: #475569; }
    .footnote { font-size: 12px; font-weight: 500; fill: #94a3b8; }
    .metric-label { font-size: 8.5px; font-weight: 700; fill: #94a3b8; text-transform: uppercase; }
    .metric-value { font-size: 9.5px; font-weight: 700; fill: #0f172a; }
    </style>
  </defs>
  <rect data-graph-role="background" width="960.0" height="700.0" fill="#ffffff" />
  <text x="48.0" y="48" text-anchor="start" class="title">API Integration Flow</text>
  <text x="48.0" y="72" text-anchor="start" class="subtitle">clean application integration from SDK input to governed delivery</text>
  <line x1="48" y1="100" x2="912.0" y2="100" stroke="#e2e8f0" stroke-width="1" />
  <g id="entry" data-graph-role="container" data-container-id="entry" data-semantic-role="boundary" data-graph-bounds="40,120,920,230">
  <rect data-graph-role="container" x="40.0" y="120.0" width="880.0" height="110.0" rx="16.0" fill="none" stroke="#e2e8f0" stroke-width="1.4" stroke-dasharray="5 4" />
  <text x="58.0" y="144.0" class="section">INTEGRATION</text>
    <rect id="entry-header" data-graph-role="reserved" data-reserved-kind="container-header" x="48" y="126" width="106.18" height="30" fill="none" stroke="none" />
  </g>
  <g id="runtime" data-graph-role="container" data-container-id="runtime" data-semantic-role="boundary" data-graph-bounds="40,280,920,410">
  <rect data-graph-role="container" x="40.0" y="280.0" width="880.0" height="130.0" rx="16.0" fill="none" stroke="#e2e8f0" stroke-width="1.4" stroke-dasharray="5 4" />
  <text x="58.0" y="304.0" class="section">MODEL + TOOLS</text>
    <rect id="runtime-header" data-graph-role="reserved" data-reserved-kind="container-header" x="48" y="286" width="125.42" height="30" fill="none" stroke="none" />
  </g>
  <g id="delivery" data-graph-role="container" data-container-id="delivery" data-semantic-role="boundary" data-graph-bounds="40,450,920,590">
  <rect data-graph-role="container" x="40.0" y="450.0" width="880.0" height="140.0" rx="16.0" fill="none" stroke="#e2e8f0" stroke-width="1.4" stroke-dasharray="5 4" />
  <text x="58.0" y="474.0" class="section">DELIVERY</text>
    <rect id="delivery-header" data-graph-role="reserved" data-reserved-kind="container-header" x="48" y="456" width="86.94" height="30" fill="none" stroke="none" />
  </g>
  <path id="connect" data-graph-role="edge" data-edge-id="connect" data-source="app" data-target="sdk" data-edge-kind="flow" data-topic-id="" data-flow="control" data-motion-role="connect" data-motion-stage="1" data-motion-order="0" data-protocol="" data-via="" data-critical-path-id="" data-critical-hop="" data-critical-hops="" data-critical="false" data-bends="0" data-route-stretch="1.0" data-bridges="" d="M 260,183 L 370,183" fill="none" stroke="#10a37f" stroke-width="2.0" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrowA)" />
  <path id="prepare" data-graph-role="edge" data-edge-id="prepare" data-source="sdk" data-target="prompt" data-edge-kind="flow" data-topic-id="" data-flow="read" data-motion-role="prepare" data-motion-stage="2" data-motion-order="0" data-protocol="" data-via="" data-critical-path-id="" data-critical-hop="" data-critical-hops="" data-critical="false" data-bends="2" data-route-stretch="1.0" data-bridges="" d="M 480,210 L 480,316 L 170,316 L 170,340" fill="none" stroke="#0891b2" stroke-width="2.0" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrowC)" />
  <path id="invoke" data-graph-role="edge" data-edge-id="invoke" data-source="prompt" data-target="model" data-edge-kind="flow" data-topic-id="" data-flow="control" data-motion-role="invoke" data-motion-stage="3" data-motion-order="0" data-protocol="" data-via="" data-critical-path-id="" data-critical-hop="" data-critical-hops="" data-critical="false" data-bends="0" data-route-stretch="1.0" data-bridges="" d="M 260,365 L 370,365" fill="none" stroke="#10a37f" stroke-width="2.0" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrowA)" />
  <path id="tool-call" data-graph-role="edge" data-edge-id="tool-call" data-source="model" data-target="tools" data-edge-kind="flow" data-topic-id="" data-flow="read" data-motion-role="tool-call" data-motion-stage="4" data-motion-order="0" data-protocol="" data-via="" data-critical-path-id="" data-critical-hop="" data-critical-hops="" data-critical="false" data-bends="0" data-route-stretch="1.0" data-bridges="" d="M 590,365 L 700,365" fill="none" stroke="#0891b2" stroke-width="2.0" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrowC)" />
  <path id="stream" data-graph-role="edge" data-edge-id="stream" data-source="model" data-target="formatter" data-edge-kind="flow" data-topic-id="" data-flow="control" data-motion-role="token-stream" data-motion-stage="4" data-motion-order="1" data-protocol="" data-via="" data-critical-path-id="" data-critical-hop="" data-critical-hops="" data-critical="false" data-bends="2" data-route-stretch="1.0" data-bridges="" d="M 480,390 L 480,414 L 170,414 L 170,510" fill="none" stroke="#10a37f" stroke-width="2.0" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrowA)" />
  <path id="measure" data-graph-role="edge" data-edge-id="measure" data-source="formatter" data-target="observability" data-edge-kind="flow" data-topic-id="" data-flow="feedback" data-motion-role="measure" data-motion-stage="5" data-motion-order="1" data-protocol="" data-via="" data-critical-path-id="" data-critical-hop="" data-critical-hops="" data-critical="false" data-bends="0" data-route-stretch="1.0" data-bridges="" d="M 260,540 L 390,540" fill="none" stroke="#475569" stroke-width="2.0" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrowG)" />
  <path id="govern" data-graph-role="edge" data-edge-id="govern" data-source="tools" data-target="release" data-edge-kind="flow" data-topic-id="" data-flow="feedback" data-motion-role="govern" data-motion-stage="5" data-motion-order="0" data-protocol="" data-via="" data-critical-path-id="" data-critical-hop="" data-critical-hops="" data-critical="false" data-bends="0" data-route-stretch="1.0" data-bridges="" d="M 790,390 L 790,510" fill="none" stroke="#475569" stroke-width="2.0" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrowG)" />
  <path id="promote" data-graph-role="edge" data-edge-id="promote" data-source="observability" data-target="release" data-edge-kind="flow" data-topic-id="" data-flow="feedback" data-motion-role="promote" data-motion-stage="6" data-motion-order="0" data-protocol="" data-via="" data-critical-path-id="" data-critical-hop="" data-critical-hops="" data-critical="false" data-bends="0" data-route-stretch="1.0" data-bridges="" d="M 570,540 L 700,540" fill="none" stroke="#475569" stroke-width="2.0" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrowG)" />
  <g id="node-app" data-graph-role="node" data-node-id="app" data-semantic-role="rect" data-motion-role="" data-motion-stage="" data-motion-order="" data-parent="" data-deployment-id="" data-topic-id="" data-span-id="" data-station-order="" data-status="" data-start-ms="" data-duration-ms="" data-parent-span="" data-graph-bounds="80,156,260,210">
  <rect x="80.0" y="156.0" width="180.0" height="54.0" rx="14.0" fill="#ffffff" stroke="#dce5e3" stroke-width="1.8" />
  <text x="170.0" y="174.0" text-anchor="middle" class="node-type">CLIENT</text>
  <text x="170.0" y="189.0" text-anchor="middle" class="node-title" font-size="18.0">Application</text>
  </g>
  <g id="node-sdk" data-graph-role="node" data-node-id="sdk" data-semantic-role="double_rect" data-motion-role="" data-motion-stage="" data-motion-order="" data-parent="" data-deployment-id="" data-topic-id="" data-span-id="" data-station-order="" data-status="" data-start-ms="" data-duration-ms="" data-parent-span="" data-graph-bounds="370,156,590,210">
  <rect x="370.0" y="156.0" width="220.0" height="54.0" rx="14.0" fill="#ffffff" stroke="#10a37f" stroke-width="2.0" />
  <rect x="376.0" y="162.0" width="208.0" height="42.0" rx="11.0" fill="none" stroke="#10a37f" stroke-width="1.2" opacity="0.65" />
  <text x="480.0" y="174.0" text-anchor="middle" class="node-type">SDK</text>
  <text x="480.0" y="189.0" text-anchor="middle" class="node-title" font-size="18.0">OpenAI SDK Layer</text>
  </g>
  <g id="node-prompt" data-graph-role="node" data-node-id="prompt" data-semantic-role="rect" data-motion-role="" data-motion-stage="" data-motion-order="" data-parent="" data-deployment-id="" data-topic-id="" data-span-id="" data-station-order="" data-status="" data-start-ms="" data-duration-ms="" data-parent-span="" data-graph-bounds="80,340,260,390">
  <rect x="80.0" y="340.0" width="180.0" height="50.0" rx="14.0" fill="#ffffff" stroke="#dce5e3" stroke-width="1.8" />
  <text x="170.0" y="358.0" text-anchor="middle" class="node-type">INPUT</text>
  <text x="170.0" y="371.0" text-anchor="middle" class="node-title" font-size="18.0">Prompt Builder</text>
  </g>
  <g id="node-model" data-graph-role="node" data-node-id="model" data-semantic-role="double_rect" data-motion-role="" data-motion-stage="" data-motion-order="" data-parent="" data-deployment-id="" data-topic-id="" data-span-id="" data-station-order="" data-status="" data-start-ms="" data-duration-ms="" data-parent-span="" data-graph-bounds="370,340,590,390">
  <rect x="370.0" y="340.0" width="220.0" height="50.0" rx="14.0" fill="#ffffff" stroke="#10a37f" stroke-width="2.0" />
  <rect x="376.0" y="346.0" width="208.0" height="38.0" rx="11.0" fill="none" stroke="#10a37f" stroke-width="1.2" opacity="0.65" />
  <text x="480.0" y="358.0" text-anchor="middle" class="node-type">REASONING</text>
  <text x="480.0" y="371.0" text-anchor="middle" class="node-title" font-size="18.0">Model Runtime</text>
  </g>
  <g id="node-tools" data-graph-role="node" data-node-id="tools" data-semantic-role="rect" data-motion-role="" data-motion-stage="" data-motion-order="" data-parent="" data-deployment-id="" data-topic-id="" data-span-id="" data-station-order="" data-status="" data-start-ms="" data-duration-ms="" data-parent-span="" data-graph-bounds="700,340,880,390">
  <rect x="700.0" y="340.0" width="180.0" height="50.0" rx="14.0" fill="#ffffff" stroke="#dce5e3" stroke-width="1.8" />
  <text x="790.0" y="358.0" text-anchor="middle" class="node-type">ACTIONS</text>
  <text x="790.0" y="371.0" text-anchor="middle" class="node-title" font-size="18.0">Tool Calls</text>
  </g>
  <g id="node-formatter" data-graph-role="node" data-node-id="formatter" data-semantic-role="rect" data-motion-role="" data-motion-stage="" data-motion-order="" data-parent="" data-deployment-id="" data-topic-id="" data-span-id="" data-station-order="" data-status="" data-start-ms="" data-duration-ms="" data-parent-span="" data-graph-bounds="80,510,260,570">
  <rect x="80.0" y="510.0" width="180.0" height="60.0" rx="14.0" fill="#ffffff" stroke="#dce5e3" stroke-width="1.8" />
  <text x="170.0" y="528.0" text-anchor="middle" class="node-type">OUTPUT</text>
  <text x="170.0" y="546.0" text-anchor="middle" class="node-title" font-size="14.13">Response Formatter</text>
  </g>
  <g id="node-observability" data-graph-role="node" data-node-id="observability" data-semantic-role="rect" data-motion-role="" data-motion-stage="" data-motion-order="" data-parent="" data-deployment-id="" data-topic-id="" data-span-id="" data-station-order="" data-status="" data-start-ms="" data-duration-ms="" data-parent-span="" data-graph-bounds="390,510,570,570">
  <rect x="390.0" y="510.0" width="180.0" height="60.0" rx="14.0" fill="#ffffff" stroke="#dce5e3" stroke-width="1.8" />
  <text x="480.0" y="528.0" text-anchor="middle" class="node-type">METRICS</text>
  <text x="480.0" y="546.0" text-anchor="middle" class="node-title" font-size="18.0">Observability</text>
  </g>
  <g id="node-release" data-graph-role="node" data-node-id="release" data-semantic-role="rect" data-motion-role="" data-motion-stage="" data-motion-order="" data-parent="" data-deployment-id="" data-topic-id="" data-span-id="" data-station-order="" data-status="" data-start-ms="" data-duration-ms="" data-parent-span="" data-graph-bounds="700,510,880,570">
  <rect x="700.0" y="510.0" width="180.0" height="60.0" rx="14.0" fill="#ffffff" stroke="#dce5e3" stroke-width="1.8" />
  <text x="790.0" y="528.0" text-anchor="middle" class="node-type">CONFIG</text>
  <text x="790.0" y="546.0" text-anchor="middle" class="node-title" font-size="18.0">Release Control</text>
  </g>
  <g id="connect-label" data-graph-role="label" data-owner="connect" data-graph-bounds="283.64,152,346.36,172">
  <rect x="283.64" y="152.0" width="62.72" height="20" rx="6" fill="#ffffff" opacity="0.96" />
  <text x="315.0" y="166.0" text-anchor="middle" class="arrow-label">connect</text>
  </g>
  <g id="prepare-label" data-graph-role="label" data-owner="prepare" data-graph-bounds="293.64,285,356.36,305">
  <rect x="293.64" y="285.0" width="62.72" height="20" rx="6" fill="#ffffff" opacity="0.96" />
  <text x="325.0" y="299.0" text-anchor="middle" class="arrow-label">prepare</text>
  </g>
  <g id="invoke-label" data-graph-role="label" data-owner="invoke" data-graph-bounds="288.68,334,341.32,354">
  <rect x="288.68" y="334.0" width="52.64" height="20" rx="6" fill="#ffffff" opacity="0.96" />
  <text x="315.0" y="348.0" text-anchor="middle" class="arrow-label">invoke</text>
  </g>
  <g id="tool-call-label" data-graph-role="label" data-owner="tool-call" data-graph-bounds="612.68,334,677.32,354">
  <rect x="612.68" y="334.0" width="64.64" height="20" rx="6" fill="#ffffff" opacity="0.96" />
  <text x="645.0" y="348.0" text-anchor="middle" class="arrow-label">tool call</text>
  </g>
  <g id="stream-label" data-graph-role="label" data-owner="stream" data-graph-bounds="297.12,383,352.88,403">
  <rect x="297.12" y="383.0" width="55.76" height="20" rx="6" fill="#ffffff" opacity="0.96" />
  <text x="325.0" y="397.0" text-anchor="middle" class="arrow-label">stream</text>
  </g>
  <g id="measure-label" data-graph-role="label" data-owner="measure" data-graph-bounds="293.64,509,356.36,529">
  <rect x="293.64" y="509.0" width="62.72" height="20" rx="6" fill="#ffffff" opacity="0.96" />
  <text x="325.0" y="523.0" text-anchor="middle" class="arrow-label">measure</text>
  </g>
  <g id="govern-label" data-graph-role="label" data-owner="govern" data-graph-bounds="731.24,436,787,456">
  <rect x="731.24" y="436.0" width="55.76" height="20" rx="6" fill="#ffffff" opacity="0.96" />
  <text x="759.12" y="450.0" text-anchor="middle" class="arrow-label">govern</text>
  </g>
  <g id="promote-label" data-graph-role="label" data-owner="promote" data-graph-bounds="603.64,509,666.36,529">
  <rect x="603.64" y="509.0" width="62.72" height="20" rx="6" fill="#ffffff" opacity="0.96" />
  <text x="635.0" y="523.0" text-anchor="middle" class="arrow-label">promote</text>
  </g>
  <g id="legend" data-graph-role="legend">
    <rect id="legend-zone" data-graph-role="reserved" data-reserved-kind="legend" x="38" y="611" width="464.48" height="46" rx="10" fill="none" stroke="none" />
    <line data-graph-role="decoration" x1="48.0" y1="625.0" x2="78.0" y2="625.0" stroke="#10a37f" stroke-width="2.0" marker-end="url(#arrowA)" />
    <text data-graph-role="decoration" x="88.0" y="629.0" class="legend">primary API path</text>
    <line data-graph-role="decoration" x1="215.84" y1="625.0" x2="245.84" y2="625.0" stroke="#0891b2" stroke-width="2.0" marker-end="url(#arrowC)" />
    <text data-graph-role="decoration" x="255.84" y="629.0" class="legend">prompt / tools</text>
    <line data-graph-role="decoration" x1="372.88" y1="625.0" x2="402.88" y2="625.0" stroke="#475569" stroke-width="2.0" marker-end="url(#arrowG)" />
    <text data-graph-role="decoration" x="412.88" y="629.0" class="legend">governance</text>
  </g>
  <g id="footer" data-graph-role="reserved" data-graph-bounds="48,668,426,684">
  <text x="48.0" y="680.0" class="footnote">Style 7 · OpenAI Official · precise integration stages</text>
  </g>
</svg></div>
    <p class="sr-only">Keyboard shortcuts: plus and minus zoom, zero resets, T toggles theme, S exports.</p>
  </main>
  <script>
  (() => {
    'use strict';
    const metadata = {"slug": "api-integration-flow"};
    const stage = document.getElementById('stage');
    const canvas = document.getElementById('canvas');
    const svg = canvas.querySelector('svg');
    const status = document.getElementById('status');
    const scaleSelect = document.getElementById('scale');
    const formatSelect = document.getElementById('format');
    let view = { x:0, y:0, scale:1 };
    let drag = null;
    const clamp = value => Math.max(.2, Math.min(8, value));
    const render = () => {
      canvas.style.transform = `translate(${view.x}px,${view.y}px) scale(${view.scale})`;
      status.textContent = `${Math.round(view.scale * 100)}%`;
    };
    const zoom = (factor, originX=stage.clientWidth/2, originY=stage.clientHeight/2) => {
      const next = clamp(view.scale * factor);
      const ratio = next / view.scale;
      view.x = originX - (originX - view.x) * ratio;
      view.y = originY - (originY - view.y) * ratio;
      view.scale = next; render();
    };
    const reset = () => { view = {x:0,y:0,scale:1}; render(); };
    stage.addEventListener('wheel', event => { event.preventDefault(); const rect=stage.getBoundingClientRect(); zoom(event.deltaY < 0 ? 1.12 : .89, event.clientX-rect.left, event.clientY-rect.top); }, {passive:false});
    stage.addEventListener('pointerdown', event => { drag={id:event.pointerId,x:event.clientX,y:event.clientY,vx:view.x,vy:view.y}; stage.setPointerCapture(event.pointerId); stage.classList.add('dragging'); });
    stage.addEventListener('pointermove', event => { if(!drag||drag.id!==event.pointerId)return; view.x=drag.vx+event.clientX-drag.x; view.y=drag.vy+event.clientY-drag.y; render(); });
    const endDrag = () => { drag=null; stage.classList.remove('dragging'); };
    stage.addEventListener('pointerup', endDrag); stage.addEventListener('pointercancel', endDrag);
    const source = () => new XMLSerializer().serializeToString(svg);
    const saveBlob = (blob, extension) => { const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download=`${(metadata.slug||'fireworks-tech-graph')}.${extension}`; a.click(); setTimeout(()=>URL.revokeObjectURL(a.href),1000); };
    const copySource = async () => {
      const value=source();
      try { await navigator.clipboard.writeText(value); status.textContent='Copied'; return; } catch {}
      const area=document.createElement('textarea'); area.value=value; area.setAttribute('readonly',''); area.style.position='fixed'; area.style.opacity='0'; document.body.appendChild(area); area.select();
      status.textContent=document.execCommand('copy')?'Copied':'Copy failed'; area.remove();
    };
    const exportDiagram = async () => {
      const format = formatSelect.value.toLowerCase();
      if(format==='svg') { saveBlob(new Blob([source()],{type:'image/svg+xml;charset=utf-8'}),'svg'); return; }
      const scale = Math.max(1,Math.min(4,Number(scaleSelect.value)||2));
      const box = svg.viewBox.baseVal; const width=box.width||svg.clientWidth; const height=box.height||svg.clientHeight;
      const image = new Image(); const url=URL.createObjectURL(new Blob([source()],{type:'image/svg+xml'}));
      await new Promise((resolve,reject)=>{ image.onload=resolve; image.onerror=reject; image.src=url; });
      const raster=document.createElement('canvas'); raster.width=Math.round(width*scale); raster.height=Math.round(height*scale);
      const ctx=raster.getContext('2d'); if(format==='jpeg'){ctx.fillStyle='#ffffff';ctx.fillRect(0,0,raster.width,raster.height);} ctx.drawImage(image,0,0,raster.width,raster.height); URL.revokeObjectURL(url);
      const mime=format==='jpeg'?'image/jpeg':format==='webp'?'image/webp':'image/png';
      const blob=await new Promise(resolve=>raster.toBlob(resolve,mime,.94)); if(blob) saveBlob(blob,format==='jpeg'?'jpg':format);
    };
    document.querySelector('.toolbar').addEventListener('click', async event => {
      const action=event.target.closest('[data-action]')?.dataset.action; if(!action)return;
      if(action==='zoom-in')zoom(1.2); else if(action==='zoom-out')zoom(.83); else if(action==='reset')reset();
      else if(action==='theme')document.documentElement.dataset.theme=document.documentElement.dataset.theme==='dark'?'light':'dark';
      else if(action==='download')await exportDiagram();
      else if(action==='copy') await copySource();
    });
    document.addEventListener('keydown', event => {
      if(event.target.matches('select'))return;
      if(event.key==='+'||event.key==='=')zoom(1.2); else if(event.key==='-')zoom(.83); else if(event.key==='0'||event.key.toLowerCase()==='r')reset();
      else if(event.key.toLowerCase()==='t')document.querySelector('[data-action=theme]').click(); else if(event.key.toLowerCase()==='s'){event.preventDefault();exportDiagram();}
    });
    render();
  })();
  </script>
</body>
</html>
skills/fireworks-tech-graph/fixtures/quality-baseline/agent-runtime-style1.json
{
  "schema_version": 1,
  "mode": "architecture",
  "template_type": "architecture",
  "style": 1,
  "quality_profile": "showcase",
  "width": 960,
  "height": 600,
  "title": "Agent Runtime Architecture",
  "subtitle": "orchestration, memory, tools, and evaluation",
  "containers": [
    { "id": "control-plane", "x": 40, "y": 120, "width": 880, "height": 116, "label": "Control Plane" },
    { "id": "execution-state", "x": 40, "y": 304, "width": 880, "height": 136, "label": "Execution and State" }
  ],
  "nodes": [
    { "id": "client", "kind": "rect", "x": 60, "y": 156, "width": 160, "height": 60, "label": "Client", "sublabel": "request + policy", "fill": "#f8fafc", "stroke": "#94a3b8", "flat": true },
    { "id": "gateway", "kind": "double_rect", "x": 300, "y": 156, "width": 180, "height": 60, "label": "Gateway", "sublabel": "auth + routing", "fill": "#fffbeb", "stroke": "#f59e0b", "flat": true },
    { "id": "agent", "kind": "double_rect", "x": 560, "y": 156, "width": 180, "height": 60, "label": "Agent Runtime", "sublabel": "plan + inference", "fill": "#faf5ff", "stroke": "#8b5cf6", "flat": true },
    { "id": "memory", "kind": "rect", "x": 300, "y": 344, "width": 180, "height": 60, "label": "Vector Memory", "sublabel": "retrieve + persist", "fill": "#f0f9ff", "stroke": "#38bdf8", "flat": true },
    { "id": "tools", "kind": "rect", "x": 560, "y": 344, "width": 180, "height": 60, "label": "Tool Runtime", "sublabel": "MCP + functions", "fill": "#f0fdf4", "stroke": "#22c55e", "flat": true },
    { "id": "eval", "kind": "rect", "x": 790, "y": 344, "width": 110, "height": 60, "label": "Trace + Eval", "sublabel": "quality gate", "fill": "#fff1f2", "stroke": "#fb7185", "flat": true }
  ],
  "arrows": [
    { "id": "request", "source": "client", "target": "gateway", "source_port": "right", "target_port": "left", "flow": "control", "label": "request" },
    { "id": "dispatch", "source": "gateway", "target": "agent", "source_port": "right", "target_port": "left", "flow": "control", "label": "dispatch" },
    { "id": "context", "source": "agent", "target": "memory", "source_port": "bottom", "target_port": "top", "flow": "read", "label": "context", "route_points": [[632, 270], [390, 270]] },
    { "id": "tool-call", "source": "agent", "target": "tools", "source_port": "bottom", "target_port": "top", "flow": "async", "label": "tool call", "dashed": true },
    { "id": "augment", "source": "memory", "target": "tools", "source_port": "right", "target_port": "left", "flow": "write", "label": "augment" },
    { "id": "trace", "source": "tools", "target": "eval", "source_port": "right", "target_port": "left", "flow": "data", "label": "trace" },
    { "id": "z-feedback", "source": "eval", "target": "agent", "source_port": "top", "target_port": "bottom", "flow": "feedback", "label": "feedback", "dashed": true, "route_points": [[845, 270], [668, 270]] }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 510,
  "legend_locked": true,
  "legend": [
    { "flow": "control", "label": "primary" },
    { "flow": "write", "label": "data" },
    { "flow": "async", "label": "dependency" }
  ],
  "footer": "Style 1 · Flat Icon · showcase composition contract",
  "footer_x": 48,
  "footer_y": 570
}
skills/fireworks-tech-graph/fixtures/quality-baseline/agent-runtime-style10.json
{
  "schema_version": 1,
  "mode": "architecture",
  "template_type": "architecture",
  "style": 10,
  "semantic_profile": "generic",
  "quality_profile": "showcase",
  "width": 960,
  "height": 600,
  "title": "Agent Runtime Architecture",
  "subtitle": "orchestration, memory, tools, and evaluation",
  "containers": [
    { "id": "control-plane", "x": 40, "y": 120, "width": 880, "height": 116, "label": "Control Plane" },
    { "id": "execution-state", "x": 40, "y": 304, "width": 880, "height": 136, "label": "Execution and State" }
  ],
  "nodes": [
    { "id": "client", "kind": "rect", "x": 60, "y": 156, "width": 160, "height": 60, "label": "Client", "sublabel": "request + policy", "fill": "#f8fafc", "stroke": "#94a3b8", "flat": true },
    { "id": "gateway", "kind": "double_rect", "x": 300, "y": 156, "width": 180, "height": 60, "label": "Gateway", "sublabel": "auth + routing", "fill": "#fffbeb", "stroke": "#f59e0b", "flat": true },
    { "id": "agent", "kind": "double_rect", "x": 560, "y": 156, "width": 180, "height": 60, "label": "Agent Runtime", "sublabel": "plan + inference", "fill": "#faf5ff", "stroke": "#8b5cf6", "flat": true },
    { "id": "memory", "kind": "rect", "x": 300, "y": 344, "width": 180, "height": 60, "label": "Vector Memory", "sublabel": "retrieve + persist", "fill": "#f0f9ff", "stroke": "#38bdf8", "flat": true },
    { "id": "tools", "kind": "rect", "x": 560, "y": 344, "width": 180, "height": 60, "label": "Tool Runtime", "sublabel": "MCP + functions", "fill": "#f0fdf4", "stroke": "#22c55e", "flat": true },
    { "id": "eval", "kind": "rect", "x": 790, "y": 344, "width": 110, "height": 60, "label": "Trace + Eval", "sublabel": "quality gate", "fill": "#fff1f2", "stroke": "#fb7185", "flat": true }
  ],
  "arrows": [
    { "id": "request", "source": "client", "target": "gateway", "source_port": "right", "target_port": "left", "flow": "control", "label": "request" },
    { "id": "dispatch", "source": "gateway", "target": "agent", "source_port": "right", "target_port": "left", "flow": "control", "label": "dispatch" },
    { "id": "context", "source": "agent", "target": "memory", "source_port": "bottom", "target_port": "top", "flow": "read", "label": "context", "route_points": [[632, 270], [390, 270]] },
    { "id": "tool-call", "source": "agent", "target": "tools", "source_port": "bottom", "target_port": "top", "flow": "async", "label": "tool call", "dashed": true },
    { "id": "augment", "source": "memory", "target": "tools", "source_port": "right", "target_port": "left", "flow": "write", "label": "augment" },
    { "id": "trace", "source": "tools", "target": "eval", "source_port": "right", "target_port": "left", "flow": "data", "label": "trace" },
    { "id": "z-feedback", "source": "eval", "target": "agent", "source_port": "top", "target_port": "bottom", "flow": "feedback", "label": "feedback", "dashed": true, "route_points": [[845, 270], [668, 270]] }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 510,
  "legend_locked": true,
  "legend": [
    { "flow": "control", "label": "primary" },
    { "flow": "write", "label": "data" },
    { "flow": "async", "label": "dependency" }
  ],
  "footer": "Style 10 · Cloud Fabric · showcase composition contract",
  "footer_x": 48,
  "footer_y": 570
}
skills/fireworks-tech-graph/package.json
{
  "name": "@yizhiyanhua-ai/fireworks-tech-graph",
  "version": "1.2.0",
  "description": "Agent Skill and CLI for geometry-safe technical diagrams, focused SVG-to-GIF motion, and offline HTML.",
  "keywords": [
    "claude-code",
    "codex",
    "openai-codex",
    "agent-skills",
    "skill",
    "diagram",
    "svg",
    "architecture",
    "flowchart",
    "uml",
    "sequence-diagram",
    "er-diagram",
    "visualization",
    "animation",
    "gif"
  ],
  "author": "yizhiyanhua-ai",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/yizhiyanhua-ai/fireworks-tech-graph.git"
  },
  "bugs": {
    "url": "https://github.com/yizhiyanhua-ai/fireworks-tech-graph/issues"
  },
  "homepage": "https://yizhiyanhua-ai.github.io/fireworks-tech-graph/",
  "bin": {
    "fireworks-tech-graph": "scripts/fireworks.py"
  },
  "scripts": {
    "test": "python3 -m unittest discover -s tests -v",
    "check": "python3 tools/check_project_consistency.py && python3 tools/distribution.py --check",
    "examples": "python3 scripts/fireworks.py examples"
  },
  "files": [
    "SKILL.md",
    "README.md",
    "README.zh.md",
    "LICENSE",
    "CHANGELOG.md",
    "CONTRIBUTING.md",
    "agents/",
    "docs/",
    "examples/",
    "references/",
    "schemas/",
    "scripts/",
    "tests/",
    "fixtures/",
    "templates/",
    "assets/",
    "!**/__pycache__/**",
    "!**/*.pyc"
  ],
  "engines": {
    "node": ">=18.0.0"
  }
}
skills/fireworks-tech-graph/references/composition-quality-contract.md
# Composition Quality Contract

This contract applies to every visual style. Style references control color,
typography, material, corner radius, and decorative treatment. They never
weaken geometry or composition quality.

## Official showcase profile

Use `"quality_profile": "showcase"` for official samples and polished delivery
artifacts. The renderer and validator enforce all of these budgets:

| Metric | Showcase limit |
|---|---:|
| Edge crossings | 0 |
| Bridge jumps | 0 |
| Bends on one edge | 2 maximum |
| Bends in the six-node reference topology | 8 maximum |
| Route length / direct Manhattan length | 1.35 maximum |
| Shortest route segment | 16px minimum |
| Node-to-node whitespace | 40px minimum |
| Node-to-container gutter | 20px minimum |
| Edge-label clearance from unrelated geometry | 4px minimum |

The six-node reference topology currently scores 100 with four total bends,
zero crossings, zero bridges, a route-stretch maximum of 1.0, 50px minimum
node spacing, and 20px minimum container gutter.

## Layout grammar

1. Establish containers, their header reservations, and node rows before any
   edge is routed.
2. Keep nodes in a row aligned to the same y coordinate and use consistent
   heights for equivalent semantic roles.
3. Reserve one empty inter-container corridor for cross-layer routes. A route
   may cross a container boundary only through an open gap and must not run on
   top of the border.
4. Route the primary horizontal flow first. Route cross-layer context and
   feedback paths through separate, non-overlapping corridor segments.
5. Use distinct node ports when several edges share a side. Never stack
   multiple arrowheads on one coordinate.
6. Prefer a monotone orthogonal route. If a connection needs more than two
   bends, change the node placement before adding waypoints.
7. Treat every external title, subtitle, tag, side label, edge label, legend,
   footer, and title block as an obstacle with measurable bounds.
8. Keep legends outside business-flow corridors. Use a single horizontal row
   when the canvas has enough width.
9. Fit long single-line node titles to the card width. Do not let text touch or
   cross the node border.
10. If the topology cannot meet the showcase limits, simplify the composition,
    split it into focused diagrams, or explicitly use the standard profile for
    a non-showcase engineering stress artifact.

## Style identity boundary

Styles may vary these properties freely within their own reference:

- background, palette, semantic accent colors;
- font family, title alignment, and typographic hierarchy;
- card fill, border treatment, shadow, glow, and corner radius;
- blueprint title blocks, terminal chrome, or restrained brand details.

Styles must share these structural properties for a direct comparison:

- topology and edge direction;
- row/column alignment;
- port assignment and corridor positions;
- crossing, bridge, bend, stretch, spacing, and gutter budgets;
- semantic SVG roles required by the validator.

Visual effects never create a second business connector. Pencil echoes, rail
casings, critical-path glow, and similar layers use
`data-graph-role="decoration"` with `data-owner`; exactly one ordinary
`data-graph-role="edge"` carries the source, target, route, and arrowhead.

## Validation

Run both gates before delivery:

```bash
python3 scripts/validate_svg.py diagram.svg --check geometry
python3 scripts/validate_svg.py diagram.svg --check composition
```

`scripts/validate-svg.sh` runs both automatically. A successful render without
these gates is still a draft.

Dense legacy diagrams live under `fixtures/stress/`. They preserve routing
pressure cases and are not visual quality references. The public 12-style
showcase keeps a distinct engineering scene for every style. The internal
`fixtures/quality-baseline/` set applies one shared Agent Runtime Architecture
topology to all 11 generator-backed styles; Style 8 remains the static,
AI-authored exception.
skills/fireworks-tech-graph/fixtures/quality-baseline/agent-runtime-style2.json
{
  "schema_version": 1,
  "mode": "architecture",
  "template_type": "architecture",
  "style": 2,
  "quality_profile": "showcase",
  "width": 960,
  "height": 600,
  "title": "Agent Runtime Architecture",
  "subtitle": "orchestration, memory, tools, and evaluation",
  "window_controls": true,
  "meta_center": "AGENT RUNTIME / SHOWCASE",
  "meta_right": "STYLE-2 · DARK TERMINAL",
  "containers": [
    { "id": "control-plane", "x": 40, "y": 120, "width": 880, "height": 116, "label": "Control Plane" },
    { "id": "execution-state", "x": 40, "y": 304, "width": 880, "height": 136, "label": "Execution and State" }
  ],
  "nodes": [
    { "id": "client", "kind": "rect", "x": 60, "y": 156, "width": 160, "height": 60, "label": "Client", "sublabel": "request + policy", "fill": "#111827", "stroke": "#64748b", "flat": true },
    { "id": "gateway", "kind": "double_rect", "x": 300, "y": 156, "width": 180, "height": 60, "label": "Gateway", "sublabel": "auth + routing", "fill": "#111827", "stroke": "#f59e0b", "glow": "orange" },
    { "id": "agent", "kind": "double_rect", "x": 560, "y": 156, "width": 180, "height": 60, "label": "Agent Runtime", "sublabel": "plan + inference", "fill": "#111827", "stroke": "#a855f7", "glow": "purple" },
    { "id": "memory", "kind": "rect", "x": 300, "y": 344, "width": 180, "height": 60, "label": "Vector Memory", "sublabel": "retrieve + persist", "fill": "#0c2238", "stroke": "#38bdf8", "flat": true },
    { "id": "tools", "kind": "rect", "x": 560, "y": 344, "width": 180, "height": 60, "label": "Tool Runtime", "sublabel": "MCP + functions", "fill": "#10291f", "stroke": "#22c55e", "flat": true },
    { "id": "eval", "kind": "rect", "x": 790, "y": 344, "width": 110, "height": 60, "label": "Trace + Eval", "sublabel": "quality gate", "fill": "#331a24", "stroke": "#fb7185", "flat": true }
  ],
  "arrows": [
    { "id": "request", "source": "client", "target": "gateway", "source_port": "right", "target_port": "left", "flow": "control", "label": "request" },
    { "id": "dispatch", "source": "gateway", "target": "agent", "source_port": "right", "target_port": "left", "flow": "control", "label": "dispatch" },
    { "id": "context", "source": "agent", "target": "memory", "source_port": "bottom", "target_port": "top", "flow": "read", "label": "context", "route_points": [[632, 270], [390, 270]] },
    { "id": "tool-call", "source": "agent", "target": "tools", "source_port": "bottom", "target_port": "top", "flow": "async", "label": "tool call", "dashed": true },
    { "id": "augment", "source": "memory", "target": "tools", "source_port": "right", "target_port": "left", "flow": "write", "label": "augment" },
    { "id": "trace", "source": "tools", "target": "eval", "source_port": "right", "target_port": "left", "flow": "data", "label": "trace" },
    { "id": "z-feedback", "source": "eval", "target": "agent", "source_port": "top", "target_port": "bottom", "flow": "feedback", "label": "feedback", "dashed": true, "route_points": [[845, 270], [668, 270]] }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 510,
  "legend_locked": true,
  "legend": [
    { "flow": "control", "label": "primary" },
    { "flow": "write", "label": "data" },
    { "flow": "async", "label": "dependency" }
  ],
  "footer": "Style 2 · Dark Terminal · showcase composition contract",
  "footer_x": 48,
  "footer_y": 570
}
skills/fireworks-tech-graph/references/icons.md
# Icon Reference

## Rules for Renderer Compatibility (cairosvg / rsvg-convert)

**Never use** `@import url()` for icon fonts — neither cairosvg nor rsvg-convert fetches external resources.
**Always use** inline SVG `<path>`, `<circle>`, `<rect>`, `<text>` combinations.
**Font fallback**: embed font-family in `<style>` using system fonts only.

---

## Generic Semantic Shapes (No product — use these first)

### Database / Vector Store (cylinder)
```xml
<!-- cx=center-x, top=top-y, w=width, h=height -->
<!-- Typical: w=80, h=70 -->
<ellipse cx="cx" cy="top" rx="w/2" ry="w/6" fill="fill" stroke="stroke" stroke-width="1.5"/>
<rect x="cx-w/2" y="top" width="w" height="h" fill="fill" stroke="none"/>
<line x1="cx-w/2" y1="top" x2="cx-w/2" y2="top+h" stroke="stroke" stroke-width="1.5"/>
<line x1="cx+w/2" y1="top" x2="cx+w/2" y2="top+h" stroke="stroke" stroke-width="1.5"/>
<!-- Optional inner rings for Vector Store -->
<ellipse cx="cx" cy="top+h*0.33" rx="w/2" ry="w/6" fill="none" stroke="stroke" stroke-width="0.7" opacity="0.5"/>
<ellipse cx="cx" cy="top+h*0.66" rx="w/2" ry="w/6" fill="none" stroke="stroke" stroke-width="0.7" opacity="0.5"/>
<ellipse cx="cx" cy="top+h" rx="w/2" ry="w/6" fill="fill-dark" stroke="stroke" stroke-width="1.5"/>
```

### LLM / Model Node (rounded rect with spark)
```xml
<!-- Rounded rect with double border = "intelligent" signal -->
<rect x="x" y="y" width="w" height="h" rx="10" fill="fill" stroke="stroke-outer" stroke-width="2.5"/>
<rect x="x+3" y="y+3" width="w-6" height="h-6" rx="8" fill="none" stroke="stroke-inner" stroke-width="0.8" opacity="0.5"/>
<!-- Spark icon (⚡) as text or small lightning path -->
<text x="cx" y="cy-6" text-anchor="middle" font-size="14">⚡</text>
<text x="cx" y="cy+10" text-anchor="middle" fill="text-color" font-size="13" font-weight="600">GPT-4o</text>
```

### Agent / Orchestrator (hexagon)
```xml
<!-- r = circumradius, cx/cy = center -->
<!-- For r=36: points at 36,0  18,31.2  -18,31.2  -36,0  -18,-31.2  18,-31.2 -->
<polygon points="cx,cy-r  cx+r*0.866,cy-r*0.5  cx+r*0.866,cy+r*0.5  cx,cy+r  cx-r*0.866,cy+r*0.5  cx-r*0.866,cy-r*0.5"
         fill="fill" stroke="stroke" stroke-width="1.5"/>
<text x="cx" y="cy+5" text-anchor="middle" fill="text" font-size="12" font-weight="600">Agent</text>
```

### Memory Node (short-term, dashed border)
```xml
<rect x="x" y="y" width="w" height="h" rx="8"
      fill="fill" stroke="stroke" stroke-width="1.5" stroke-dasharray="6,3"/>
<text x="cx" y="cy-6" text-anchor="middle" fill="text" font-size="10" opacity="0.7">MEMORY</text>
<text x="cx" y="cy+8" text-anchor="middle" fill="text" font-size="13">Short-term</text>
```

### Tool / Function Call (rect with gear symbol)
```xml
<rect x="x" y="y" width="w" height="h" rx="6" fill="fill" stroke="stroke" stroke-width="1.5"/>
<!-- Gear: simplified as ⚙ unicode or small circle with lines -->
<text x="cx" y="cy-4" text-anchor="middle" font-size="16">⚙</text>
<text x="cx" y="cy+12" text-anchor="middle" fill="text" font-size="12">Tool Name</text>
```

### Queue / Stream (horizontal pipe)
```xml
<!-- Pipe tube: left cap ellipse + body + right cap ellipse -->
<ellipse cx="x1" cy="cy" rx="ry*0.6" ry="ry" fill="fill-dark" stroke="stroke" stroke-width="1.5"/>
<rect x="x1" y="cy-ry" width="x2-x1" height="ry*2" fill="fill" stroke="none"/>
<line x1="x1" y1="cy-ry" x2="x2" y2="cy-ry" stroke="stroke" stroke-width="1.5"/>
<line x1="x1" y1="cy+ry" x2="x2" y2="cy+ry" stroke="stroke" stroke-width="1.5"/>
<ellipse cx="x2" cy="cy" rx="ry*0.6" ry="ry" fill="fill-light" stroke="stroke" stroke-width="1.5"/>
```

### User / Human Actor
```xml
<!-- Head -->
<circle cx="cx" cy="cy-18" r="10" fill="fill" stroke="stroke" stroke-width="1.2"/>
<!-- Body / shoulders -->
<path d="M cx-14,cy+16 Q cx-14,cy-4 cx,cy-4 Q cx+14,cy-4 cx+14,cy+16"
      fill="fill" stroke="stroke" stroke-width="1.2"/>
<text x="cx" y="cy+30" text-anchor="middle" fill="text" font-size="12">User</text>
```

### API Gateway (hexagon, single border, smaller)
```xml
<polygon points="cx,cy-28  cx+24,cy-14  cx+24,cy+14  cx,cy+28  cx-24,cy+14  cx-24,cy-14"
         fill="fill" stroke="stroke" stroke-width="1.5"/>
<text x="cx" y="cy+5" text-anchor="middle" fill="text" font-size="11">API</text>
```

### Browser / Web Client
```xml
<rect x="x" y="y" width="w" height="h" rx="6" fill="fill" stroke="stroke" stroke-width="1.5"/>
<!-- Title bar -->
<rect x="x" y="y" width="w" height="20" rx="6" fill="fill-dark" stroke="none"/>
<rect x="x" y="y+14" width="w" height="6" fill="fill-dark"/>
<!-- Traffic light dots -->
<circle cx="x+12" cy="y+10" r="4" fill="#ef4444" opacity="0.8"/>
<circle cx="x+24" cy="y+10" r="4" fill="#f59e0b" opacity="0.8"/>
<circle cx="x+36" cy="y+10" r="4" fill="#10b981" opacity="0.8"/>
```

### Document / File
```xml
<!-- Folded corner rectangle -->
<path d="M x,y L x+w-12,y L x+w,y+12 L x+w,y+h L x,y+h Z"
      fill="fill" stroke="stroke" stroke-width="1.5"/>
<!-- Fold -->
<path d="M x+w-12,y L x+w-12,y+12 L x+w,y+12" fill="fill-dark" stroke="stroke" stroke-width="1"/>
<!-- Lines inside -->
<line x1="x+8" y1="y+h*0.45" x2="x+w-8" y2="y+h*0.45" stroke="stroke" stroke-width="1" opacity="0.5"/>
<line x1="x+8" y1="y+h*0.6"  x2="x+w-8" y2="y+h*0.6"  stroke="stroke" stroke-width="1" opacity="0.5"/>
<line x1="x+8" y1="y+h*0.75" x2="x+w-16" y2="y+h*0.75" stroke="stroke" stroke-width="1" opacity="0.5"/>
```

### Decision Diamond (flowcharts)
```xml
<!-- cx/cy = center, hw = half-width, hh = half-height -->
<polygon points="cx,cy-hh  cx+hw,cy  cx,cy+hh  cx-hw,cy"
         fill="fill" stroke="stroke" stroke-width="1.5"/>
<text x="cx" y="cy+5" text-anchor="middle" fill="text" font-size="12">Condition?</text>
```

### Swim Lane Container
```xml
<!-- Background band for a layer/group -->
<rect x="x" y="y" width="w" height="h" rx="6"
      fill="fill" fill-opacity="0.04" stroke="stroke" stroke-width="1" stroke-dasharray="6,4"/>
<!-- Layer label top-left -->
<text x="x+12" y="y+16" fill="label-color" font-size="10" font-weight="600" letter-spacing="0.06em">LAYER NAME</text>
```

---

## Product Icons (Brand Colors + Inline SVG)

All use circle badge + text abbreviation pattern. Replace `cx`, `cy` with actual coordinates.

### AI / ML Products

| Product | Color | Badge Text |
|---------|-------|-----------|
| OpenAI / ChatGPT | `#10A37F` | `OAI` |
| Anthropic / Claude | `#D97757` | `Claude` |
| Google Gemini | `#4285F4` | `Gemini` |
| Meta LLaMA | `#0467DF` | `LLaMA` |
| Mistral | `#FF7000` | `Mistral` |
| Cohere | `#39594D` | `Cohere` |
| Groq | `#F55036` | `Groq` |
| Together AI | `#6366F1` | `Together` |
| Replicate | `#191919` | `Rep` |
| Hugging Face | `#FFD21E` (text dark) | `HF` |

**Template:**
```xml
<circle cx="cx" cy="cy" r="22" fill="BRAND_COLOR"/>
<text x="cx" y="cy+5" text-anchor="middle" fill="white"
      font-size="10" font-weight="700" font-family="Helvetica">BADGE_TEXT</text>
<!-- Optional: outer ring for "AI" products -->
<circle cx="cx" cy="cy" r="24" fill="none" stroke="BRAND_COLOR" stroke-width="1" opacity="0.4"/>
```

### AI Memory & RAG Products

| Product | Color | Badge |
|---------|-------|-------|
| Mem0 | `#6366F1` | `mem0` |
| LangChain | `#1C3C3C` | `🦜` or `LC` |
| LlamaIndex | `#8B5CF6` | `LI` |
| LangGraph | `#1C3C3C` | `LG` |
| CrewAI | `#EF4444` | `Crew` |
| AutoGen | `#0078D4` | `AG` |
| Haystack | `#FF6D00` | `🌾` or `HS` |
| DSPy | `#7C3AED` | `DSPy` |

### Vector Databases

| Product | Color | Badge |
|---------|-------|-------|
| Pinecone | `#1C1C2E` + green | `Pine` |
| Weaviate | `#FA0050` | `Wea` |
| Qdrant | `#DC244C` | `Qdrant` |
| Chroma | `#FF6B35` | `Chr` |
| Milvus | `#00A1EA` | `Milvus` |
| pgvector | `#336791` | `pgv` |
| Faiss | `#0467DF` | `FAISS` |

**Vector DB template (cylinder + badge):**
```xml
<!-- Cylinder shape -->
<ellipse cx="cx" cy="top" rx="40" ry="12" fill="FILL" stroke="STROKE" stroke-width="1.5"/>
<rect x="cx-40" y="top" width="80" height="50" fill="FILL" stroke="none"/>
<line x1="cx-40" y1="top" x2="cx-40" y2="top+50" stroke="STROKE" stroke-width="1.5"/>
<line x1="cx+40" y1="top" x2="cx+40" y2="top+50" stroke="STROKE" stroke-width="1.5"/>
<ellipse cx="cx" cy="top+50" rx="40" ry="12" fill="FILL_DARK" stroke="STROKE" stroke-width="1.5"/>
<!-- Product name -->
<text x="cx" y="top+30" text-anchor="middle" fill="white"
      font-size="11" font-weight="700">Pinecone</text>
```

### Classic Databases & Storage

| Product | Color |
|---------|-------|
| PostgreSQL | `#336791` |
| MySQL | `#4479A1` |
| MongoDB | `#47A248` |
| Redis | `#DC382D` |
| Elasticsearch | `#005571` |
| Cassandra | `#1287B1` |
| Neo4j | `#008CC1` |
| SQLite | `#003B57` |

### Message Queues & Streaming

| Product | Color |
|---------|-------|
| Apache Kafka | `#231F20` |
| RabbitMQ | `#FF6600` |
| AWS SQS | `#FF9900` |
| NATS | `#27AAE1` |
| Pulsar | `#188FFF` |

### Cloud & Infra

| Product | Color |
|---------|-------|
| AWS | `#FF9900` |
| GCP | `#4285F4` |
| Azure | `#0089D6` |
| Cloudflare | `#F48120` |
| Vercel | `#000000` |
| Docker | `#2496ED` |
| Kubernetes | `#326CE5` |
| Terraform | `#7B42BC` |
| Nginx | `#009639` |
| FastAPI | `#009688` |

### Observability

| Product | Color |
|---------|-------|
| Grafana | `#F46800` |
| Prometheus | `#E6522C` |
| Datadog | `#632CA6` |
| LangSmith | `#1C3C3C` |
| Langfuse | `#6366F1` |
| Arize | `#6B48FF` |

---

## Azure Service Icons

Azure brand colour: `#0089D6` (top tile / outer ring).
Service-specific accents come from Microsoft's Azure icon set; use them
as the inner badge fill so a glance still tells you "this is Azure".

**Template (Azure tile):**
```xml
<!-- Azure tile: outer rounded square in Azure blue, inner badge for the service. -->
<rect x="cx-22" y="cy-22" width="44" height="44" rx="6"
      fill="#0089D6" stroke="none"/>
<rect x="cx-19" y="cy-19" width="38" height="38" rx="4"
      fill="SERVICE_COLOR" stroke="none"/>
<text x="cx" y="cy+5" text-anchor="middle" fill="white"
      font-size="9" font-weight="700" font-family="Helvetica">BADGE</text>
```

### Azure Compute

| Product | Service Color | Badge |
|---------|---------------|-------|
| Azure Functions | `#0062AD` | `Func` |
| Azure App Service | `#0072C6` | `App` |
| Azure Container Apps | `#3F8624` | `ACA` |
| Azure Container Instances | `#0078D4` | `ACI` |
| Azure Kubernetes Service (AKS) | `#326CE5` | `AKS` |
| Azure Virtual Machines | `#0078D4` | `VM` |
| Azure Batch | `#0072C6` | `Batch` |
| Azure Spring Apps | `#6DB33F` | `Spring` |

### Azure Data & Analytics

| Product | Service Color | Badge |
|---------|---------------|-------|
| Azure SQL Database | `#0066A1` | `SQL` |
| Azure Cosmos DB | `#3D7AB3` | `Cosmos` |
| Azure Database for PostgreSQL | `#336791` | `pg` |
| Azure Database for MySQL | `#4479A1` | `MySQL` |
| Azure Synapse Analytics | `#0078D4` | `Syn` |
| Azure Data Factory | `#0078D4` | `ADF` |
| Azure Databricks | `#FF3621` | `Bricks` |
| Azure Stream Analytics | `#0072C6` | `Stream` |
| Azure Data Explorer (Kusto) | `#1E5180` | `Kusto` |
| Azure Cache for Redis | `#DC382D` | `Redis` |

### Azure Storage

| Product | Service Color | Badge |
|---------|---------------|-------|
| Azure Blob Storage | `#0078D4` | `Blob` |
| Azure Queue Storage | `#0078D4` | `Queue` |
| Azure Table Storage | `#0078D4` | `Table` |
| Azure Files | `#0078D4` | `Files` |
| Azure Data Lake Storage Gen2 | `#0078D4` | `Lake` |

### Azure AI

| Product | Service Color | Badge |
|---------|---------------|-------|
| Azure OpenAI Service | `#10A37F` | `AOAI` |
| Azure AI Search (Cognitive Search) | `#0078D4` | `AISrch` |
| Azure AI Foundry | `#742774` | `Foundry` |
| Azure Machine Learning | `#0078D4` | `AML` |
| Azure AI Content Safety | `#107C10` | `Safety` |
| Azure Speech / Translator | `#0078D4` | `Speech` |

### Azure Messaging & Eventing

| Product | Service Color | Badge |
|---------|---------------|-------|
| Azure Service Bus | `#0078D4` | `SB` |
| Azure Event Grid | `#0078D4` | `Grid` |
| Azure Event Hubs | `#0078D4` | `Hubs` |
| Azure Notification Hubs | `#0078D4` | `Notif` |
| Azure SignalR Service | `#0078D4` | `SignalR` |

### Azure Networking & Edge

| Product | Service Color | Badge |
|---------|---------------|-------|
| Azure Front Door | `#0078D4` | `AFD` |
| Azure Application Gateway | `#0078D4` | `AppGW` |
| Azure Load Balancer | `#0078D4` | `LB` |
| Azure API Management | `#1FBA9F` | `APIM` |
| Azure Virtual Network | `#0078D4` | `VNet` |
| Azure Private Link | `#0078D4` | `PL` |
| Azure CDN | `#0078D4` | `CDN` |
| Azure DNS | `#0078D4` | `DNS` |

### Azure Identity & Security

| Product | Service Color | Badge |
|---------|---------------|-------|
| Microsoft Entra ID (Azure AD) | `#0072C6` | `Entra` |
| Azure Key Vault | `#FFB900` | `KV` |
| Azure Sentinel | `#0072C6` | `Sentinel` |
| Microsoft Defender for Cloud | `#0078D4` | `Defender` |

### Azure DevOps & Operations

| Product | Service Color | Badge |
|---------|---------------|-------|
| Azure DevOps Pipelines | `#0078D4` | `Pipelines` |
| GitHub Actions (Azure target) | `#181717` | `GHA` |
| Azure Monitor | `#0078D4` | `Monitor` |
| Application Insights | `#0072C6` | `AppI` |
| Azure Log Analytics | `#0078D4` | `Logs` |

### Azure-specific shapes

For diagrams that need a recognisable "Azure" visual without a service
badge — e.g. a region container or a subscription boundary — use a
dashed Azure-blue outline:

```xml
<!-- Azure region/subscription container -->
<rect x="x" y="y" width="w" height="h" rx="8"
      fill="#0089D6" fill-opacity="0.04"
      stroke="#0089D6" stroke-width="1.2" stroke-dasharray="6,4"/>
<text x="x+12" y="y+16" fill="#0089D6" font-size="10"
      font-weight="700" letter-spacing="0.06em">AZURE • REGION NAME</text>
```

---

## Icon Sizing Guide

| Context | Recommended Size | Padding |
|---------|-----------------|---------|
| Node badge (inside box) | 28×28px circle | 10px |
| Standalone icon node | 40×40px | 16px |
| Hero / central node | 56×56px | 20px |
| Small inline indicator | 16×16px | 6px |

## Arrow Marker Templates

```xml
<defs>
  <!-- Standard filled arrow -->
  <marker id="arrow-COLORNAME" markerWidth="10" markerHeight="7"
          refX="9" refY="3.5" orient="auto">
    <polygon points="0 0, 10 3.5, 0 7" fill="COLOR"/>
  </marker>

  <!-- Open arrow (outline only) -->
  <marker id="arrow-open" markerWidth="10" markerHeight="8"
          refX="9" refY="4" orient="auto">
    <path d="M 0 0 L 10 4 L 0 8" fill="none" stroke="COLOR" stroke-width="1.5"/>
  </marker>

  <!-- Circle dot (for association lines) -->
  <marker id="dot" markerWidth="8" markerHeight="8"
          refX="4" refY="4" orient="auto">
    <circle cx="4" cy="4" r="3" fill="COLOR"/>
  </marker>
</defs>
```
skills/fireworks-tech-graph/fixtures/quality-baseline/agent-runtime-style5.json
{
  "schema_version": 1,
  "mode": "architecture",
  "template_type": "architecture",
  "style": 5,
  "quality_profile": "showcase",
  "width": 960,
  "height": 600,
  "title": "Agent Runtime Architecture",
  "subtitle": "orchestration, memory, tools, and evaluation",
  "containers": [
    { "id": "control-plane", "x": 40, "y": 120, "width": 880, "height": 116, "label": "Control Plane" },
    { "id": "execution-state", "x": 40, "y": 304, "width": 880, "height": 136, "label": "Execution and State" }
  ],
  "nodes": [
    { "id": "client", "kind": "rect", "x": 60, "y": 156, "width": 160, "height": 60, "label": "Client", "sublabel": "request + policy", "fill": "rgba(255,255,255,0.10)", "stroke": "rgba(255,255,255,0.34)", "flat": true },
    { "id": "gateway", "kind": "double_rect", "x": 300, "y": 156, "width": 180, "height": 60, "label": "Gateway", "sublabel": "auth + routing", "fill": "rgba(255,255,255,0.12)", "stroke": "#f59e0b" },
    { "id": "agent", "kind": "double_rect", "x": 560, "y": 156, "width": 180, "height": 60, "label": "Agent Runtime", "sublabel": "plan + inference", "fill": "rgba(255,255,255,0.12)", "stroke": "#c084fc" },
    { "id": "memory", "kind": "rect", "x": 300, "y": 344, "width": 180, "height": 60, "label": "Vector Memory", "sublabel": "retrieve + persist", "fill": "rgba(56,189,248,0.10)", "stroke": "#60a5fa", "flat": true },
    { "id": "tools", "kind": "rect", "x": 560, "y": 344, "width": 180, "height": 60, "label": "Tool Runtime", "sublabel": "MCP + functions", "fill": "rgba(52,211,153,0.10)", "stroke": "#34d399", "flat": true },
    { "id": "eval", "kind": "rect", "x": 790, "y": 344, "width": 110, "height": 60, "label": "Trace + Eval", "sublabel": "quality gate", "fill": "rgba(251,113,133,0.10)", "stroke": "#fb7185", "flat": true }
  ],
  "arrows": [
    { "id": "request", "source": "client", "target": "gateway", "source_port": "right", "target_port": "left", "flow": "control", "label": "request" },
    { "id": "dispatch", "source": "gateway", "target": "agent", "source_port": "right", "target_port": "left", "flow": "control", "label": "dispatch" },
    { "id": "context", "source": "agent", "target": "memory", "source_port": "bottom", "target_port": "top", "flow": "read", "label": "context", "route_points": [[632, 270], [390, 270]] },
    { "id": "tool-call", "source": "agent", "target": "tools", "source_port": "bottom", "target_port": "top", "flow": "async", "label": "tool call", "dashed": true },
    { "id": "augment", "source": "memory", "target": "tools", "source_port": "right", "target_port": "left", "flow": "write", "label": "augment" },
    { "id": "trace", "source": "tools", "target": "eval", "source_port": "right", "target_port": "left", "flow": "data", "label": "trace" },
    { "id": "z-feedback", "source": "eval", "target": "agent", "source_port": "top", "target_port": "bottom", "flow": "feedback", "label": "feedback", "dashed": true, "route_points": [[845, 270], [668, 270]] }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 510,
  "legend_locked": true,
  "legend": [
    { "flow": "control", "label": "primary" },
    { "flow": "write", "label": "data" },
    { "flow": "async", "label": "dependency" }
  ],
  "footer": "Style 5 · Glassmorphism · showcase composition contract",
  "footer_x": 48,
  "footer_y": 570
}
skills/fireworks-tech-graph/fixtures/quality-baseline/agent-runtime-style7.json
{
  "schema_version": 1,
  "mode": "architecture",
  "template_type": "architecture",
  "style": 7,
  "quality_profile": "showcase",
  "width": 960,
  "height": 600,
  "title": "Agent Runtime Architecture",
  "subtitle": "orchestration, memory, tools, and evaluation",
  "containers": [
    { "id": "control-plane", "x": 40, "y": 120, "width": 880, "height": 116, "label": "Control Plane" },
    { "id": "execution-state", "x": 40, "y": 304, "width": 880, "height": 136, "label": "Execution and State" }
  ],
  "nodes": [
    { "id": "client", "kind": "rect", "x": 60, "y": 156, "width": 160, "height": 60, "label": "Client", "sublabel": "request + policy", "fill": "#ffffff", "stroke": "#cbd5e1", "flat": true },
    { "id": "gateway", "kind": "double_rect", "x": 300, "y": 156, "width": 180, "height": 60, "label": "Gateway", "sublabel": "auth + routing", "fill": "#ffffff", "stroke": "#10a37f" },
    { "id": "agent", "kind": "double_rect", "x": 560, "y": 156, "width": 180, "height": 60, "label": "Agent Runtime", "sublabel": "plan + inference", "fill": "#f0fdfa", "stroke": "#10a37f" },
    { "id": "memory", "kind": "rect", "x": 300, "y": 344, "width": 180, "height": 60, "label": "Vector Memory", "sublabel": "retrieve + persist", "fill": "#f0f9ff", "stroke": "#0891b2", "flat": true },
    { "id": "tools", "kind": "rect", "x": 560, "y": 344, "width": 180, "height": 60, "label": "Tool Runtime", "sublabel": "MCP + functions", "fill": "#ecfdf5", "stroke": "#10a37f", "flat": true },
    { "id": "eval", "kind": "rect", "x": 790, "y": 344, "width": 110, "height": 60, "label": "Trace + Eval", "sublabel": "quality gate", "fill": "#fff7ed", "stroke": "#f59e0b", "flat": true }
  ],
  "arrows": [
    { "id": "request", "source": "client", "target": "gateway", "source_port": "right", "target_port": "left", "flow": "control", "label": "request" },
    { "id": "dispatch", "source": "gateway", "target": "agent", "source_port": "right", "target_port": "left", "flow": "control", "label": "dispatch" },
    { "id": "context", "source": "agent", "target": "memory", "source_port": "bottom", "target_port": "top", "flow": "read", "label": "context", "route_points": [[632, 270], [390, 270]] },
    { "id": "tool-call", "source": "agent", "target": "tools", "source_port": "bottom", "target_port": "top", "flow": "async", "label": "tool call", "dashed": true },
    { "id": "augment", "source": "memory", "target": "tools", "source_port": "right", "target_port": "left", "flow": "write", "label": "augment" },
    { "id": "trace", "source": "tools", "target": "eval", "source_port": "right", "target_port": "left", "flow": "data", "label": "trace" },
    { "id": "z-feedback", "source": "eval", "target": "agent", "source_port": "top", "target_port": "bottom", "flow": "feedback", "label": "feedback", "dashed": true, "route_points": [[845, 270], [668, 270]] }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 510,
  "legend_locked": true,
  "legend": [
    { "flow": "control", "label": "primary" },
    { "flow": "write", "label": "data" },
    { "flow": "async", "label": "dependency" }
  ],
  "footer": "Style 7 · OpenAI Official · showcase composition contract",
  "footer_x": 48,
  "footer_y": 570
}
skills/fireworks-tech-graph/references/style-1-flat-icon.md
# Style 1: Flat Icon (Default)

Inspired by draw.io defaults and Apple documentation style.

## Colors

```
Background:     #ffffff
Box fill:       #ffffff
Box stroke:     #d1d5db  (gray-300)
Box radius:     8px
Text primary:   #111827  (gray-900)
Text secondary: #6b7280  (gray-500)

Semantic arrow colors (pick by flow type):
  Flow A (main):   #2563eb  (blue-600)
  Flow B (alt):    #dc2626  (red-600)
  Flow C (data):   #16a34a  (green-600)
  Flow D (async):  #9333ea  (purple-600)

Icon accent backgrounds:
  Blue tint:   #eff6ff / #dbeafe
  Red tint:    #fef2f2 / #fee2e2
  Green tint:  #f0fdf4 / #dcfce7
  Purple tint: #faf5ff / #ede9fe
  Orange tint: #fff7ed / #fed7aa
  Teal tint:   #f0fdfa / #ccfbf1
```

## Typography

```
font-family: 'Helvetica Neue', Helvetica, Arial, 'PingFang SC',
             'Microsoft YaHei', 'Microsoft JhengHei', 'SimHei', sans-serif
font-size:   14px labels, 12px sub-labels, 16px titles
font-weight: 400 normal, 600 semi-bold for titles
```

## Box Shapes

```xml
<!-- Standard node box -->
<rect rx="8" ry="8" fill="#ffffff" stroke="#d1d5db" stroke-width="1.5"/>

<!-- Icon accent box (colored background) -->
<rect rx="8" ry="8" fill="#eff6ff" stroke="#bfdbfe" stroke-width="1.5"/>

<!-- Database cylinder (use SVG path) -->
<!-- Terminal box: rx=4, fill=#111827, stroke=#374151 -->
<!-- User/actor: circle or rounded rect with icon -->
```

## Arrows

```xml
<defs>
  <marker id="arrow-blue" markerWidth="10" markerHeight="7"
          refX="9" refY="3.5" orient="auto">
    <polygon points="0 0, 10 3.5, 0 7" fill="#2563eb"/>
  </marker>
  <marker id="arrow-red" markerWidth="10" markerHeight="7"
          refX="9" refY="3.5" orient="auto">
    <polygon points="0 0, 10 3.5, 0 7" fill="#dc2626"/>
  </marker>
</defs>

<!-- Line -->
<line stroke="#2563eb" stroke-width="1.5" marker-end="url(#arrow-blue)"/>
<!-- Or path for curved/orthogonal routing -->
<path stroke="#2563eb" stroke-width="1.5" fill="none" marker-end="url(#arrow-blue)"/>
```

## Legend

Always include a legend in the bottom-left if multiple arrow colors are used:

```xml
<g transform="translate(20, 560)">
  <line x1="0" y1="8" x2="30" y2="8" stroke="#2563eb" stroke-width="1.5"
        marker-end="url(#arrow-blue)"/>
  <text x="36" y="12" fill="#6b7280" font-size="12">Agent flow</text>
  <line x1="0" y1="24" x2="30" y2="24" stroke="#dc2626" stroke-width="1.5"
        marker-end="url(#arrow-red)"/>
  <text x="36" y="28" fill="#6b7280" font-size="12">RAG flow</text>
</g>
```

## SVG Template

```xml
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 600"
     width="960" height="600">
  <style>
    /* NO @import — cairosvg / rsvg-convert cannot fetch external URLs */
    text { font-family: 'Helvetica Neue', Helvetica, Arial, 'PingFang SC', 'Microsoft YaHei', 'Microsoft JhengHei', 'SimHei', sans-serif; }
  </style>
  <defs>
    <!-- arrow markers here -->
    <!-- gradients/filters if needed -->
  </defs>
  <!-- white background -->
  <rect width="960" height="600" fill="#ffffff"/>
  <!-- diagram title (optional) -->
  <!-- nodes -->
  <!-- edges -->
  <!-- legend -->
</svg>
```
skills/fireworks-tech-graph/references/style-11-event-transit.md
# Style 11: Event Transit

A transit-map metaphor for event-driven systems. Topics are rails, processing
steps are stations, declared junctions are branch points, and dead letters get
a visibly separate route.

## Best fit

- Kafka/Pulsar/NATS topology reviews
- Event choreography and consumer-group discussions
- Stream processing, retry, and DLQ runbooks
- Schema evolution and materialized-view pipelines

## Visual tokens

| Token | Value |
|---|---|
| Canvas | `#fbf7ee` with transit dots |
| Card | `#fffdf8` |
| Primary rail | `#e4475b` |
| State projection | `#00897b` |
| Read/consumer | `#2563eb` |
| DLQ | `#c62828`, dashed |
| Text | `#17213c` |

The dark casing beneath a rail is decoration. The colored path with
`data-graph-role="edge"` remains the ordinary semantic connector. Semantic
rails use a maximum `2.8px` stroke and fixed user-space arrowheads so markers do
not grow with line width.

## Required semantic contract

Select `semantic_profile: "event-transit"` and provide:

- `diagram_type: "event_stream"`
- one to four topics, each with unique `id` and `color`
- every node: `transit_role` and optional/required topic metadata
- stations/junctions: `operation`
- consumers: `consumer_group`
- rail stations: unique integer `station_order`
- rail edges: `transit_type: "rail"`, topic id, `right → left` ports

A rail edge must connect consecutive station orders on one horizontal
centerline with at least `64px` clearance. A dead-letter edge must target a
real `dlq` node. Multiple rail departures are only valid from a `junction`.

## Composition rules

- One horizontal centerline per topic rail
- Use branches only where the node is declared as a junction
- Put DLQ and state-store terminals below their owning station
- Reserve dashed red for dead-letter/retry semantics
- Show partition, lag, or schema facts as short badges
- Zero bridge crossings and no duplicated rail overlay in official samples
- Keep at least `64px` clear rail between adjacent station cards
- Use compact user-space arrowheads and a subtle casing; the semantic rail stays at or below `2.8px`

## Signature checklist

- Dark top-right `EVENT METRO` line stamp and a signed topic-line count
- Numbered station medallions, compact midpoint chevrons, and one rail centerline per topic
- Distinct junction dot, dashed DLQ terminal with `×`, and state-store terminal with a square glyph
- Partition, schema, latency, fan-out, lag, replay, and state badges attached to stations

## Prompt cues

- English: `event metro map`, `topic rail map`, `Kafka topology`, `stream choreography map`
- 中文:`事件地铁图`、`事件轨道图`、`Topic 线路图`、`Kafka 拓扑图`
- Copyable cue: `Use Style 11 Event Transit; render topics as thin metro rails, processors as numbered stations, declared junctions, consumer groups, DLQ, and state projections.`

## Do not blend with

Do not nest Region/VPC deployment boundaries, label cards as C4 containers, or
attach golden-signal dashboards to every station. A request/response flow with
no topic, consumer-group, or stream evidence should use a generic flow style.

## Fixture

`fixtures/event-transit-style11.json` shows a checkout event line with schema
validation, fraud enrichment, a declared junction, DLQ, and materialized state.
skills/fireworks-tech-graph/fixtures/system-architecture-style6.json
{
  "schema_version": 1,
  "mode": "architecture",
  "template_type": "architecture",
  "style": 6,
  "motion_scene": "governed-runtime",
  "quality_profile": "showcase",
  "width": 960,
  "height": 700,
  "title": "System Architecture",
  "subtitle": "warm hierarchy for interface, orchestration, inference, safety, and operations",
  "containers": [
    { "id": "interface", "x": 40, "y": 120, "width": 880, "height": 110, "label": "Interface", "stroke": "#ded8cf", "fill": "none" },
    { "id": "core", "x": 40, "y": 280, "width": 880, "height": 130, "label": "Core Runtime", "stroke": "#ded8cf", "fill": "none" },
    { "id": "foundation", "x": 40, "y": 450, "width": 880, "height": 140, "label": "Foundation", "stroke": "#ded8cf", "fill": "none" }
  ],
  "nodes": [
    { "id": "client", "kind": "rect", "x": 80, "y": 156, "width": 180, "height": 54, "label": "Client Surface", "sublabel": "web + product UI", "fill": "#e9f1fb", "stroke": "#8c6f5a", "flat": true },
    { "id": "gateway", "kind": "double_rect", "x": 340, "y": 156, "width": 200, "height": 54, "label": "Gateway", "sublabel": "auth + routing", "fill": "#f7ecda", "stroke": "#d97757" },
    { "id": "planner", "kind": "rect", "x": 80, "y": 340, "width": 180, "height": 50, "label": "Task Planner", "sublabel": "decompose + schedule", "fill": "#f7ecda", "stroke": "#d0b893", "flat": true },
    { "id": "runtime", "kind": "double_rect", "x": 340, "y": 340, "width": 200, "height": 50, "label": "Model Runtime", "sublabel": "inference core", "fill": "#c8e4db", "stroke": "#8c6f5a" },
    { "id": "guardrails", "kind": "rect", "x": 740, "y": 340, "width": 160, "height": 50, "label": "Guardrails", "sublabel": "policy + safety", "fill": "#efe8de", "stroke": "#d0c3b3", "flat": true },
    { "id": "memory", "kind": "cylinder", "x": 90, "y": 510, "width": 160, "height": 60, "label": "Memory", "sublabel": "context + recall", "fill": "#e5e8df", "stroke": "#7b8b5c" },
    { "id": "tools", "kind": "rect", "x": 360, "y": 510, "width": 160, "height": 60, "label": "Tool Runtime", "sublabel": "actions", "fill": "#f7ecda", "stroke": "#d0b893", "flat": true },
    { "id": "observability", "kind": "rect", "x": 580, "y": 510, "width": 120, "height": 60, "label": "Observability", "sublabel": "traces", "fill": "#efe8de", "stroke": "#d0c3b3", "flat": true },
    { "id": "registry", "kind": "rect", "x": 760, "y": 510, "width": 120, "height": 60, "label": "Registry", "sublabel": "rollouts", "fill": "#efe8de", "stroke": "#d0c3b3", "flat": true }
  ],
  "arrows": [
    { "id": "request", "source": "client", "target": "gateway", "source_port": "right", "target_port": "left", "flow": "read", "motion_role": "ingress", "motion_stage": 1, "motion_order": 0, "label": "request" },
    { "id": "dispatch", "source": "gateway", "target": "runtime", "source_port": "bottom", "target_port": "top", "flow": "control", "motion_role": "dispatch", "motion_stage": 2, "motion_order": 0, "label": "dispatch" },
    { "id": "plan", "source": "runtime", "target": "planner", "source_port": "left", "target_port": "right", "flow": "control", "motion_role": "runtime-branch", "motion_stage": 3, "motion_order": 0, "label": "plan" },
    { "id": "enforce", "source": "runtime", "target": "guardrails", "source_port": "right", "target_port": "left", "flow": "control", "motion_role": "runtime-branch", "motion_stage": 3, "motion_order": 1, "label": "enforce" },
    { "id": "remember", "source": "planner", "target": "memory", "source_port": "bottom", "target_port": "top", "flow": "write", "motion_role": "foundation", "motion_stage": 4, "motion_order": 0, "label": "remember" },
    { "id": "act", "source": "runtime", "target": "tools", "source_port": "bottom", "target_port": "top", "flow": "read", "motion_role": "runtime-branch", "motion_stage": 3, "motion_order": 2, "label": "act" },
    { "id": "register", "source": "guardrails", "target": "registry", "source_port": "bottom", "target_port": "top", "flow": "feedback", "motion_role": "foundation", "motion_stage": 4, "motion_order": 1, "label": "release" },
    { "id": "trace", "source": "tools", "target": "observability", "source_port": "right", "target_port": "left", "flow": "feedback", "motion_role": "foundation", "motion_stage": 4, "motion_order": 2, "label": "trace" },
    { "id": "promote", "source": "observability", "target": "registry", "source_port": "right", "target_port": "left", "flow": "feedback", "motion_role": "promote", "motion_stage": 5, "motion_order": 0, "label": "promote" }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 625,
  "legend_locked": true,
  "legend": [
    { "flow": "read", "label": "request / action" },
    { "flow": "control", "label": "orchestration" },
    { "flow": "write", "label": "memory write" },
    { "flow": "feedback", "label": "operations" }
  ],
  "footer": "Style 6 · Claude Official · restrained system hierarchy",
  "footer_x": 48,
  "footer_y": 680
}
skills/fireworks-tech-graph/fixtures/tool-call-style2.json
{
  "schema_version": 1,
  "mode": "agent",
  "template_type": "agent",
  "style": 2,
  "motion_scene": "tool-grounding",
  "quality_profile": "showcase",
  "width": 960,
  "height": 720,
  "title": "Tool Call Flow",
  "subtitle": "retrieval, terminal execution, source grounding, and answer synthesis",
  "window_controls": true,
  "meta_center": "AGENT TOOL LOOP / GROUNDED EXECUTION",
  "meta_right": "STYLE-2 · DARK TERMINAL",
  "containers": [
    { "id": "request-path", "x": 40, "y": 120, "width": 880, "height": 110, "label": "Request Path" },
    { "id": "tooling-fabric", "x": 40, "y": 290, "width": 880, "height": 150, "label": "Tooling Fabric" },
    { "id": "source-grounding", "x": 40, "y": 480, "width": 880, "height": 140, "label": "Source Grounding" }
  ],
  "nodes": [
    { "id": "query", "kind": "speech", "x": 60, "y": 156, "width": 140, "height": 54, "label": "User Query", "fill": "#0f172a", "stroke": "#38bdf8", "flat": true },
    { "id": "retrieve", "kind": "double_rect", "x": 280, "y": 156, "width": 180, "height": 54, "label": "Retrieve Chunks", "sublabel": "select evidence", "fill": "#111827", "stroke": "#a855f7", "glow": "purple" },
    { "id": "generate", "kind": "rect", "x": 540, "y": 156, "width": 180, "height": 54, "label": "Generate Answer", "sublabel": "compose + cite", "fill": "#111827", "stroke": "#f97316" },
    { "id": "grounded", "kind": "speech", "x": 790, "y": 156, "width": 110, "height": 54, "label": "Grounded", "sublabel": "final reply", "fill": "#0f172a", "stroke": "#34d399", "flat": true, "glow": "green" },
    { "id": "agent", "kind": "double_rect", "x": 100, "y": 360, "width": 180, "height": 60, "label": "Agent", "sublabel": "plan + tool policy", "fill": "#111827", "stroke": "#34d399", "glow": "green" },
    { "id": "terminal", "kind": "terminal", "x": 390, "y": 360, "width": 180, "height": 60, "label": "Terminal", "fill": "#111111", "stroke": "#475569", "header_fill": "#222222" },
    { "id": "knowledge", "kind": "cylinder", "x": 700, "y": 360, "width": 180, "height": 60, "label": "Knowledge Base", "sublabel": "embeddings", "fill": "#0f172a", "stroke": "#38bdf8", "glow": "blue" },
    { "id": "documents", "kind": "folder", "x": 390, "y": 520, "width": 180, "height": 50, "label": "Source Documents", "fill": "#3f2a00", "stroke": "#f59e0b", "line_stroke": "#fcd34d" }
  ],
  "arrows": [
    { "id": "request", "source": "query", "target": "retrieve", "source_port": "right", "target_port": "left", "flow": "control", "motion_role": "ingress", "motion_stage": 1, "motion_order": 0, "label": "request" },
    { "id": "draft", "source": "retrieve", "target": "generate", "source_port": "right", "target_port": "left", "flow": "data", "motion_role": "grounding", "motion_stage": 6, "motion_order": 1, "label": "context" },
    { "id": "answer", "source": "generate", "target": "grounded", "source_port": "right", "target_port": "left", "flow": "feedback", "motion_role": "answer", "motion_stage": 7, "motion_order": 0, "label": "answer" },
    { "id": "delegate", "source": "retrieve", "target": "agent", "source_port": "bottom", "target_port": "top", "flow": "control", "motion_role": "delegate", "motion_stage": 2, "motion_order": 0, "label": "delegate" },
    { "id": "tool-call", "source": "agent", "target": "terminal", "source_port": "right", "target_port": "left", "flow": "read", "motion_role": "tool-call", "motion_stage": 3, "motion_order": 0, "label": "tool call" },
    { "id": "inspect", "source": "terminal", "target": "documents", "source_port": "bottom", "target_port": "top", "flow": "read", "motion_role": "inspect", "motion_stage": 4, "motion_order": 0, "label": "inspect" },
    { "id": "index", "source": "documents", "target": "knowledge", "source_port": "right", "target_port": "bottom", "flow": "write", "motion_role": "index", "motion_stage": 5, "motion_order": 0, "label": "index" },
    { "id": "ground", "source": "knowledge", "target": "generate", "source_port": "top", "target_port": "bottom", "flow": "data", "motion_role": "grounding", "motion_stage": 6, "motion_order": 0, "label": "ground" }
  ],
  "legend_orientation": "horizontal",
  "legend_x": 48,
  "legend_y": 650,
  "legend_locked": true,
  "legend": [
    { "flow": "control", "label": "control" },
    { "flow": "read", "label": "tool read" },
    { "flow": "write", "label": "index write" },
    { "flow": "data", "label": "grounding data" }
  ],
  "footer": "Style 2 · Dark Terminal · grounded tool execution",
  "footer_x": 48,
  "footer_y": 700
}
skills/fireworks-tech-graph/references/style-3-blueprint.md
# Style 3: Blueprint

Engineering blueprint aesthetic with grid background and technical annotation style.

## Colors

```
Background:     #0a1628
Grid color:     #112240  (subtle grid lines)
Panel fill:     #0d1f3c
Panel stroke:   #00b4d8  (cyan/teal)
Box radius:     2px  (sharp corners for technical feel)

Text primary:   #caf0f8  (light cyan)
Text secondary: #90e0ef
Text label:     #00b4d8
Text muted:     #48cae4 at 60% opacity

Accent colors:
  Cyan:    #00b4d8 / #48cae4
  White:   #ffffff (key labels)
  Orange:  #f77f00 (warnings/alerts)
  Green:   #06d6a0 (success/active)
```

## Background with Grid

```xml
<defs>
  <pattern id="grid" width="30" height="30" patternUnits="userSpaceOnUse">
    <path d="M 30 0 L 0 0 0 30" fill="none"
          stroke="#112240" stroke-width="0.5"/>
  </pattern>
</defs>
<rect width="960" height="600" fill="#0a1628"/>
<rect width="960" height="600" fill="url(#grid)" opacity="0.6"/>
```

## Typography

```
font-family: 'Courier New', 'Lucida Console', 'Microsoft YaHei', 'SimHei', monospace
font-size:   13px labels, 10px annotations, 16px title
font-weight: 400; titles use 700
text-transform: uppercase for section headers
letter-spacing: 0.05em
```

## Box Styles

```xml
<!-- Technical node box -->
<rect rx="2" ry="2" fill="#0d1f3c" stroke="#00b4d8" stroke-width="1"/>

<!-- Corner brackets instead of full border (engineering style) -->
<!-- Draw 4 L-shapes at corners instead of full rect -->

<!-- Dashed box (external/optional component) -->
<rect rx="2" fill="none" stroke="#00b4d8" stroke-width="1"
      stroke-dasharray="6,3"/>
```

## Arrows & Annotations

```xml
<defs>
  <marker id="arrow-cyan" markerWidth="8" markerHeight="6"
          refX="7" refY="3" orient="auto">
    <polygon points="0 0, 8 3, 0 6" fill="#00b4d8"/>
  </marker>
</defs>
<!-- Lines are sharp, orthogonal routing preferred -->
<polyline points="x1,y1 x2,y1 x2,y2"
          stroke="#00b4d8" stroke-width="1" fill="none"
          marker-end="url(#arrow-cyan)"/>

<!-- Annotation label on line -->
<text fill="#48cae4" font-size="10" text-anchor="middle">HTTP/REST</text>
```

## Title Block (bottom-right)

```xml
<!-- Blueprint title block -->
<rect x="700" y="530" width="240" height="60"
      fill="#0d1f3c" stroke="#00b4d8" stroke-width="1"/>
<line x1="700" y1="545" x2="940" y2="545"
      stroke="#00b4d8" stroke-width="0.5"/>
<text x="820" y="542" text-anchor="middle"
      fill="#caf0f8" font-size="10">SYSTEM ARCHITECTURE</text>
<text x="820" y="578" text-anchor="middle"
      fill="#00b4d8" font-size="13" font-weight="700">DIAGRAM TITLE</text>
```

## SVG Template

```xml
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 600"
     width="960" height="600">
  <style>
    text { font-family: 'Courier New', 'Lucida Console', 'Microsoft YaHei', 'SimHei', monospace; fill: #caf0f8; }
  </style>
  <defs>
    <pattern id="grid" width="30" height="30" patternUnits="userSpaceOnUse">
      <path d="M 30 0 L 0 0 0 30" fill="none" stroke="#112240" stroke-width="0.5"/>
    </pattern>
    <!-- arrow markers -->
  </defs>
  <rect width="960" height="600" fill="#0a1628"/>
  <rect width="960" height="600" fill="url(#grid)" opacity="0.6"/>
  <!-- nodes, edges, title block -->
</svg>
```
skills/fireworks-tech-graph/references/style-5-glassmorphism.md
# Style 5: Glassmorphism

Frosted glass cards on dark gradient. Designed for product sites, keynotes, and hero sections.

## Colors

```
Background gradient: #0d1117 → #161b22 → #0d1117 (diagonal)

Glass card:
  fill:           rgba(255,255,255,0.05)
  stroke:         rgba(255,255,255,0.15)
  backdrop-filter: blur(12px)  [SVG: use feGaussianBlur]
  box-radius:     12px

Text primary:   #f0f6fc  (near-white)
Text secondary: #8b949e  (muted)
Text gradient:  use linearGradient on text fill for hero labels

Accent glows (one per layer):
  Blue glow:    #58a6ff  / rgba(88,166,255,0.3)
  Purple glow:  #bc8cff  / rgba(188,140,255,0.3)
  Green glow:   #3fb950  / rgba(63,185,80,0.3)
  Orange glow:  #f78166  / rgba(247,129,102,0.3)
```

## Background

```xml
<defs>
  <linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
    <stop offset="0%"   stop-color="#0d1117"/>
    <stop offset="50%"  stop-color="#161b22"/>
    <stop offset="100%" stop-color="#0d1117"/>
  </linearGradient>
  <!-- Ambient glow spots -->
  <radialGradient id="glow-blue" cx="30%" cy="40%" r="40%">
    <stop offset="0%" stop-color="rgba(88,166,255,0.15)"/>
    <stop offset="100%" stop-color="rgba(88,166,255,0)"/>
  </radialGradient>
  <radialGradient id="glow-purple" cx="70%" cy="60%" r="35%">
    <stop offset="0%" stop-color="rgba(188,140,255,0.12)"/>
    <stop offset="100%" stop-color="rgba(188,140,255,0)"/>
  </radialGradient>
</defs>
<rect width="960" height="600" fill="url(#bg)"/>
<rect width="960" height="600" fill="url(#glow-blue)"/>
<rect width="960" height="600" fill="url(#glow-purple)"/>
```

## Glass Card Effect

SVG cannot do real backdrop-filter, so simulate with:

```xml
<defs>
  <filter id="glass-blur">
    <feGaussianBlur in="SourceGraphic" stdDeviation="0.5"/>
  </filter>
</defs>

<!-- Glass card: layered rects -->
<!-- 1. Subtle inner shadow -->
<rect rx="12" fill="rgba(255,255,255,0.03)" stroke="none"/>
<!-- 2. Glass body -->
<rect rx="12" fill="rgba(255,255,255,0.06)"
      stroke="rgba(255,255,255,0.15)" stroke-width="1"/>
<!-- 3. Top highlight line -->
<line stroke="rgba(255,255,255,0.25)" stroke-width="1"/>
```

## Typography

```
font-family: 'Inter', -apple-system, 'SF Pro Display', 'PingFang SC', 'Microsoft YaHei', 'Microsoft JhengHei', 'SimHei', sans-serif
font-size:   14px labels, 12px sublabels, 20px hero title
font-weight: 400 normal, 600 semi-bold, 700 bold titles
```

## Gradient Text (for hero labels)

```xml
<defs>
  <linearGradient id="text-grad-blue" x1="0%" y1="0%" x2="100%" y2="0%">
    <stop offset="0%"   stop-color="#58a6ff"/>
    <stop offset="100%" stop-color="#bc8cff"/>
  </linearGradient>
</defs>
<text fill="url(#text-grad-blue)" font-weight="700" font-size="20">
  AI Pipeline
</text>
```

## Arrows

```xml
<defs>
  <marker id="arrow-blue" markerWidth="8" markerHeight="6"
          refX="7" refY="3" orient="auto">
    <polygon points="0 0, 8 3, 0 6" fill="#58a6ff"/>
  </marker>
</defs>
<!-- Slightly glowing line -->
<path stroke="#58a6ff" stroke-width="1.5" fill="none"
      opacity="0.8" marker-end="url(#arrow-blue)"/>
```

## SVG Template

```xml
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 600"
     width="960" height="600">
  <style>
    @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
    text { font-family: 'Inter', -apple-system, 'SF Pro Display', 'PingFang SC', 'Microsoft YaHei', 'Microsoft JhengHei', 'SimHei', sans-serif; fill: #f0f6fc; }
  </style>
  <defs>
    <!-- bg gradients, glow gradients, glass filter, arrow markers -->
  </defs>
  <!-- background layers -->
  <!-- glass cards (nodes) -->
  <!-- glowing edges -->
  <!-- labels with gradient text for heroes -->
</svg>
```
skills/fireworks-tech-graph/references/style-6-claude-official.md
# Style 6: Claude Official

Inspired by Anthropic's Claude blog technical diagrams — warm, approachable, professional.

```
Background:     #f8f6f3  (warm cream)
Box fill:
  - Blue tint:   #a8c5e6  (alert/input nodes)
  - Green tint:  #9dd4c7  (agent nodes)
  - Beige:       #f4e4c1  (infrastructure/bus)
  - Gray tint:   #e8e6e3  (storage/state)
Box stroke:     #4a4a4a  (dark gray)
Box radius:     12px
Text primary:   #1a1a1a  (near black)
Text secondary: #6a6a6a  (medium gray)
Text labels:    #5a5a5a  (arrow labels)

Semantic colors:
  Input/Source:    #a8c5e6  (soft blue)
  Agent/Process:   #9dd4c7  (soft teal-green)
  Infrastructure:  #f4e4c1  (warm beige)
  Storage/State:   #e8e6e3  (light gray)

Arrow color:     #5a5a5a  (consistent dark gray)
```

## Typography

```
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue',
             Arial, 'PingFang SC', 'Microsoft YaHei', 'Microsoft JhengHei', 'SimHei', sans-serif
font-size:   16px node labels, 14px descriptions, 13px arrow labels
font-weight: 600 for node labels, 400 for descriptions, 700 for titles
```

## Box Shapes

```xml
<!-- Agent node (teal-green) -->
<rect rx="12" ry="12" fill="#9dd4c7" stroke="#4a4a4a" stroke-width="2.5"/>

<!-- Input/Source node (soft blue) -->
<rect rx="12" ry="12" fill="#a8c5e6" stroke="#4a4a4a" stroke-width="2.5"/>

<!-- Infrastructure node (warm beige) -->
<rect rx="12" ry="12" fill="#f4e4c1" stroke="#4a4a4a" stroke-width="2.5"/>

<!-- Storage/State node (light gray) -->
<rect rx="12" ry="12" fill="#e8e6e3" stroke="#4a4a4a" stroke-width="2.5"/>
```

## Arrows

```xml
<defs>
  <marker id="arrow-claude" markerWidth="8" markerHeight="8"
          refX="7" refY="4" orient="auto">
    <polygon points="0 0, 8 4, 0 8" fill="#5a5a5a"/>
  </marker>
</defs>

<!-- Arrow line -->
<line stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>

<!-- Or use simple line without arrowhead for cleaner look -->
<line stroke="#5a5a5a" stroke-width="2"/>
```

## Arrow Semantics

Use different arrow styles to convey meaning:

| Flow Type | Color | Stroke | Dash | Usage |
|-----------|-------|--------|------|-------|
| Primary data flow | #5a5a5a | 2px solid | none | Main request/response path |
| Memory write | #5a5a5a | 2px | `5,3` | Write/store operations |
| Memory read | #5a5a5a | 2px solid | none | Retrieval from store |
| Control/trigger | #5a5a5a | 1.5px | `3,2` | Event triggers |

```xml
<!-- Solid arrow for reads -->
<line stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>

<!-- Dashed arrow for writes -->
<line stroke="#5a5a5a" stroke-width="2" stroke-dasharray="5,3" marker-end="url(#arrow-claude)"/>
```

## Arrow Labels

Arrow labels should be **technical and specific**, positioned mid-arrow:

```xml
<text x="..." y="..." fill="#5a5a5a" font-size="13" text-anchor="middle">
  store(embedding)
</text>
```

Good labels: `query(text)`, `retrieve(top_k=5)`, `embed(768d)`, `POST /api/search`
Avoid vague labels: "Process", "Send", "Get"

## Node Content Guidelines

Node content should include **technical details**, not just concepts:

**Good examples:**
- "Vector Store" → "Vector Store" + "• 768-dim embeddings" + "• Cosine similarity"
- "LLM" → "GPT-4" + "• 8K context" + "• Temperature: 0.7"
- "Memory" → "Redis Cache" + "• TTL: 5min" + "• Max: 4K tokens"

**Avoid vague descriptions:**
- "Process data" → specify what processing
- "Store information" → specify storage type and format
- "Handle requests" → specify request type and method

Use 2-3 lines per node:
1. Component name (bold, 16px)
2. Technical detail or implementation (14px)
3. Key parameter or constraint (14px, optional)

## Layer Labels

For multi-layer architectures, add layer labels on the left side:

```xml
<text x="30" y="130" fill="#6a6a6a" font-size="14" font-weight="600">Input</text>
<text x="30" y="290" fill="#6a6a6a" font-size="14" font-weight="600">Processing</text>
<text x="30" y="490" fill="#6a6a6a" font-size="14" font-weight="600">Storage</text>
```

Position at the vertical center of each layer.

## Legend Requirement

When using 2+ arrow types or colors, include a legend in the bottom-right corner:

```xml
<rect x="720" y="500" width="220" height="80" rx="8" ry="8"
      fill="#ffffff" stroke="#4a4a4a" stroke-width="1.5"/>
<text x="735" y="522" fill="#1a1a1a" font-size="13" font-weight="600">Legend</text>

<!-- Legend items -->
<line x1="735" y1="540" x2="765" y2="540" stroke="#5a5a5a" stroke-width="2"/>
<text x="775" y="545" fill="#6a6a6a" font-size="12">Read operation</text>

<line x1="735" y1="560" x2="765" y2="560" stroke="#5a5a5a" stroke-width="2" stroke-dasharray="5,3"/>
<text x="775" y="565" fill="#6a6a6a" font-size="12">Write operation</text>
```

Position: bottom-right, 20px margin from edges.

## Layout Principles

- **Generous spacing**: Minimum 80px between node edges
- **Horizontal alignment**: Same-layer nodes align perfectly
- **Vertical flow**: Top-to-bottom preferred
- **Symmetry**: Balanced composition
- **Clean lines**: Orthogonal routing — vertical then horizontal, or vice versa

## SVG Template

```xml
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 600"
     width="960" height="600">
  <style>
    text { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI',
                   'Helvetica Neue', Arial, 'PingFang SC', 'Microsoft YaHei', 'Microsoft JhengHei', 'SimHei', sans-serif;
    }
  </style>
  <defs>
    <marker id="arrow-claude" markerWidth="8" markerHeight="8"
            refX="7" refY="4" orient="auto">
      <polygon points="0 0, 8 4, 0 8" fill="#5a5a5a"/>
    </marker>
    <filter id="shadow-soft">
      <feDropShadow dx="0" dy="2" stdDeviation="6" flood-color="#00000008"/>
    </filter>
  </defs>

  <!-- Warm cream background -->
  <rect width="960" height="600" fill="#f8f6f3"/>

  <!-- Title (optional) -->
  <text x="480" y="40" text-anchor="middle" fill="#1a1a1a"
        font-size="20" font-weight="700">Diagram Title</text>

  <!-- Nodes -->
  <!-- Agent node example -->
  <rect x="100" y="100" width="180" height="80" rx="12" ry="12"
        fill="#9dd4c7" stroke="#4a4a4a" stroke-width="2.5"
        filter="url(#shadow-soft)"/>
  <text x="190" y="145" text-anchor="middle" fill="#1a1a1a"
        font-size="16" font-weight="600">Agent name</text>

  <!-- Edges -->
  <line x1="190" y1="180" x2="190" y2="240"
        stroke="#5a5a5a" stroke-width="2" marker-end="url(#arrow-claude)"/>
  <text x="210" y="215" fill="#5a5a5a" font-size="13">Publish</text>
</svg>
```

## Design Philosophy

Claude's official style emphasizes:
- **Warmth**: Cream background, soft fills
- **Clarity**: High contrast text, generous spacing
- **Professionalism**: Consistent stroke weights, aligned elements
- **Approachability**: Rounded corners, friendly colors

Avoid:
- Harsh shadows or gradients
- Overly saturated colors
- Thin stroke weights (< 2px)
- Cluttered layouts
skills/fireworks-tech-graph/references/style-7-openai.md
# Style 7: OpenAI Official

Clean, modern aesthetic matching OpenAI's documentation and research diagrams — minimal but precise.

## Color Palette

```
Background:     #ffffff  (pure white)
Primary text:   #0d0d0d  (near black)
Secondary text: #6e6e80  (muted gray)
Border:         #e5e5e5  (light gray)

Accent colors (reserved):
Green accent:   #10a37f  (OpenAI brand green)
Blue accent:    #1d4ed8  (links, actions)
Orange accent:  #f97316  (highlights, warnings)
Gray accent:    #71717a  (secondary elements)
```

## Typography

```
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica Neue, 'PingFang SC', 'Microsoft YaHei', 'Microsoft JhengHei', 'SimHei', sans-serif
font-size:   16px node labels, 13px descriptions, 12px arrow labels
font-weight: 600 for titles, 500 for labels, 400 for descriptions
letter-spacing: 0
```

No custom fonts. System font stack only for maximum compatibility.

## Node Boxes

Clean, minimal boxes with subtle borders:

```xml
<!-- Standard node -->
<rect x="100" y="100" width="180" height="80" rx="8" ry="8"
      fill="#ffffff" stroke="#e5e5e5" stroke-width="1.5"/>

<!-- Accent node (with green left border) -->
<rect x="100" y="100" width="180" height="80" rx="8" ry="8"
      fill="#ffffff" stroke="#e5e5e5" stroke-width="1.5"/>
<rect x="100" y="100" width="4" height="80" rx="2" ry="2"
      fill="#10a37f"/>
```

**Key techniques:**
1. White fill with light gray border (no shadows)
2. Optional colored left-border accent strip (4px wide)
3. `rx="8"` for subtle rounding
4. `stroke-width: 1.5` — thin, precise borders
5. No gradients, no shadows, no decorative elements

## Arrows

Thin, precise arrows with subtle colors:

```xml
<defs>
  <!-- Default arrow (gray) -->
  <marker id="arrow-oai" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
    <polygon points="0 0, 10 3.5, 0 7" fill="#71717a"/>
  </marker>

  <!-- Accent arrow (green) -->
  <marker id="arrow-oai-green" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
    <polygon points="0 0, 10 3.5, 0 7" fill="#10a37f"/>
  </marker>
</defs>

<!-- Default connection -->
<line x1="280" y1="140" x2="400" y2="140"
      stroke="#71717a" stroke-width="1.5" marker-end="url(#arrow-oai)"/>

<!-- Accent connection -->
<line x1="280" y1="140" x2="400" y2="140"
      stroke="#10a37f" stroke-width="1.5" marker-end="url(#arrow-oai-green)"/>
```

**Arrow guidelines:**
- Prefer straight lines (orthogonal routing with right angles)
- `stroke-width: 1.5` — thin and precise
- Filled polygon arrowheads (not stroke-based)
- Gray for default, green for primary/accent flows
- Dashed (`stroke-dasharray="4,3"`) for optional/async flows

## Arrow Labels

Minimal, small labels:

```xml
<text x="340" y="133" text-anchor="middle" fill="#6e6e80" font-size="12">
  label
</text>
```

Labels should be:
- 12px, gray color (`#6e6e80`)
- No background rect by default (white background is usually sufficient); add a white fallback rect only when offsetting cannot prevent a collision
- Short, technical language
- Midpoint of arrow

## Database Shapes

Clean cylinders with thin borders:

```xml
<ellipse cx="200" cy="100" rx="50" ry="12" fill="#ffffff" stroke="#e5e5e5" stroke-width="1.5"/>
<path d="M 150,100 L 150,140 Q 200,155 250,140 L 250,100"
      fill="#ffffff" stroke="#e5e5e5" stroke-width="1.5"/>
<ellipse cx="200" cy="140" rx="50" ry="12" fill="none" stroke="#e5e5e5" stroke-width="1.5"/>
```

## Grouping Containers

Dashed rect containers for logical grouping:

```xml
<rect x="80" y="80" width="400" height="200" rx="8" ry="8"
      fill="none" stroke="#e5e5e5" stroke-width="1" stroke-dasharray="4,3"/>
<text x="90" y="97" fill="#6e6e80" font-size="12" font-weight="500">
  Group Label
</text>
```

## Node Content

Clean, minimal text layout:

```xml
<rect x="100" y="100" width="180" height="80" rx="8" ry="8"
      fill="#ffffff" stroke="#e5e5e5" stroke-width="1.5"/>
<text x="190" y="130" text-anchor="middle" fill="#0d0d0d"
      font-size="16" font-weight="600">
  Component Name
</text>
<text x="190" y="150" text-anchor="middle" fill="#6e6e80"
      font-size="13">
  Brief description
</text>
```

**Content guidelines:**
- 1-2 lines per box
- Main label: 16px, font-weight: 600, near-black
- Description: 13px, font-weight: 400, gray
- Center-aligned text within boxes

## Layout Principles

**Precise, grid-aligned layout:**
- Snap all coordinates to 8px grid
- Consistent 100px horizontal spacing
- Consistent 120px vertical spacing
- Generous whitespace (40px+ margins)
- No decorative elements

**OpenAI minimalism:**
- Only use color when semantically meaningful (brand green for primary flow)
- White boxes on white background — differentiation through border and label only
- Avoid: shadows, gradients, patterns, icons, decorative elements
- Prefer: straight lines, orthogonal routing, thin strokes

## SVG Template

```xml
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 600" width="960" height="600">
  <style>
    text { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica Neue, 'PingFang SC', 'Microsoft YaHei', 'Microsoft JhengHei', 'SimHei', sans-serif; }
  </style>
  <defs>
    <marker id="arrow-oai" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
      <polygon points="0 0, 10 3.5, 0 7" fill="#71717a"/>
    </marker>
    <marker id="arrow-oai-green" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
      <polygon points="0 0, 10 3.5, 0 7" fill="#10a37f"/>
    </marker>
  </defs>

  <!-- White background -->
  <rect width="960" height="600" fill="#ffffff"/>

  <!-- Title -->
  <text x="480" y="30" text-anchor="middle" fill="#0d0d0d"
        font-size="20" font-weight="600">Diagram Title</text>

  <!-- Standard node -->
  <rect x="100" y="70" width="180" height="80" rx="8" ry="8"
        fill="#ffffff" stroke="#e5e5e5" stroke-width="1.5"/>
  <text x="190" y="100" text-anchor="middle" fill="#0d0d0d"
        font-size="16" font-weight="600">Component</text>
  <text x="190" y="120" text-anchor="middle" fill="#6e6e80"
        font-size="13">Description</text>

  <!-- Connection -->
  <line x1="280" y1="110" x2="400" y2="110"
        stroke="#71717a" stroke-width="1.5" marker-end="url(#arrow-oai)"/>
  <text x="340" y="103" text-anchor="middle" fill="#6e6e80" font-size="12">label</text>
</svg>
```

## Design Philosophy

OpenAI Official style emphasizes:
- **Minimalism**: White on white, only essential visual elements
- **Precision**: Thin strokes, sharp corners (rx=8), grid-aligned
- **Clarity**: Content-first, no visual noise
- **Brand consistency**: `#10a37f` green used sparingly for primary flows

Avoid:
- Shadows and gradients
- Colorful fills (white fill only)
- Thick borders (>2px)
- Decorative elements (icons, patterns, textures)
- Custom fonts (system stack only)
skills/fireworks-tech-graph/references/png-export.md
# PNG Export

Load this reference only when the bundled `generate-diagram.sh` fallback is insufficient or a specific renderer must be selected.

## Renderer Choice

| Tool | Install | Use when |
| --- | --- | --- |
| CairoSVG | `python3 -m pip install cairosvg` | Default; good CSS support |
| `rsvg-convert` | `brew install librsvg` or `apt install librsvg2-bin` | Simple SVGs without complex CSS |
| Puppeteer | Node.js plus Chromium | Browser-generated SVG, CJK/emoji fallback, or pixel fidelity |

Prefer the bundled validation/export entry point:

```bash
SKILL_ROOT="${CLAUDE_SKILL_DIR:-/absolute/path/from-codex-skill-metadata}"
"$SKILL_ROOT/scripts/generate-diagram.sh" -t architecture -s 1 -o ./diagram.svg -w 1920
```

## Manual CairoSVG

```bash
python3 -c "import cairosvg; cairosvg.svg2png(url='input.svg', write_to='output.png', output_width=1920)"
```

## Manual librsvg

```bash
rsvg-convert -w 1920 input.svg -o output.png
```

## Puppeteer

Install Puppeteer outside the user's project and run the bundled converter:

```bash
SKILL_ROOT="${CLAUDE_SKILL_DIR:-/absolute/path/from-codex-skill-metadata}"
npm install --prefix /tmp/fireworks-tech-graph-puppeteer puppeteer
NODE_PATH=/tmp/fireworks-tech-graph-puppeteer/node_modules node "$SKILL_ROOT/scripts/svg2png.js" ./output
```

## Known Limits

- `rsvg-convert` can omit complex CSS, filters, and `<foreignObject>` content.
- CairoSVG can miss CJK or emoji glyphs when the selected system font lacks them.
- Use Puppeteer when browser rendering is required; its viewport path preserves the SVG dimensions more reliably than a raw Chrome `--window-size` screenshot.
skills/fireworks-tech-graph/references/style-12-ops-pulse.md
# Style 12: Ops Pulse

An operational review surface that combines service topology, the four golden
signals, one critical request path, and a correlated trace waterfall. It is
for incident and reliability work, not a screenshot of a live dashboard.

## Best fit

- Incident reviews and production-readiness reviews
- SLO/error-budget discussions
- Latency and dependency investigations
- Runbooks that connect topology to one representative trace

## Visual tokens

| Token | Value |
|---|---|
| Canvas | `#07111f` with a restrained ops grid |
| Service card | `#0d1b2a` |
| Border | `#29435d` |
| Healthy | `#22c55e` |
| Warning | `#f59e0b` |
| Critical | `#f43f5e` |
| Telemetry | `#22d3ee`, dashed |
| Trace | `#38bdf8` |

## Required semantic contract

Select `semantic_profile: "ops-pulse"` and provide:

- `diagram_type: "observability"`
- one to twelve nodes with `ops_role: "service"`
- each service: exactly `latency`, `traffic`, `errors`, and `saturation`
- every signal: `value`, `unit`, `window`, and `status`
- service cards at least `180×108` and a visible `status_label`
- a non-empty ordered `critical_path` of business edge ids
- business and telemetry edges using different `flow` tokens
- trace spans with positive duration and valid parent coverage

The critical path must be contiguous and may not include telemetry edges. A
trace waterfall has exactly one root when spans are present.

## Composition rules

- Keep service health cards in one aligned band
- Show only one highlighted critical path per view
- Telemetry export is dashed and visually subordinate
- Put the trace waterfall in its own boundary below topology
- Do not duplicate a critical edge to create glow; glow is decoration owned by
  the single semantic path
- Use a fixed observation window and display units on every signal
- Zero bridge crossings and no more than two bends per edge

## Signature checklist

- Top-right `LIVE` investigation stamp with observation window and worst status
- Status rail on every service card and four metric chips carrying explicit windows
- Numbered critical-hop markers on the single highlighted business path
- Dedicated trace boundary with a 0–100% ruler and one correlated waterfall

## Prompt cues

- English: `reliability pulse`, `incident investigation view`, `golden signals trace`, `SRE trace review`
- 中文:`可靠性脉冲`、`事故排查视图`、`黄金信号追踪图`、`SRE Trace 评审`
- Copyable cue: `Use Style 12 Ops Pulse; show a fixed observation window, four golden signals per service, one numbered critical path, telemetry export, and one correlated trace waterfall.`

## Do not blend with

Do not turn the service band into a Region/VPC deployment map, an event-metro
rail, or a C4 responsibility review. If the prompt lacks measured signals,
status, time window, and trace evidence, use a generic architecture style.

## Fixture

`fixtures/ops-pulse-style12.json` demonstrates a degraded checkout path with
four service cards, an OTel collector, and a correlated four-span trace.
skills/fireworks-tech-graph/references/style-8-dark-luxury.md
# Style 8 — Dark Luxury

Inspired by the `dark-gold` theme from [Understand-Anything](https://github.com/nicholasgasior/understand-anything).
Deep black canvas with warm champagne-gold accents and hybrid serif/sans typography.
Uniquely warm among all dark styles — closest peer is style-2 (Dark Terminal) but with a premium editorial feel.

## Color Tokens

| Token | Value | Usage |
|-------|-------|-------|
| Background | `#0a0a0a` | Canvas fill (deepest black, no blue tint) |
| Surface | `#111111` | Node / panel fill |
| Elevated | `#1a1a1a` | Secondary panels, sub-headers |
| Accent gold | `#d4a574` | Primary arrows, titles, borders, cluster containers |
| Accent dim | `#c9a96e` | Muted gold borders, secondary accents, cluster labels |
| Accent bright | `#e8c49a` | Highlights, selected state stroke |
| Text primary | `#f5f0eb` | Main labels (warm near-white) |
| Text secondary | `#a39787` | Sub-labels, descriptions |
| Text muted | `#6b5f53` | Annotations, fine print |

## Typography

```
Title / Section label:  Georgia, 'Times New Roman', serif
                        font-size: 21px (diagram title), 14px (section labels)
                        font-weight: 700
                        fill: #f5f0eb / #c9a96e (gold for section headers)

Node name:              -apple-system, 'Helvetica Neue', Arial, 'PingFang SC', sans-serif
                        font-size: 13-14px, font-weight: 600, fill: <bucket color>

Sub-label / detail:     sans-serif, font-size: 10-11px, fill: #a39787 or #6b5f53

Arrow label / legend:   sans-serif, font-size: 10-11px, fill: #a39787

Code / path text:       'Cascadia Code', 'SF Mono', 'Courier New', monospace
                        font-size: 10-11px, fill: #a39787
```

**Rule**: Georgia serif is used ONLY for diagram titles and cluster/section labels (≥14px).
All node names, arrow labels, and fine-print use sans-serif. This prevents CJK readability issues
at small sizes while preserving the luxury editorial hook where it's most visible.

## Node Semantic Color Buckets

Six buckets cover the full color wheel — no two adjacent bucket colors are similar:

| Bucket | Border Color | Use For |
|--------|-------------|---------|
| Code / Logic | `#5a9e6f` (sage green) | functions, classes, modules, algorithms |
| Service / API | `#a78bfa` (soft violet) | services, endpoints, APIs, gateways |
| Data / Storage | `#38bdf8` (sky blue) | databases, tables, files, caches |
| Concept / Domain | `#f87171` (soft rose) | concepts, domains, entities, topics |
| Infra / Config | `#fbbf24` (amber yellow) | infrastructure, config, scripts, pipelines |
| Meta / Doc | `#94a3b8` (cool gray) | documents, schemas, resources, sources |

All nodes share: `rx="6"` rounded rect, `fill="#111111"`, `stroke-width="1.5"`, stroke color matches bucket.

### SVG node example (Code / Logic):

```xml
<rect x="60" y="100" width="260" height="52" rx="6"
      fill="#111111" stroke="#5a9e6f" stroke-width="1.5"/>
<text x="72" y="122" font-family="-apple-system,'Helvetica Neue',Arial,sans-serif"
      font-size="13" font-weight="600" fill="#5a9e6f">MyComponent</text>
<text x="72" y="138" font-family="-apple-system,sans-serif"
      font-size="10" fill="#a39787">React component · state management</text>
```

## Arrow System

| Flow Type | Color | Stroke | Dash | Marker |
|-----------|-------|--------|------|--------|
| Primary / structural | `#d4a574` (gold) | 2px solid | none | gold arrowhead |
| Data flow | `#6ee7b7` (mint) | 1.5px solid | none | mint arrowhead |
| Control / trigger | `#fdba74` (amber-orange) | 1.5px solid | none | orange arrowhead |
| Reference / semantic | `#a39787` (warm muted) | 1px dashed | `4,3` | muted arrowhead |
| Dependency | `#a78bfa` (violet) | 1px dashed | `6,3` | violet arrowhead |
| Feedback / loop | `#d4a574` (gold) | 1.5px curved | — | gold arrowhead |

### SVG arrow markers (add to `<defs>`):

```xml
<defs>
  <!-- Gold — primary structural arrows -->
  <marker id="arr-gold" markerWidth="10" markerHeight="7"
          refX="9" refY="3.5" orient="auto">
    <polygon points="0 0,10 3.5,0 7" fill="#d4a574"/>
  </marker>

  <!-- Amber-orange — control/trigger -->
  <marker id="arr-orange" markerWidth="8" markerHeight="6"
          refX="7" refY="3" orient="auto">
    <polygon points="0 0,8 3,0 6" fill="#fdba74"/>
  </marker>

  <!-- Sky blue — data/storage references -->
  <marker id="arr-blue" markerWidth="10" markerHeight="7"
          refX="9" refY="3.5" orient="auto">
    <polygon points="0 0,10 3.5,0 7" fill="#38bdf8"/>
  </marker>

  <!-- Gray — muted/optional paths -->
  <marker id="arr-gray" markerWidth="8" markerHeight="6"
          refX="7" refY="3" orient="auto">
    <polygon points="0 0,8 3,0 6" fill="#94a3b8"/>
  </marker>
</defs>
```

### Arrow label placement

Use **offset-first**: place label 6–8px above horizontal arrows (not centered on the line).
Add background rect (`fill="#0a0a0a" opacity="0.9"`) only when the label still collides with another element.

## Container / Cluster Style

```xml
<!-- Layer cluster container -->
<rect x="40" y="80" width="880" height="140" rx="8"
      fill="none"
      stroke="#d4a574" stroke-width="0.5"
      stroke-dasharray="6,4" opacity="0.4"/>

<!-- Cluster label: top-left, Georgia serif, gold muted -->
<text x="52" y="98"
      font-family="Georgia,'Times New Roman',serif"
      font-size="11" font-weight="700"
      fill="#c9a96e" opacity="0.7">LAYER NAME</text>
```

## Background Treatment

No gradient needed — depth comes from contrast levels:

1. Canvas `#0a0a0a` — absolute black floor
2. Node surface `#111111` — first elevation (+1 stop)
3. Panel/sub-header `#1a1a1a` — second elevation (+2 stops)
4. Gold `#d4a574` — the only warmth; draws the eye to structure

Optional: subtle ambient radial glow around the central cluster
```xml
<radialGradient id="glow" cx="50%" cy="50%" r="30%">
  <stop offset="0%" stop-color="#d4a574" stop-opacity="0.04"/>
  <stop offset="100%" stop-color="#d4a574" stop-opacity="0"/>
</radialGradient>
<rect width="960" height="600" fill="url(#glow)"/>
```

## Full `<style>` Block

```xml
<style>
  text { font-family: -apple-system,"Helvetica Neue",Arial,"PingFang SC",sans-serif; }
  .ttl { font-family: Georgia,"Times New Roman",serif;
         font-size: 21px; font-weight: 700; fill: #f5f0eb; }
  .lbl { font-family: Georgia,"Times New Roman",serif;
         font-size: 11px; font-weight: 700; fill: #c9a96e; opacity: 0.7; }
  .nm  { font-size: 13px; font-weight: 600; }    /* node name — fill set per bucket */
  .sm  { font-size: 10px; fill: #a39787; }        /* sub-label */
  .xs  { font-size:  9px; fill: #6b5f53; }        /* fine print */
  .al  { font-size: 10px; fill: #8c7e72; }        /* arrow label */
  .fn  { font-family: "Cascadia Code","SF Mono","Courier New",monospace;
         font-size: 10px; fill: #a39787; }        /* code / path text */
</style>
```

## ViewBox Recommendations

| Diagram | ViewBox |
|---------|---------|
| Standard architecture | `0 0 960 600` |
| Tall pipeline / flow | `0 0 960 820` |
| Wide multi-layer | `0 0 1200 600` |

## Comparable Styles

| Style | Background | Accent | Typography |
|-------|-----------|--------|-----------|
| Style 2 Dark Terminal | `#0f0f1a` (blue-black) | `#00ff88` (neon green) | monospace throughout |
| **Style 8 Dark Luxury** | `#0a0a0a` (pure black) | `#d4a574` (champagne gold) | serif titles + sans body |
| Style 3 Blueprint | `#0a1628` (navy) | `#4fc3f7` (cyan) | sans-serif |

**When to choose Style 8**: architecture/pipeline docs where you want editorial gravitas —
README hero images, conference slides, knowledge-base diagrams, premium product docs.
skills/fireworks-tech-graph/references/style-2-dark-terminal.md
# Style 2: Dark Terminal

Neon-on-dark hacker aesthetic. Matches CLAUDE.md standard tech diagram style.

## Colors

```
Background:     #0f0f1a  (near-black)
Panel fill:     #0f172a  (slate-950)
Panel stroke:   #334155  (slate-700)
Box radius:     6px

Text primary:   #e2e8f0  (slate-200)
Text secondary: #94a3b8  (slate-400)
Text muted:     #475569  (slate-600)

Accent palette (use per theme/layer):
  Purple:   #7c3aed / #a855f7
  Orange:   #ea580c / #f97316
  Blue:     #1d4ed8 / #3b82f6
  Green:    #059669 / #10b981
  Gold:     #eab308
  Red:      #dc2626 / #ef4444

Arrow colors: match accent of the source node's theme
```

## Background Gradient

```xml
<defs>
  <linearGradient id="bg-grad" x1="0%" y1="0%" x2="100%" y2="100%">
    <stop offset="0%" stop-color="#0f0f1a"/>
    <stop offset="100%" stop-color="#1a1a2e"/>
  </linearGradient>
</defs>
<rect width="960" height="600" fill="url(#bg-grad)"/>
```

## Typography

```
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', 'Courier New', 'Microsoft YaHei', 'SimHei', monospace
font-size:   13px labels, 11px sub-labels, 15px titles
font-weight: 400 normal, 700 bold for section headers
letter-spacing: 0.02em for labels
```

## Box Styles

```xml
<!-- Standard panel -->
<rect rx="6" ry="6" fill="#0f172a" stroke="#334155" stroke-width="1"/>

<!-- Colored accent box (themed by function) -->
<rect rx="6" ry="6" fill="#1e1b4b" stroke="#7c3aed" stroke-width="1.5"/>
<!-- Purple for AI/ML nodes -->
<!-- #1c1917 / #ea580c for compute/API nodes -->
<!-- #052e16 / #059669 for storage/DB nodes -->
<!-- #1e3a5f / #3b82f6 for network/gateway nodes -->
```

## Glow Effect (optional, for key nodes)

```xml
<defs>
  <filter id="glow-purple">
    <feGaussianBlur stdDeviation="3" result="blur"/>
    <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
  </filter>
</defs>
<rect filter="url(#glow-purple)" stroke="#a855f7" .../>
```

## Arrows

```xml
<defs>
  <marker id="arrow-purple" markerWidth="8" markerHeight="6"
          refX="7" refY="3" orient="auto">
    <polygon points="0 0, 8 3, 0 6" fill="#a855f7"/>
  </marker>
</defs>
<path stroke="#a855f7" stroke-width="1.5" stroke-dasharray="none"
      fill="none" marker-end="url(#arrow-purple)"/>
```

## SVG Template

```xml
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 600"
     width="960" height="600">
  <style>
    text { font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', 'Courier New', 'Microsoft YaHei', 'SimHei', monospace; fill: #e2e8f0; }
  </style>
  <defs>
    <linearGradient id="bg-grad" x1="0%" y1="0%" x2="100%" y2="100%">
      <stop offset="0%" stop-color="#0f0f1a"/>
      <stop offset="100%" stop-color="#1a1a2e"/>
    </linearGradient>
    <!-- arrow markers -->
    <!-- glow filters -->
  </defs>
  <rect width="960" height="600" fill="url(#bg-grad)"/>
  <!-- nodes, edges, legend -->
</svg>
```
skills/fireworks-tech-graph/references/style-10-cloud-fabric.md
# Style 10: Cloud Fabric

A deployment-topology view for regions, VPCs/networks, compute, data stores,
and cross-boundary traffic. It answers “where does this run?” without turning
the diagram into a vendor icon poster.

## Best fit

- Multi-region and disaster-recovery reviews
- Cloud deployment and network ownership maps
- Kubernetes cluster/namespace placement
- Migration plans and platform boundary discussions

## Visual tokens

| Token | Value |
|---|---|
| Canvas | `#edf5fb` with a restrained blue grid |
| Card | `#ffffff` |
| Primary text | `#102a43` |
| Boundary | `#7fa3c2` |
| Traffic | `#2563eb` |
| Write | `#ea580c` |
| Data | `#059669` |
| Cross-region | `#7c3aed`, dashed |

Nested boundaries carry a small `GLOBAL`, `REGION`, or `NETWORK` badge. Nodes
use a manifest-backed neutral glyph plus provider metadata. A colored ownership
spine makes each nested deployment boundary readable even in grayscale.

## Required semantic contract

Select `semantic_profile: "cloud-fabric"` and provide:

- `diagram_type: "deployment"`
- `platform_profile`: `provider-neutral`, `aws`, `azure`, `gcp`, or `kubernetes`
- `icon_manifest_version: "2026.07-neutral.1"`
- at least one `deployment_kind: "region"` boundary
- every node: `deployment_id` and a manifest-backed `icon_id`
- every cross-deployment edge: a non-empty `via`

Boundary parents must form an acyclic tree, nesting depth is capped at four,
child boundaries stay at least `16px` inside their parent, and nodes stay at
least `20px` inside their assigned deployment.

## Icon policy

The bundled manifest contains provider-neutral geometric glyphs. It records
official AWS, Azure, and Google Cloud asset URLs as adapter metadata; it does
not redistribute vendor trademarks. If a user supplies an official icon pack,
preserve its license and version outside this repository and map it through the
manifest boundary.

Never invent a vendor service logo. Unknown `icon_id` values fail validation.

## Composition rules

- Show deployment ownership through nesting, not background color alone
- Use a shallow `global → region → network` hierarchy where possible
- Keep ingress paths vertical and regional data paths aligned
- Name cross-boundary mechanisms (`peering`, `transit gateway`, `service mesh`)
- Zero crossings in official samples; use no more than two bends per edge
- Do not encode both logical service architecture and every subnet in one view
- Reserve the icon/provider area before fitting text; no title or subtitle may cross a card edge
- Place ingress mechanism labels in inter-boundary whitespace, clear of Region and Network headers

## Signature checklist

- Pale cloud grid and explicit `global → region → network` nesting
- Ownership spines plus `GLOBAL`, `REGION`, and `NETWORK` boundary badges
- Manifest-backed globe, compute, database, gateway, stream, or observe glyphs
- Top-right platform/region/deployment-mode stamp and named cross-boundary mechanisms

## Prompt cues

- English: `multi-region deployment map`, `cloud landing zone map`, `region/VPC ownership map`
- 中文:`多区域部署图`、`云部署拓扑`、`Region/VPC 归属图`
- Copyable cue: `Use Style 10 Cloud Fabric; show global ingress, Region and VPC ownership, neutral cloud glyphs, deployment mode, and every cross-boundary mechanism.`

## Do not blend with

Do not use C4 abstraction labels, transit-map station numbering, or live SRE
metric cards. When deployment evidence is absent, fall back to a generic Style
1–7 architecture view instead of inventing cloud ownership.

## Fixture

`fixtures/cloud-fabric-style10.json` demonstrates active-active checkout
deployment with two regions, explicit VPC ownership, and neutral glyphs.
skills/fireworks-tech-graph/references/style-9-c4-review-canvas.md
# Style 9: C4 Review Canvas

A review-room canvas for C4 context, container, and component diagrams. The
warm paper surface and deterministic pencil echoes make design discussion feel
human while the underlying geometry remains exact.

## Best fit

- Architecture Decision Records and design reviews
- C4 context, container, or component views
- Responsibility and dependency reviews
- Diagrams that need annotations without looking like a slide template

## Visual tokens

| Token | Value |
|---|---|
| Canvas | `#f7f2e8` |
| Card | `#fffdf7` |
| Primary text | `#24312f` |
| Boundary | `#8c7d68`, dashed |
| Primary relationship | `#365f56` |
| State change | `#a44a3f` |
| Data/read | `#356a8a` |
| Asynchronous | `#7a5c99` |

Use Avenir/system sans-serif. Keep type labels uppercase and small; keep
descriptions sentence-like and technologies terse.

## Required semantic contract

Select `semantic_profile: "c4-review"` and provide:

- `diagram_type: "c4"`
- one `c4_level`: `context`, `container`, or `component`
- `scope`, `title`, integer `rough_seed`, and a legend
- every node: `c4_type`, `label`, and `description`
- container/component nodes: `technology` and a card at least `170×96`
- every relationship: short `label` and explicit `protocol`

Do not mix a deeper C4 element into a higher-level view. The runtime rejects a
component in a container view and rejects unknown boundary parents.

## Roughness without geometry drift

The `rough_seed` creates stable decorative second strokes. Decorations use
`data-graph-role="decoration"`; the ordinary orthogonal path remains the only
semantic edge. A seed must reproduce byte-identical SVG across runs.

Never jitter:

- node bounds or port anchors
- the semantic route or arrowhead
- label bounds
- container gutters

## Composition rules

- One abstraction level per canvas
- At least `40px` between nodes and `20px` inside boundaries
- Zero undeclared crossings and zero bridge crossings for official samples
- At most two bends per relationship; prefer aligned straight paths
- Put external people/systems outside the system boundary with a clear gutter
- Keep labels short enough to sit in whitespace, not on top of cards
- Wrap responsibility copy to at most two lines; keep technology in the lower-right safe area

## Signature checklist

- Top-right dashed review stamp showing the C4 level and review state
- Uppercase C4 type, responsibility copy, and a separate technology footer on each card
- Dashed external-system cards and deterministic pencil echoes that never alter geometry
- Two-line relationship badges: action first, protocol in brackets below

## Prompt cues

- English: `C4 review board`, `ADR review canvas`, `container responsibility review`
- 中文:`C4 评审画布`、`ADR 评审图`、`职责边界评审图`
- Copyable cue: `Use Style 9 C4 Review Canvas; show one C4 level, responsibilities, technologies, review state, and relationship protocols.`

## Do not blend with

Do not add Region/VPC ownership bands, event-metro stations, or golden-signal
metric chips. If those facts dominate the request, route to Style 10, 11, or
12 instead of turning the C4 review into a mixed abstraction canvas.

## Fixture

`fixtures/c4-review-canvas-style9.json` demonstrates a checkout container
review with a deterministic seed and a 100-point showcase composition score.
skills/fireworks-tech-graph/references/motion-effects.md
# Focused SVG-to-GIF Motion

Motion has one public contract:

```text
generated semantic SVG -> validated animated GIF
```

PNG and JPEG are not accepted as animation inputs. Animated WebP, MP4, WebM,
poster generation, and the old offline motion-review player are not part of this
surface. Static SVG/PNG rendering and the single-file interactive HTML exporter
remain unchanged.

## Simplest prompts

Use the most recently generated SVG when the user says:

- `让这张图动起来`
- `生成 GIF`
- `制作 GIF`
- `把刚才的 SVG 转成 GIF`
- `Generate a GIF`
- `Animate this diagram`
- `Animate this SVG as a GIF`

No format, preset, duration, or frame-rate wording is needed. The default is a
960px-wide, 5.75-second, 20fps, 115-frame, infinitely looping GIF. `auto` reads the
style id and explicit `data-motion-*` edge metadata from the generated SVG.

The input must satisfy one of the twelve approved motion-scene contracts. Exact
source bytes are not pinned, so validated title and content variants of a
supported topology are accepted. The role/stage/order schedule, route direction,
required source colors, and geometry remain fail-closed; `auto` does not infer
missing metadata or apply reviewed motion to an arbitrary same-style topology.
The only motion media artifact is GIF. Unless `--report` supplies another path,
the CLI also writes `<output>.motion.json` with the validation evidence.

## ByteByteGo-derived rules

Six measured ByteByteGo newsletter GIFs use short 2.10–2.75 second loops at a
uniform 20 or 33.33fps. Only 1.20–3.56% of their canvases change over a loop and
every measured frame is unique. The default loop extends to 5.75 seconds so the
accepted 1.8-second construction leaves frames 38–109 for a clearly readable,
continuously moving operating state. This project applies those lessons as a reusable technical-diagram contract:

1. Frame 0 keeps nodes and regions readable while every connector is absent.
2. Existing routes draw on in semantic order, then settle with their direction markers.
3. Nodes, labels, containers, marker geometry, and the camera remain fixed.
4. Connector overlap is scene-pinned: Styles 4 and 12 use one active build, plans that call for two enforce two, and Style 7's exact parallel governance schedule reaches three. The completed topology gets a noticeable operating hold.
5. Samples use a uniform GIF-native cadence; no 6/7-centisecond alternation.
6. The final slot is a real animation sample, never a copied opening frame.
7. Every settled route carries marker-free operating motion toward the target. Styles 1–4 retain their approved packet-head, terminal-evidence, Blueprint-bead, and Notion-card identities. Styles 5–12 use a separate rail/signature family per scene and remain legible at 50% review size.

## Approved default gate

Styles 1–12 are enabled and their signature, speed, path, geometry, and
construction contracts are user-approved. The shared `+2s-settled-flow`
timing revision was approved on 2026-07-17, so new default 5.75-second packages
record `review_status: user-approved`. The explicit 3.75-second and 2.75-second
compatibility timelines remain available when requested.

### Style 1 — Memory Weave

Style 1 follows eight explicit route roles:

| Frames | Role | Edge meaning |
|---:|---|---|
| `1–8` | `ingress` | user request |
| `5–12` | `reason` | application to model |
| `9–16` | `extract` | fact extraction |
| `13–20` | `transform` | extracted facts |
| `17–24` | `resolve` | conflict resolution |
| `21–28` | `memory-write` | durable write |
| `25–32` | `memory-read` | memory relation/read |
| `29–36` | `response-context` | context to response |

The `connector-draw-on-with-persistent-data-flow` primitive transiently hides all immutable
source edges and their route-owned label groups. Each route gets one marker-free
linear draw clone and one settled clone that restores the original marker at
arrival. Its label plate and text appear only with that settled route; label
geometry never moves. The topology is complete by frame 36. From frames `36–114`,
every settled route gets an adjacent marker-free, filter-free pair that preserves
the source path direction. The `persistent-data-flow-stream` body uses memory-cyan
`#06b6d4`, rounded caps and joins, opacity `.90`, width
`min(4.0px, max(3.0px, source stroke × 1.60))`, and dash pattern `16 25`. Style 1's
current 2.4px source routes therefore resolve to exactly 3.84px. Its immediately
following `persistent-data-flow-head` clone stays inside that body with color
`#e0f2fe`, width `2.20px`, opacity `.98`, rounded caps and joins, and pattern
`6 35`. Both patterns have a 41-unit period; the head dash offset is always the
body offset minus 10 units, placing the bright six-unit segment over the leading
six units of the cyan body. Neither clone carries a marker, filter, glow, or blur.

Both layers advance together by `-6.0` SVG user units per rendered frame toward
the target: 6px/frame or 120px/s at 960px, and 3px/frame or 60px/s at 50% review
scale. Their shared phase is `(motionStage × 7 + motionOrder × 3) mod 41`; the
request/reason/extract/facts/resolved/write/relate/context phase order is
`[7, 14, 21, 28, 31, 35, 1, 8]`. Period 41 and step 6 are coprime, so the 39
stream samples do not repeat a phase. Frames 36, 37, and 38 use pair-layer factors
`.30`, `.65`, and `1.00`; frames `38–109` hold full body/head opacities `.90`/`.98`.
Frames `110–114` keep both layers advancing while multiplying settled topology,
labels, body, and head opacity by `1.00`, `.7575`, `.515`, `.2725`, and `.03`.
Sampling uses `time × fps − 0.5`, so frames `0–35` match the accepted raw Chromium
baseline exactly.
This keeps the reset in motion and leaves frame 74 distinct from the connector-free
frame 0. Runtime geometry sentinels also fail closed unless `ingress` travels right,
`resolve` travels left, and `memory-write` follows down → left → down while the
dash offset remains negative; declaring source/target metadata alone is not enough.

### Style 2 — Dark Terminal evidence trace

Style 2 selects the `tool-grounding` scene by SVG style metadata. Its schedule is
keyed by the exact `(data-motion-role, data-motion-order)` pair, so two grounding
routes remain independently addressable and every expected key must appear exactly
once. The source edge stage and route color are validated before rendering.

| Frames | Stage | Role / order | Evidence transition | Body color |
|---:|---:|---|---|---|
| `1–8` | `1` | `ingress / 0` | query enters the application | `#a855f7` |
| `5–12` | `2` | `delegate / 0` | application delegates to the model | `#a855f7` |
| `9–16` | `3` | `tool-call / 0` | model issues the terminal command | `#38bdf8` |
| `13–20` | `4` | `inspect / 0` | terminal output is inspected | `#38bdf8` |
| `17–24` | `5` | `index / 0` | inspected evidence enters the index | `#22c55e` |
| `21–28` | `6` | `grounding / 0` | indexed evidence returns to grounding | `#fb7185` |
| `25–32` | `6` | `grounding / 1` | grounding reaches the answer context | `#fb7185` |
| `29–36` | `7` | `answer / 0` | grounded answer returns to the user | `#f97316` |

After each route settles, it receives one adjacent, marker-free, filter-free pair.
The `terminal-evidence-stream` body inherits that source route's stroke, uses width
`min(3.8px, max(3.0px, source stroke × 1.50))`, opacity `.94`, and dash pattern
`15 26`. Style 2's 2.3px routes resolve to exactly 3.45px. The immediately following
`terminal-command-head` is white `#f8fafc`, 2.0px wide, opacity `1.00`, and uses
pattern `5 36`; its offset is the body offset minus 10 units. Both advance by
`-6.0` SVG user units per rendered frame, equal to 6px/frame at 960px and 3px/frame
at 50% review size. The phase formula is
`(motionStage × 6 + motionOrder × 3) mod 41`, producing
`[6, 12, 18, 24, 30, 36, 39, 1]` in schedule order. Period 41 and step 6 are
coprime, so all 39 stream samples remain phase-unique.

All eight directions are runtime sentinels: `ingress / 0` right;
`delegate / 0` down → left → down; `tool-call / 0` right; `inspect / 0` down;
`index / 0` right → up; `grounding / 0` up → left → up; `grounding / 1` right;
and `answer / 0` right. The body/head pair fades in on frames `36–38`, holds full
opacity on frames `38–109`, then keeps moving while sharing the five-frame reset.

One scene signature appears over the terminal prompt: `terminal-prompt-cursor`.
The renderer requires exactly one `data-node-id="terminal"` node and exactly one
unchanged `_` source text within it. A 2.2px-high `#a7f3d0` rectangle is derived
from that underscore's runtime `getBBox()` and placed in a separate signature
layer. It changes opacity only: from frames `16–69`, each 16-frame period has eight
bright frames at `.95` followed by eight absent frames. Its cadence runs through
`reset_start - 1`; frames `110–114` multiply a
bright cursor by the shared reset opacity. The source underscore remains visible
and unmutated. Terminal text typing, scan lines, glows, camera motion, and animated
backgrounds are outside this scene contract.

### Style 3 — Blueprint distribution wave

Style 3 selects the `service-blueprint` scene. SHA-256
`b8f55d9ea0c6111176d8ff50d2e844b2001ee5087a3940621e635e1b875d470d`
remains the reviewed reference source, not an exact-byte input lock. Its schedule
is keyed by the exact `(data-motion-role, data-motion-order)` pair.
Every key resolves to one immutable source edge, and every source stage and route
color is validated before rendering.

| Frames | Stage | Role / order | Blueprint transition | Body color |
|---:|---:|---|---|---|
| `1–6` | `1` | `ingress / 0` | client request enters the gateway | `#38bdf8` |
| `4–9` | `2` | `policy / 0` | gateway checks identity and policy | `#67e8f9` |
| `8–13` | `3` | `fanout / 0` | gateway opens the Order branch | `#38bdf8` |
| `11–16` | `3` | `fanout / 1` | gateway opens the Catalog branch | `#38bdf8` |
| `14–19` | `3` | `fanout / 2` | gateway opens the Billing branch | `#38bdf8` |
| `18–23` | `4` | `data-write / 0` | Order writes Postgres | `#fde047` |
| `21–26` | `4` | `data-write / 1` | Catalog writes Redis | `#fde047` |
| `24–29` | `4` | `data-write / 2` | Billing writes Warehouse | `#fde047` |
| `28–33` | `5` | `event / 0` | Billing publishes an event | `#fb7185` |
| `31–36` | `6` | `telemetry / 0` | Event Router emits metrics | `#fb7185` |

From frame 36, each settled route receives one marker-free, filter-free
`blueprint-distribution-wave` body. It inherits the source stroke, uses width
`min(3.4px, max(2.8px, source stroke × 1.40))`, opacity `.92`, rounded caps and
joins, and dash pattern `12 31`. The reviewed 2.1px routes resolve to exactly
2.94px, or 1.47px at 50% review scale. Dash offset advances by `-6.0` SVG units
per rendered frame.

Each body is paired with one `blueprint-registration-bead`: a circle with radius
3.0px, fill `#e0f2fe`, source-colored 1.2px stroke, and opacity `.98`. Its initial
path distance is the route phase. Every rendered frame advances `cx` and `cy` by
`+6.0` user units through `getPointAtLength`; the bead wraps from the target end
to the source start and never reverses. Only `cx`, `cy`, and `opacity` change.

The stage-only phase formula is
`(motionStage × 7 + motionOrder × 0) mod 43`, producing
`[7, 14, 21, 21, 21, 28, 28, 28, 35, 42]`. All three fan-out routes therefore
share phase 21, and all three equal-length data-write routes share phase 28 and
the same bead Y coordinate throughout the live interval. Period 43 and step 6
are coprime, so the 39 live samples do not repeat a body phase.

The ten runtime direction sentinels require: ingress right; policy right;
fan-out/0 down → left → down; fan-out/1 down; fan-out/2 down → right → down;
all three data writes down; event right; and telemetry down. Body dash offset
must stay negative while bead travel stays positive. Bodies and beads are absent
through frame 35, fade in with `.30`, `.65`, and `1.00` factors on frames
`36–38`, hold full opacity on frames `38–109`, then keep advancing through the
five-frame reset. Glow, blur, shadow, scan lines, node/text/camera/background
motion, halo, ripple, and terminal cursors remain outside the Style 3 contract.

### Style 4 — Notion memory-card handoff

Style 4 selects the `memory-lifecycle` scene. SHA-256
`04cf833659e82c3e1743db4042cacf839a6d784a99c32d076e36fd4776e70c1b`
remains the reviewed reference source, not an exact-byte input lock. Its exact
`(data-motion-role, data-motion-order)` schedule allows only one active
connector build:

| Frames | Stage | Role / order | Memory transition | Rail/card color |
|---:|---:|---|---|---|
| `1–4` | `1` | `sample / 0` | sensory input enters working memory | `#3b82f6` |
| `5–8` | `2` | `attend / 0` | active context reaches the agent | `#3b82f6` |
| `9–12` | `3` | `invoke / 0` | the agent invokes procedural memory | `#7c3aed` |
| `13–22` | `4` | `remember / 0` | working context enters episodic memory | `#059669` |
| `23–26` | `5` | `consolidate / 0` | episodes become semantic knowledge | `#ea580c` |
| `27–36` | `6` | `recall / 0` | semantic knowledge returns to the agent | `#ea580c` |

From frame 36, all six settled paths receive a marker-free, filter-free
`notion-memory-rail`. Each rail uses destination-memory color, width
`min(3.0px, max(2.4px, source stroke × 1.50))`, opacity `.88`, round caps and
joins, and dash pattern `12 35`. The reviewed 1.8px paths resolve to exactly
2.70px, or 1.35px at 50% review size. The phase formula
`(motionStage × 7 + motionOrder × 0) mod 47` produces
`[7, 14, 21, 28, 35, 42]`; dash offset advances by `-6.0` user units per frame.
Period 47 and step 6 are coprime, so none of the 39 live rail samples repeats.

Each rail is paired with one `notion-memory-card` group. Its white outer rect is
`x=-7`, `y=-5`, `14×10`, `rx=2`, with a 1.4px semantic-color stroke. Two 2px
butt-cap ink lines use `shape-rendering="crispEdges"` and run from `(-4.5,-2)`
to `(4,-2)` and from `(-4.5,2)` to `(0.5,2)`. The exact normalized starting-progress vector is
`[0.08, 0.22, 0.36, 0.50, 0.64, 0.78]`; distance is
`8 + progress × (pathLength − 16)`. Cards keep 8px clearance from both endpoints,
advance `+6.0` path units per rendered frame, wrap from target clearance to
source clearance, and animate only group `transform` and `opacity`.

Direction sentinels require sample, attend, and invoke to point right at `0°`;
remember to point down at `90°`; consolidate to point right at `0°`; and recall
to point up at `-90°`. Rails and cards are absent through frame 35, fade in with
`.30`, `.65`, and `1.00` factors on frames `36–38`, hold full opacity on frames
`38–109`, and keep advancing during the shared five-frame reset. Cards and rails
remain below route labels and nodes. Generic packet heads, circular beads,
terminal cursors, glow, blur, shadow, scan lines, node/text/camera/background
motion, halo, and ripple remain outside the Style 4 contract.

### Styles 5–12 — approved signatures and shared timing

All eight approved style contracts preserve immutable source paths, labels, nodes, containers,
text, and markers. They reuse the connector-free opening, exact semantic draw-on,
frames `36–38` live fade, frames `38–109` operating hold, and moving `110–114`
reset. Live rails never exceed their source stroke width or their scene ceiling.

| Style | Preset | Rail | Signature / auxiliary | Travel | Style / timing status |
|---:|---|---|---|---:|---|
| 5 | `agent-orchestration` | `glass-handoff-rail` | 14×9 `glass-task-capsule`; coordinator halo | 6px/frame | approved / `+2s` approved |
| 6 | `governed-runtime` | `governance-thread` | 12×12 `policy-seal` | 6px/frame | approved / `+2s` approved |
| 7 | `token-stream` | `api-token-rail` | three-cell `token-train` | 6px/frame | approved / `+2s` approved |
| 8 | `golden-circuit` | `luxury-circuit-rail` | `gem-tracer`; only its gem halo is filtered | 6px/frame | approved / `+2s` approved |
| 9 | `review-trace` | `review-trace-rail` | `review-cursor` | 5px/frame | approved / `+2s` approved |
| 10 | `cloud-flow` | `cloud-flow-rail` | region chevrons, replication capsule, availability pulses | 6px/frame | approved / `+2s` approved |
| 11 | `event-transit` | `event-transit-rail` | event train, exception/projection cars, dwell rings | 5px/frame | approved / `+2s` approved |
| 12 | `ops-pulse` | incident / telemetry rails | ECG/export heads, trace reveals, `waterfall-scanner` | 5px/frame | approved / `+2s` approved |

Styles 8, 11, and 12 use `(data-motion-role, data-motion-stage,
data-motion-order)` because repeated roles must stay independently addressable.
Styles 10–12 add only explicit motion metadata to their source fixtures; stripping
`data-motion-role`, `data-motion-stage`, and `data-motion-order` reproduces the
pre-animation static SVG geometry exactly.

## CLI

```bash
SKILL_ROOT="${CLAUDE_SKILL_DIR:-/absolute/path/from-codex-skill-metadata}"
python3 "$SKILL_ROOT/scripts/fireworks.py" animate diagram.svg diagram.gif
```

Optional controls remain deliberately small:

```bash
python3 "$SKILL_ROOT/scripts/fireworks.py" animate diagram.svg diagram.gif \
  --preset auto --duration 5.75 --fps 20 --width 960 \
  --report diagram.motion.json
```

Use `--dry-run` to validate identity, semantics, geometry, timeline, output path,
and render budget without launching Chrome or writing artifacts. `duration × fps`
must be a whole number of at least 55 frames. Width × height × frame count may
not exceed 600 million rendered pixels.

## Runtime

SVG-to-GIF export needs Node.js 18+, FFmpeg/FFprobe, Chrome/Chromium, and either
`puppeteer` or `puppeteer-core`:

```bash
brew install ffmpeg
for SKILL_ROOT in \
  "$HOME/.agents/skills/fireworks-tech-graph" \
  "$HOME/.claude/skills/fireworks-tech-graph"
do
  [ -d "$SKILL_ROOT" ] || continue
  npm install --prefix "$SKILL_ROOT" --ignore-scripts --no-save --package-lock=false puppeteer-core@25.3.0
  python3 "$SKILL_ROOT/scripts/fireworks.py" doctor
done
```

Set `FIREWORKS_CHROME_PATH` for a nonstandard Chrome install. Set
`FIREWORKS_PUPPETEER_PATH` to an explicit trusted module directory when keeping
the Node runtime elsewhere. Renderer resolution never searches the caller's
working directory, so installing `puppeteer-core` in the diagram project does
not make it available to a copied Skill. A `skills --copy` install creates two
independent roots, so each existing root needs its own runtime installation.
Chrome sandboxing is enabled by default; the no-sandbox override is reserved for
isolated CI runners.

## Quality contract

- Require a fireworks-tech-graph SVG with `viewBox`, valid semantic metadata, and
  explicit motion role, stage, and order on every reviewed edge.
- Sanitize SVG and block scripts, event handlers, external references, active CSS,
  and browser network requests.
- Pass XML, marker, geometry, and composition checks before rendering.
- Preserve the source SHA-256 and assert the static DOM before every frame capture.
- Hide source edges and route labels with a transient stylesheet; insert draw,
  settled-marker, per-style stream-body, packet-head, registration-bead, or
  memory-card decorations below nodes. Keep each Style 1/2 head immediately
  after its body, keep Style 3 beads and Style 4 cards below route labels and
  nodes, and keep the arrival-label layer above every motion path. Keep Style 2's
  opacity-only cursor above the terminal node without mutating its source underscore.
- Render uniform frame-center samples on a manual timeline.
- Compare Style 1 raw Chromium frames `0–35` from 55-frame and 75-frame timelines;
  their hashes must match before GIF palette quantization. Keep a separate 75-frame
  raw baseline for the approved Style 1 worker and require every post-change frame
  to remain byte-identical.
- Raster-test Style 2's body/head layering, 6px/frame full-scale travel,
  3px/frame half-scale travel, semantic route colors, all eight direction
  sentinels, cursor cadence, and five-frame reset before GIF encoding.
- Raster-test Style 3's connector-free opening, ten bodies and ten registration
  beads, 6px/frame full-scale and 3px/frame half-scale bead travel, stage-locked
  fan-out/data-write synchrony, all ten directions, and source-DOM integrity.
- Preserve separate 75-frame raw Chromium baselines for approved Styles 1–3 and
  require every post-change frame to remain byte-identical.
- Render every style at 75 and 115 frames and accept all 852 raw comparisons for
  frames `0–70` through a three-tier compatibility gate: binary SHA-256 exact,
  decoded RGBA exact, then guarded compositor-antialias equivalence. The guarded
  tier requires AE ≤ 128, normalized RMSE ≤ 0.001, every connected difference
  component to be at most 2px wide or high, and every component to remain on an
  edge or node border. Report each tier separately. DOM and per-style signature
  geometry remain strict-exact before GIF palette quantization.
- Raster-test Style 4's connector-free opening, sequential six-route arrivals,
  six semantic rails and six outlined cards, 8px endpoint clearance, 6px/frame
  full-scale and 3px/frame half-scale travel, exact progress/phase/rotation
  vectors, all six directions, and source-DOM integrity. After real Chromium
  capture and 50% point downsampling, horizontal, downward, and upward cards
  must each retain two non-touching unequal-length ink strokes with a complete
  background row or column between them.
- Require exact codec, dimensions, duration, and frame count from FFprobe.
- Require exact raw and encoded frame counts and zero adjacent duplicates. Keep
  every frame unique through 75 frames. For longer timelines, require at least 75
  unique rasters and allow repeated hashes inside full-opacity frames `38–109`.
  Frame `110` is the sole permitted boundary repeat because its unchanged reset
  opacity is exactly `1.00`; classify it as `intentional_reset_boundary_repeat`.
  Frames `111–114` remain globally distinct. Record all repeated frame indices
  and embed infinite GIF looping.
- Install the GIF and optional JSON report atomically.
- Treat 500KB as the focused artifact target and report deterministic size advice.
- Deliver one live GIF plus a phase contact sheet for each style review.
skills/fireworks-tech-graph/references/style-diagram-matrix.md
# Style-to-Diagram-Type Adaptation Guide

Not all styles work equally well for every diagram type. Use this guide to pick the best style.

## Engineering-first styles (9–12)

Styles 9–12 pair a visual language with an executable semantic contract. They
are deliberately specialized rather than universal skins.

| Style | Required evidence | Visual fingerprint | Prompt cues | Fallback | Never blend with |
|---|---|---|---|---|---|
| 9 C4 Review Canvas | One declared C4 level, responsibilities, technology, labeled protocols | Warm review board, C4 type headers, dashed review stamp, deterministic pencil echo | `C4 review board`, `C4 评审画布`, `ADR 评审图` | Styles 1–7 generic architecture | Region/VPC bands, event stations, live metric chips |
| 10 Cloud Fabric | Region/network/workload ownership and named cross-boundary mechanisms | Cloud grid, nested ownership spines, neutral manifest glyphs, deployment-mode stamp | `deployment topology`, `多区域部署图`, `Region/VPC 归属图` | Styles 1–7 when deployment facts are absent | C4 abstraction labels, station numbering, golden-signal cards |
| 11 Event Transit | Topics, ordered processors, consumer groups, junctions, DLQ/state | Thin metro rails, numbered stations, fixed arrowheads, role-specific terminals | `event metro map`, `事件地铁图`, `Kafka 拓扑图` | Generic flow style when stream evidence is absent | Cloud nesting, C4 cards, SRE dashboards |
| 12 Ops Pulse | Fixed window, four golden signals, statuses, one critical path and trace | Live stamp, status rails, metric windows, numbered hops, trace ruler | `reliability pulse`, `事故排查视图`, `黄金信号追踪图` | Generic architecture when measured evidence is absent | Deployment ownership, event rail metaphor, C4 review marks |

All four official fixtures use the `showcase` composition contract: at least
40px node spacing, 20px container gutter, zero bridge crossings, at most two
bends per edge, and no semantic edge duplicated for visual effects.

## Architecture Diagram
| Style | Suitability | Notes |
|-------|----------|
| 1 Flat Icon | Excellent | Default choice; colorful node fills, clear layering |
| 2 Dark Terminal | Excellent | Popular for dev blogs; use colored borders on dark bg |
| 3 Blueprint | Excellent | Perfect for formal architecture docs |
| 4 Notion Clean | Good | Minimal, works for inline docs |
| 5 Glassmorphism | Good | Striking for presentations and product pages |
| 6 Claude Official | Good | Warm aesthetic, Anthropic-style presentations |
| 7 OpenAI Official | Good | Clean, precise; minimal borders, brand green accents |
| 8 Dark Luxury *(AI-authored)* | Excellent | Premium editorial; gold-on-black layers stand out for architecture docs |

## Class Diagram / ER Diagram
| Style | Suitability | Notes |
|-------|------------|-------|
| 1 Flat Icon | Good | Colored headers per class category |
| 2 Dark Terminal | Good | High contrast for code-like diagrams |
| 3 Blueprint | Excellent | Best for formal UML documentation |
| 4 Notion Clean | Excellent | Clean, minimal; ideal for Notion-embedded diagrams |
| 5 Glassmorphism | Poor | Glass effects distract from structural content |
| 6 Claude Official | Excellent | Warm, readable; good for documentation |
| 7 OpenAI Official | Excellent | Minimal aesthetic matches UML precision |
| 8 Dark Luxury *(AI-authored)* | Fair | Non-standard dark bg for UML; use only for premium editorial contexts |

## Sequence Diagram
| Style | Suitability | Notes |
|-------|------------|-------|
| 1 Flat Icon | Good | Clear lifelines; activation boxes visible |
| 2 Dark Terminal | Good | Good for dev articles; dashed lifelines visible |
| 3 Blueprint | Excellent | Formal, technical documentation |
| 4 Notion Clean | Excellent | Best for Notion-embedded sequence diagrams |
| 5 Glassmorphism | Poor | Glass effects make lifelines hard to read |
| 6 Claude Official | Excellent | Ward contrast |
| 7 OpenAI Official | Excellent | Minimal, precise; ideal for API docs |
| 8 Dark Luxury *(AI-authored)* | Good | Dramatic contrast; dark lifelines suit developer blogs and premium tech docs |

## Flowchart / Process Flow
| Style | Suitability | Notes |
|-------|------------|-------|
| 1 Flat Icon | Excellent | Default; colorful decision diamonds |
| 2 Dark Terminal | Good | Works well for dev workflow diagrams |
| 3 Blueprint | Good | Formal process documentation |
| 4 Notion Clean | Good | Clean for SOPs and inline docs |
| 5 Glassmorphism | Good | Striking for product demos |
| 6 Claude Official | Good | Warm aesthetic for presentations |
| 7 OpenAI Official | Good | Clean and minimal |
| 8 Dark Luxury *(AI-authored)* | Good | Striking for premium process documentation |

## Mind Map / Concept Map
| Style | Suitability | Notes |
|-------|------------|-------|
| 1 Flat Icon | Excellent branches, engaging |
| 2 Dark Terminal | Good | Neon-like branches on dark bg |
| 3 Blueprint | Poor | Blueprint grid conflicts with radial layout |
| 4 Notion Clean | Excellent | Ideal for n brainstorming |
| 5 Glassmorphism | Excellent | Stunning visual for presentations |
| 6 Claude Official | Good | Warm, readable |
| 7 OpenAI Official | Good | Clean and minimal |
| 8 Dark Luxury *(AI-authored)* | Excellent | Gold accent branches on black; radial layouts stand out in presentations |

## Data Flow Diagram
| Style | Suitability | Notes |
|-------|------------|-------|
| 1 Flat Icon | Excellent | Color-coded arrows by data type |
| 2 Dark Terminal | Excellent | Glowing data paths on dark bg |
| 3 Blueprint | Excellent | Formal data flow documentation |
| 4 Notion Clean | Good | Minimal, clean |
| 5 Glassmorphism | Poor | Distracts from flow semantics |
| 6 Claude Official | Good | Readable |
| 7 OpenAI Official | Good | Precise, minimal |
| 8 Dark Luxury *(AI-authored)* | Excellent | Color-coded data paths shine against deep black; ideal for data engineering docs |

## Use Case Diagram
| Style | Suitability | Notes |
|-------|------------|-------|
| 1 Flat Icon | Good | Colorful use case ellipses |
| 2 Dark Terminal | Poor | Stick figures less visible on dark bg |
| 3 rint | Excellent | Classic UML aesthetic |
| 4 Notion Clean | Excellent | Perfect for product requirement docs |
| 5 Glassmorphism | Poor | Unnecessary visual noise |
| 6 Claude Official | Excellent | Warm, professional |
| 7 OpenAI Official | Excellent | Clean, precise UML |
| 8 Dark Luxury *(AI-authored)* | Fair | Stick figures less visible on deep black; use cautiously |

## State Machine Diagram
| Style | Suitability | Notes |
|-------|------------|-------|
Flat Icon | Good | Colorful states |
| 2 Dark Terminal | Good | Glowing states and transitions |
| 3 Blueprint | Excellent | Best for formal UML state machines |
| 4 Notion Clean | Excellent | Clean for documentation |
| 5 Glassmorphism | Poor | Distracts from state transitions |
| 6 Claude Official | Excellent | Readable |
| 7 OpenAI Official | Excellent | Minimal, precise |
| 8 Dark Luxury *(AI-authored)* | Good | High contrast for state transitions; editorial quality |

## Network Topology
| Style | Suitabili |
|-------|------------|-------|
| 1 Flat Icon | Excellent | Colorful device icons |
| 2 Dark Terminal | Excellent | Cyberpunk-style network maps |
| 3 Blueprint | Excellent | Ideal for infrastructure docs |
| 4 Notion Clean | Good | Clean for IT documentation |
| 5 Glassmorphism | Good | Striking for presentations |
| 6 Claude Official | Good | Professional network diagrams |
| 7 OpenAI Official | Good | Clean infrastructure diagrams |
| 8 Dark Luxury *(AI-authored)* | Excellent | Deep black classic for infrastructure docs; gold topology lines pop |

## Comparison / Feature Matrix
| Style | Suitability | Notes |
|-------|------------|-------|
| 1 Flat Icon | Excellent | Color-coded checkmarks |
| 2 Dark Terminal | Good | Works for dev tool comparisons |
| 3 Blor | Grid conflicts with table layout |
| 4 Notion Clean | Excellent | Perfect for Notion-embedded tables |
| 5 Glassmorphism | Poor | Distabular data |
| 6 Claude Official | Excellent | Clean, warm |
| 7 OpenAI Official | Excellent | Minimal, precise |
| 8 Dark Luxury *(AI-authored)* | Fair | Dark bg non-standard for comparison tables; use cautiously |

## Timeline / Gantt
| Style | Suitability | Notes |
|-------|------------|-------|
| 1 Flat Icon | Excellent | Colorful bars by category |
| 2 Dark Terminal | Good | Works for dev roadmaps |
| 3 Blueprint | Good | Formal project plans |
| 4 Notion Clean | Excellent | Ideal for Notion project docs |
| 5 Glassmorphism | Good | Striking for keynote presentations |
| 6 Claude Official | Good | Warm, professional |
| 7 OpenAI Official | Good | Clean timeline |
| 8 Dark Luxury *(AI-authored)* | Good | Premium project roadmaps and keynote presentations |

## Agent / Memory Architecture
| Style | Suitability | Notes |
|-------|------------|-------|
| 1 Flat Icon | Excellent | Colorful layers, engaging |
| 2 Dark Terminal | Excellent | Popular for AI/ML blog posts |
| 3 Blueprint | Good | Formal AI system documentation |
| 4 Notion Clean | Good | Clean for AI research notes |
| 5 Glassmorphism | Excellent | Stunning for AI product presentations |
| 6 Claude Official | Excellent | Anthropic AI aesthetic |
| 7 OpenAI Official | Excellent | OpenAI AI aesthetic |
| 8 Dark Luxury *(AI-authored)* | Excellent | Best for premium AI system docs; champagne gold on deep black |
skills/fireworks-tech-graph/schemas/architecture-v1.schema.json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://yizhiyanhua-ai.github.io/fireworks-tech-graph/schemas/architecture-v1.schema.json",
  "allOf": [
    { "$ref": "diagram-v1.schema.json" },
    { "properties": { "mode": { "const": "architecture" } } }
  ]
}
skills/fireworks-tech-graph/references/style-4-notion-clean.md
# Style 4: Notion Clean

Minimal, documentation-friendly. Designed to embed in Notion, Confluence, or GitHub wikis.

## Colors

```
Background:     #ffffff
Box fill:       #f9fafb  (gray-50) or #ffffff
Box stroke:     #e5e7eb  (gray-200)
Box radius:     4px

Text primary:   #111827  (gray-900)
Text secondary: #374151  (gray-700)
Text muted:     #9ca3af  (gray-400)
Text label:     #6b7280  (gray-500), uppercase, 11px

Accent (subtle, used sparingly):
  Blue:   #3b82f6 (arrows only)
  Gray:   #d1d5db (dividers)
```

## Design Principles

- **No decorative icons** — use geometric shapes only (rect, circle, diamond)
- **Generous whitespace** — 24px+ padding between elements
- **Single arrow color** — blue (#3b82f6) for all connections
- **Labels in ALL CAPS** — section headers and node type labels
- **No drop shadows** — flat only

## Typography

```
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI',
             'Helvetica Neue', Arial, 'PingFang SC', 'Microsoft YaHei',
             'Microsoft JhengHei', 'SimHei', sans-serif
font-size:   14px labels, 11px uppercase type labels, 18px title
font-weight: 400 normal, 500 medium for node labels
```

## Box Styles

```xml
<!-- Standard node -->
<rect rx="4" fill="#f9fafb" stroke="#e5e7eb" stroke-width="1"/>
<text fill="#111827" font-size="14" font-weight="500"/>

<!-- Type label (inside or above box) -->
<text fill="#9ca3af" font-size="11"
      font-weight="500" letter-spacing="0.08em">DATABASE</text>

<!-- Section grouping (dashed container) -->
<rect rx="4" fill="none" stroke="#e5e7eb" stroke-width="1"
      stroke-dasharray="4,3"/>
```

## Arrows

```xml
<defs>
  <marker id="arrow-blue" markerWidth="8" markerHeight="6"
          refX="7" refY="3" orient="auto">
    <polygon points="0 0, 8 3, 0 6" fill="#3b82f6"/>
  </marker>
</defs>
<line stroke="#3b82f6" stroke-width="1.5"
      marker-end="url(#arrow-blue)"/>
<!-- Optional: gray arrow for secondary flows -->
<line stroke="#d1d5db" stroke-width="1"
      stroke-dasharray="4,3" marker-end="url(#arrow-gray)"/>
```

## SVG Template

```xml
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 560"
     width="960" height="560">
  <style>
    text { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Arial, 'PingFang SC', 'Microsoft YaHei', 'Microsoft JhengHei', 'SimHei', sans-serif; }
  </style>
  <defs>
    <!-- arrow markers (blue only) -->
  </defs>
  <rect width="960" height="560" fill="#ffffff"/>
  <!-- nodes (no icons, geometry only) -->
  <!-- edges (single color) -->
  <!-- legend (minimal, only if 2+ flows) -->
</svg>
```

## Sizing Guide

- Node box: min 120×40px, prefer 160×48px for readability
- Title: top-left, 18px, gray-900, margin 32px from edges
- Spacing: 80px minimum between nodes horizontally, 60px vertically
skills/fireworks-tech-graph/references/svg-layout-best-practices.md
# SVG Technical Diagram Layout Best Practices

## Universal Layout Rules (Apply to All Styles)

### 1. Component Spacing
- **Minimum clearance between components**: 80px (edge to edge)
- **Minimum clearance for arrow paths**: 60px from component edges
- **Layer vertical spacing**: 120px between horizontal layers
- **Same-layer horizontal spacing**: 100-120px between components

### 2. Arrow Routing & Connection Points

#### Connection Point Rules
- **Never connect arrows to component corners** - use midpoints of edges
- **Entry/exit points**:
  - Top edge: `cx ± offset` where offset = 0 for single arrow, ±30px for multiple
  - Bottom edge: same rule
  - Left/right edges: `cy ± offset`
- **Clearance from corners**: minimum 20px

#### Arrow Path Routing
- **Avoid diagonal lines crossing components** - use orthogonal routing (L-shaped paths)
- **For curved arrows**:
  - Control point should be at least 40px away from any component edge
  - Use intermediate waypoints for complex routing: `M x1,y1 L x2,y2 Q cx,cy x3,y3`
- **Multiple arrows between same layers**: stagger Y-coordinates by 15-20px to avoid overlap

#### Arrow Overlap Prevention
```svg
<!-- Bad: diagonal arrow crosses component -->
<path d="M 200,100 L 600,400"/>

<!-- Good: orthogonal routing around component -->
<path d="M 200,100 L 200,250 L 600,250 L 600,400"/>

<!-- Good: curved with safe control point -->
<path d="M 200,100 Q 400,200 600,400"/>
<!-- Control point (400,200) is 50px+ away from any component -->
```

### 3. Arrow Label Placement
- **Position**: midpoint of arrow path, offset by 5-10px perpendicular to arrow direction
- **Offset first**: move the label 5-10px perpendicular to the arrow so it does not sit on the stroke
- **Background rect fallback**: include only when offsetting cannot avoid another visual element, with:
  - Padding: 4px horizontal, 2px vertical
  - Fill: match background color
  - Opacity: 0.9-0.95
- **Safety distance**: 15px minimum from any component edge
- **Multiple converging arrows**: stagger label positions vertically by 20px

### 4. Component Overlap Detection
Before finalizing SVG, check:
- No component bounding boxes overlap (8px safety margin)
- No arrow paths pass through component interiors (except intentional tunneling with dashed style)
- No text labels overlap with components or other labels

### 5. Z-Index Layering (SVG render order)
```svg
<!-- Render order (top to bottom = back to front): -->
1. Background rect
2. Grouping containers (dashed rects)
3. Arrow paths
4. Arrow label background rects when collision fallback is needed
5. Components (boxes, cylinders, etc.)
6. Component text
7. Arrow label text
8. Legend
```

## Style-Specific Enhancements

### Style-1: Flat Icon Clean
- **Perfect alignment**: snap all coordinates to 8px grid
- **Sharp corners**: rx="8" ry="8" for rounded rects (consistent)
- **Arrows**: thin (1.5-2px), filled polygon markers
- **No shadows**: flat design principle

### Style-6: Claude Official Warm
- **Soft shadows**: `<feDropShadow dx="0" dy="2" stdDeviation="6" flood-color="#00000008"/>`
- **Rounded corners**: rx="12" ry="12" (more rounded than Style-1)
- **Arrows**: medium weight (2px), subtle markers

## Validation Checklist

Before exporting PNG, verify:
- [ ] No arrow-component overlaps (visual inspection)
- [ ] Arrow labels are offset from lines; fallback background rects are used only where needed
- [ ] Minimum 60px clearance for all arrow paths
- [ ] Component spacing ≥ 80px
- [ ] Arrow connection points avoid corners (≥20px from corner)
- [ ] Multiple arrows between layers are staggered
- [ ] Legend is readable and doesn't overlap content
- [ ] SVG renders cleanly via `cairosvg` (or `rsvg-convert` as fallback)

## Common Anti-Patterns to Avoid

| Anti-Pattern | Fix |
|--------------|-----|
| Arrow crosses component | Use orthogonal routing, increase control point distance |
| Label overlaps component | Increase offset; add a matching-background rect if the collision remains |
| Components too close | Increase spacing to 80px minimum |
| Arrow connects to corner | Move connection point to edge midpoint offset |
| No z-index planning | Follow render order: arrows -> components -> text |
skills/fireworks-tech-graph/schemas/workflow-v1.schema.json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://yizhiyanhua-ai.github.io/fireworks-tech-graph/schemas/workflow-v1.schema.json",
  "allOf": [
    { "$ref": "diagram-v1.schema.json" },
    { "properties": { "mode": { "enum": ["flowchart", "agent", "state-machine"] } } }
  ]
}
skills/fireworks-tech-graph/schemas/sequence-v1.schema.json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://yizhiyanhua-ai.github.io/fireworks-tech-graph/schemas/sequence-v1.schema.json",
  "allOf": [
    { "$ref": "diagram-v1.schema.json" },
    { "properties": { "mode": { "const": "sequence" } } }
  ]
}
skills/fireworks-tech-graph/scripts/diagram_ir.py
#!/usr/bin/env python3
"""Typed, backwards-compatible input model for Fireworks Tech Graph.

The renderer keeps accepting the historical JSON shape. This module gives
that shape a versioned semantic boundary before any layout code runs.
"""

from __future__ import annotations

import copy
import math
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Mapping, Optional, Sequence


SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
    sys.path.insert(0, str(SCRIPT_DIR))

from semantic_contracts import resolve_style_index, validate_semantic_contract  # noqa: E402


class DiagramValidationError(ValueError):
    """Raised when input cannot be normalized to diagram schema v1."""


def _finite(value: Any, path: str) -> float:
    try:
        number = float(value)
    except (TypeError, ValueError) as error:
        raise DiagramValidationError(f"{path} must be a finite number") from error
    if not math.isfinite(number):
        raise DiagramValidationError(f"{path} must be a finite number")
    return number


@dataclass(frozen=True)
class NodeIR:
    node_id: str
    raw: Mapping[str, Any]


@dataclass(frozen=True)
class EdgeIR:
    edge_id: str
    source: Optional[str]
    target: Optional[str]
    waypoints: tuple[tuple[float, float], ...]
    raw: Mapping[str, Any]


@dataclass(frozen=True)
class DiagramIR:
    schema_version: int
    input_schema: str
    mode: str
    style_index: int
    semantic_report: Mapping[str, Any]
    nodes: tuple[NodeIR, ...]
    edges: tuple[EdgeIR, ...]
    raw: Mapping[str, Any]

    def as_dict(self) -> dict[str, Any]:
        return copy.deepcopy(dict(self.raw))


def _validate_numeric_fields(item: Mapping[str, Any], fields: Sequence[str], path: str) -> None:
    for field in fields:
        if field in item and item[field] is not None:
            _finite(item[field], f"{path}.{field}")


def normalize_diagram(data: Mapping[str, Any], expected_mode: str) -> DiagramIR:
    if not isinstance(data, Mapping):
        raise DiagramValidationError("diagram input must be a JSON object")
    normalized = copy.deepcopy(dict(data))
    input_schema = "v1" if "schema_version" in normalized else "legacy"
    schema_version = normalized.get("schema_version", 1)
    if isinstance(schema_version, bool) or schema_version != 1:
        raise DiagramValidationError(f"unsupported schema_version: {schema_version}")

    mode = str(normalized.get("mode") or normalized.get("template_type") or expected_mode)
    if normalized.get("mode") and mode != expected_mode:
        raise DiagramValidationError(f"mode {mode!r} conflicts with template type {expected_mode!r}")
    normalized["schema_version"] = 1
    normalized["mode"] = mode

    _validate_numeric_fields(normalized, ("width", "height"), "diagram")
    for field in ("width", "height"):
        if field in normalized and _finite(normalized[field], f"diagram.{field}") <= 0:
            raise DiagramValidationError(f"diagram.{field} must be greater than zero")

    raw_containers = normalized.get("containers", [])
    if not isinstance(raw_containers, list):
        raise DiagramValidationError("containers must be an array")
    containers: list[dict[str, Any]] = []
    container_ids: set[str] = set()
    for index, raw_container in enumerate(raw_containers):
        if not isinstance(raw_container, Mapping):
            raise DiagramValidationError(f"containers[{index}] must be an object")
        container = copy.deepcopy(dict(raw_container))
        container_id = str(container.get("id") or "").strip()
        if not container_id:
            raise DiagramValidationError(f"containers[{index}].id must be a non-empty string")
        if container_id in container_ids:
            raise DiagramValidationError(f"duplicate container id: {container_id}")
        container_ids.add(container_id)
        container["id"] = container_id
        _validate_numeric_fields(container, ("x", "y", "width", "height"), f"containers[{index}]")
        containers.append(container)
    normalized["containers"] = containers

    raw_nodes = normalized.get("nodes", [])
    if not isinstance(raw_nodes, list):
        raise DiagramValidationError("nodes must be an array")
    nodes: list[NodeIR] = []
    node_ids: set[str] = set()
    for index, raw_node in enumerate(raw_nodes):
        if not isinstance(raw_node, Mapping):
            raise DiagramValidationError(f"nodes[{index}] must be an object")
        node = copy.deepcopy(dict(raw_node))
        node_id = str(node.get("id") or f"node-{index:03d}")
        if node_id in node_ids:
            raise DiagramValidationError(f"duplicate node id: {node_id}")
        if node_id in container_ids:
            raise DiagramValidationError(f"duplicate diagram id: {node_id}")
        node_ids.add(node_id)
        node["id"] = node_id
        _validate_numeric_fields(node, ("x", "y", "width", "height", "r", "offset_y"), f"nodes[{index}]")
        nodes.append(NodeIR(node_id, node))
    normalized["nodes"] = [copy.deepcopy(dict(node.raw)) for node in nodes]

    if "edges" in normalized and "arrows" not in normalized:
        normalized["arrows"] = normalized.pop("edges")
    raw_edges = normalized.get("arrows", [])
    if not isinstance(raw_edges, list):
        raise DiagramValidationError("arrows must be an array")
    edges: list[EdgeIR] = []
    edge_ids: set[str] = set()
    for index, raw_edge in enumerate(raw_edges):
        if not isinstance(raw_edge, Mapping):
            raise DiagramValidationError(f"arrows[{index}] must be an object")
        edge = copy.deepcopy(dict(raw_edge))
        edge_id = str(edge.get("id") or f"edge-{index:03d}")
        if edge_id in edge_ids:
            raise DiagramValidationError(f"duplicate edge id: {edge_id}")
        if edge_id in container_ids or edge_id in node_ids:
            raise DiagramValidationError(f"duplicate diagram id: {edge_id}")
        edge_ids.add(edge_id)
        edge["id"] = edge_id
        source = str(edge["source"]) if edge.get("source") is not None else None
        target = str(edge["target"]) if edge.get("target") is not None else None
        for endpoint_name, endpoint in (("source", source), ("target", target)):
            if endpoint is not None and endpoint not in node_ids:
                raise DiagramValidationError(f"arrows[{index}].{endpoint_name} references unknown node: {endpoint}")
        _validate_numeric_fields(
            edge,
            ("x1", "y1", "x2", "y2", "label_dx", "label_dy", "routing_padding", "port_clearance"),
            f"arrows[{index}]",
        )
        raw_waypoints = edge.get("route_points", [])
        if not isinstance(raw_waypoints, list):
            raise DiagramValidationError(f"arrows[{index}].route_points must be an array")
        waypoints: list[tuple[float, float]] = []
        for waypoint_index, raw_waypoint in enumerate(raw_waypoints):
            if not isinstance(raw_waypoint, (list, tuple)) or len(raw_waypoint) != 2:
                raise DiagramValidationError(
                    f"arrows[{index}].route_points[{waypoint_index}] must be [x, y]"
                )
            waypoint = (
                _finite(raw_waypoint[0], f"arrows[{index}].route_points[{waypoint_index}][0]"),
                _finite(raw_waypoint[1], f"arrows[{index}].route_points[{waypoint_index}][1]"),
            )
            waypoints.append(waypoint)
        edge["route_points"] = [[x, y] for x, y in waypoints]
        edges.append(EdgeIR(edge_id, source, target, tuple(waypoints), edge))
    normalized["arrows"] = [copy.deepcopy(dict(edge.raw)) for edge in edges]

    style_index = resolve_style_index(normalized)
    semantic_report = validate_semantic_contract(normalized)
    return DiagramIR(
        1,
        input_schema,
        mode,
        style_index,
        semantic_report,
        tuple(nodes),
        tuple(edges),
        normalized,
    )


__all__ = [
    "DiagramIR",
    "DiagramValidationError",
    "EdgeIR",
    "NodeIR",
    "normalize_diagram",
]
skills/fireworks-tech-graph/scripts/fireworks_geometry.py
#!/usr/bin/env python3
"""Deterministic geometry primitives shared by the renderer and SVG checker.

The module deliberately uses only the Python standard library.  Generated
diagrams can therefore enforce the same routing contract in a fresh Agent
Skill install, in CI, and in the post-render artifact checker.
"""

from __future__ import annotations

import math
import unicodedata
from dataclasses import dataclass
from typing import Iterable, Optional, Sequence


Point = tuple[float, float]
Bounds = tuple[float, float, float, float]
EPSILON = 1e-6


@dataclass(frozen=True)
class SegmentInteraction:
    kind: str
    point: Optional[Point] = None
    overlap_length: float = 0.0


@dataclass(frozen=True)
class RouteInteractions:
    crossings: tuple[Point, ...]
    overlap_count: int
    overlap_length: float


def almost_equal(first: float, second: float, epsilon: float = EPSILON) -> bool:
    return abs(first - second) <= epsilon


def same_point(first: Point, second: Point, epsilon: float = EPSILON) -> bool:
    return almost_equal(first[0], second[0], epsilon) and almost_equal(first[1], second[1], epsilon)


def segment_axis(first: Point, second: Point) -> str:
    if almost_equal(first[1], second[1]):
        return "horizontal"
    if almost_equal(first[0], second[0]):
        return "vertical"
    return "other"


def route_is_orthogonal(points: Sequence[Point]) -> bool:
    return all(segment_axis(first, second) != "other" for first, second in zip(points, points[1:]))


def route_length(points: Sequence[Point]) -> float:
    return sum(math.hypot(second[0] - first[0], second[1] - first[1]) for first, second in zip(points, points[1:]))


def bend_count(points: Sequence[Point]) -> int:
    axes = [segment_axis(first, second) for first, second in zip(points, points[1:]) if not same_point(first, second)]
    return sum(first != second for first, second in zip(axes, axes[1:]))


def bounds_intersect(first: Bounds, second: Bounds, padding: float = 0.0) -> bool:
    left_a, top_a, right_a, bottom_a = first
    left_b, top_b, right_b, bottom_b = second
    return not (
        right_a + padding <= left_b
        or right_b + padding <= left_a
        or bottom_a + padding <= top_b
        or bottom_b + padding <= top_a
    )


def expand_bounds(bounds: Bounds, padding: float) -> Bounds:
    left, top, right, bottom = bounds
    return (left - padding, top - padding, right + padding, bottom + padding)


def point_in_bounds(point: Point, bounds: Bounds, *, padding: float = 0.0, interior: bool = False) -> bool:
    x, y = point
    left, top, right, bottom = bounds
    if interior:
        return left + padding < x < right - padding and top + padding < y < bottom - padding
    return left - padding <= x <= right + padding and top - padding <= y <= bottom + padding


def bounds_inside(inner: Bounds, outer: Bounds, padding: float = 0.0) -> bool:
    left, top, right, bottom = inner
    outer_left, outer_top, outer_right, outer_bottom = outer
    return (
        left >= outer_left + padding
        and top >= outer_top + padding
        and right <= outer_right - padding
        and bottom <= outer_bottom - padding
    )


def route_inside_canvas(points: Sequence[Point], canvas: Bounds, margin: float = 0.0) -> bool:
    left, top, right, bottom = canvas
    safe = (left + margin, top + margin, right - margin, bottom - margin)
    return all(point_in_bounds(point, safe) for point in points)


def _orientation(first: Point, second: Point, third: Point) -> float:
    return (second[0] - first[0]) * (third[1] - first[1]) - (second[1] - first[1]) * (third[0] - first[0])


def _on_segment(point: Point, first: Point, second: Point, epsilon: float = EPSILON) -> bool:
    return (
        min(first[0], second[0]) - epsilon <= point[0] <= max(first[0], second[0]) + epsilon
        and min(first[1], second[1]) - epsilon <= point[1] <= max(first[1], second[1]) + epsilon
        and abs(_orientation(first, second, point)) <= epsilon
    )


def segment_interaction(first_a: Point, first_b: Point, second_a: Point, second_b: Point) -> Optional[SegmentInteraction]:
    """Return a proper crossing, touch, or collinear overlap for two segments."""

    first_axis = segment_axis(first_a, first_b)
    second_axis = segment_axis(second_a, second_b)

    if first_axis == second_axis == "horizontal" and almost_equal(first_a[1], second_a[1]):
        start = max(min(first_a[0], first_b[0]), min(second_a[0], second_b[0]))
        end = min(max(first_a[0], first_b[0]), max(second_a[0], second_b[0]))
        if end - start > EPSILON:
            return SegmentInteraction("overlap", overlap_length=end - start)
        if almost_equal(start, end):
            return SegmentInteraction("touch", (start, first_a[1]))
        return None

    if first_axis == second_axis == "vertical" and almost_equal(first_a[0], second_a[0]):
        start = max(min(first_a[1], first_b[1]), min(second_a[1], second_b[1]))
        end = min(max(first_a[1], first_b[1]), max(second_a[1], second_b[1]))
        if end - start > EPSILON:
            return SegmentInteraction("overlap", overlap_length=end - start)
        if almost_equal(start, end):
            return SegmentInteraction("touch", (first_a[0], start))
        return None

    if first_axis == "horizontal" and second_axis == "vertical":
        point = (second_a[0], first_a[1])
        if _on_segment(point, first_a, first_b) and _on_segment(point, second_a, second_b):
            return SegmentInteraction("crossing", point)
        return None

    if first_axis == "vertical" and second_axis == "horizontal":
        point = (first_a[0], second_a[1])
        if _on_segment(point, first_a, first_b) and _on_segment(point, second_a, second_b):
            return SegmentInteraction("crossing", point)
        return None

    # General line intersection keeps the artifact checker useful for authored
    # SVGs and sampled curves. Collinear diagonal overlap is intentionally
    # treated conservatively as an overlap.
    o1 = _orientation(first_a, first_b, second_a)
    o2 = _orientation(first_a, first_b, second_b)
    o3 = _orientation(second_a, second_b, first_a)
    o4 = _orientation(second_a, second_b, first_b)
    if abs(o1) <= EPSILON and abs(o2) <= EPSILON and abs(o3) <= EPSILON and abs(o4) <= EPSILON:
        candidates = [point for point in (first_a, first_b, second_a, second_b) if _on_segment(point, first_a, first_b) and _on_segment(point, second_a, second_b)]
        unique = unique_points(candidates)
        if len(unique) >= 2:
            return SegmentInteraction("overlap", overlap_length=max(math.dist(a, b) for a in unique for b in unique))
        if unique:
            return SegmentInteraction("touch", unique[0])
        return None
    if o1 * o2 <= EPSILON and o3 * o4 <= EPSILON:
        denominator = (first_a[0] - first_b[0]) * (second_a[1] - second_b[1]) - (first_a[1] - first_b[1]) * (second_a[0] - second_b[0])
        if abs(denominator) <= EPSILON:
            return None
        determinant_first = first_a[0] * first_b[1] - first_a[1] * first_b[0]
        determinant_second = second_a[0] * second_b[1] - second_a[1] * second_b[0]
        x = (determinant_first * (second_a[0] - second_b[0]) - (first_a[0] - first_b[0]) * determinant_second) / denominator
        y = (determinant_first * (second_a[1] - second_b[1]) - (first_a[1] - first_b[1]) * determinant_second) / denominator
        point = (x, y)
        if _on_segment(point, first_a, first_b) and _on_segment(point, second_a, second_b):
            return SegmentInteraction("crossing", point)
    return None


def unique_points(points: Iterable[Point], tolerance: float = 0.01) -> list[Point]:
    result: list[Point] = []
    for point in points:
        rounded = (round(point[0], 2), round(point[1], 2))
        if not any(same_point(rounded, existing, tolerance) for existing in result):
            result.append(rounded)
    return result


def is_shared_route_endpoint(point: Point, first: Sequence[Point], second: Sequence[Point], tolerance: float = 0.01) -> bool:
    return any(same_point(point, endpoint, tolerance) for endpoint in (first[0], first[-1])) and any(
        same_point(point, endpoint, tolerance) for endpoint in (second[0], second[-1])
    )


def route_interactions(route: Sequence[Point], others: Sequence[Sequence[Point]]) -> RouteInteractions:
    crossings: list[Point] = []
    overlap_count = 0
    overlap_length = 0.0
    for other in others:
        if len(other) < 2:
            continue
        for first_a, first_b in zip(route, route[1:]):
            for second_a, second_b in zip(other, other[1:]):
                interaction = segment_interaction(first_a, first_b, second_a, second_b)
                if interaction is None:
                    continue
                if interaction.kind == "overlap":
                    overlap_count += 1
                    overlap_length += interaction.overlap_length
                    continue
                if interaction.point is None or is_shared_route_endpoint(interaction.point, route, other):
                    continue
                if interaction.kind in {"crossing", "touch"}:
                    crossings.append(interaction.point)
    return RouteInteractions(tuple(unique_points(crossings)), overlap_count, round(overlap_length, 2))


def route_crossing_count(route: Sequence[Point], others: Sequence[Sequence[Point]]) -> int:
    return len(route_interactions(route, others).crossings)


def route_overlap_length(route: Sequence[Point], others: Sequence[Sequence[Point]]) -> float:
    return route_interactions(route, others).overlap_length


def estimate_text_width(text: str, font_size: float = 12.0, weight: float = 1.0) -> float:
    units = 0.0
    for character in text:
        if unicodedata.combining(character):
            continue
        east_asian = unicodedata.east_asian_width(character)
        if east_asian in {"W", "F"}:
            units += 1.0
        elif character.isspace():
            units += 0.36
        elif character in "ilI.,:;!'`|":
            units += 0.32
        elif character in "MW@#%&":
            units += 0.82
        else:
            units += 0.58
    return max(font_size * 1.5, units * font_size * weight)


def estimate_text_bounds(
    x: float,
    y: float,
    text: str,
    *,
    font_size: float = 12.0,
    anchor: str = "start",
    padding: float = 0.0,
) -> Bounds:
    width = estimate_text_width(text, font_size)
    if anchor == "middle":
        left = x - width / 2
    elif anchor == "end":
        left = x - width
    else:
        left = x
    top = y - font_size * 0.82
    return (left - padding, top - padding, left + width + padding, y + font_size * 0.24 + padding)


def parse_bridge_points(raw: Optional[str]) -> list[Point]:
    if not raw:
        return []
    points: list[Point] = []
    for token in raw.split(";"):
        parts = token.strip().split(",")
        if len(parts) != 2:
            continue
        try:
            points.append((float(parts[0]), float(parts[1])))
        except ValueError:
            continue
    return points


def bridge_declared(point: Point, bridges: Sequence[Point], tolerance: float = 1.0) -> bool:
    return any(same_point(point, bridge, tolerance) for bridge in bridges)


def format_number(value: float) -> str:
    rounded = round(value, 2)
    if float(rounded).is_integer():
        return str(int(rounded))
    return str(rounded)


def path_with_bridges(route: Sequence[Point], bridges: Sequence[Point], radius: float = 5.0) -> str:
    """Build an SVG path, adding a deterministic arc at declared crossings."""

    if not route:
        return ""
    commands = [f"M {format_number(route[0][0])},{format_number(route[0][1])}"]
    remaining = unique_points(bridges)
    for start, end in zip(route, route[1:]):
        axis = segment_axis(start, end)
        if axis == "horizontal":
            direction = 1.0 if end[0] >= start[0] else -1.0
            candidates = [
                point
                for point in remaining
                if almost_equal(point[1], start[1], 0.1)
                and min(start[0], end[0]) + radius * 1.5 < point[0] < max(start[0], end[0]) - radius * 1.5
            ]
            candidates.sort(key=lambda point: direction * point[0])
            last_coordinate = start[0]
            for x, y in candidates:
                if abs(x - last_coordinate) < radius * 2.5:
                    continue
                before = x - direction * radius
                after = x + direction * radius
                commands.append(f"L {format_number(before)},{format_number(y)}")
                sweep = 0 if direction > 0 else 1
                commands.append(f"A {format_number(radius)} {format_number(radius)} 0 0 {sweep} {format_number(after)},{format_number(y)}")
                last_coordinate = after
            commands.append(f"L {format_number(end[0])},{format_number(end[1])}")
        elif axis == "vertical":
            direction = 1.0 if end[1] >= start[1] else -1.0
            candidates = [
                point
                for point in remaining
                if almost_equal(point[0], start[0], 0.1)
                and min(start[1], end[1]) + radius * 1.5 < point[1] < max(start[1], end[1]) - radius * 1.5
            ]
            candidates.sort(key=lambda point: direction * point[1])
            last_coordinate = start[1]
            for x, y in candidates:
                if abs(y - last_coordinate) < radius * 2.5:
                    continue
                before = y - direction * radius
                after = y + direction * radius
                commands.append(f"L {format_number(x)},{format_number(before)}")
                sweep = 1 if direction > 0 else 0
                commands.append(f"A {format_number(radius)} {format_number(radius)} 0 0 {sweep} {format_number(x)},{format_number(after)}")
                last_coordinate = after
            commands.append(f"L {format_number(end[0])},{format_number(end[1])}")
        else:
            commands.append(f"L {format_number(end[0])},{format_number(end[1])}")
    return " ".join(commands)
skills/fireworks-tech-graph/scripts/motion.py
#!/usr/bin/env python3
"""Validated SVG-to-GIF motion planning for semantic technical diagrams."""

from __future__ import annotations

import hashlib
import json
import math
import os
import re
import shutil
import subprocess
import tempfile
import uuid
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Optional

from interactive_html import sanitize_svg
from validate_svg import run_check


SCRIPT_DIR = Path(__file__).resolve().parent
MOTION_WORKER = SCRIPT_DIR / "svg2gif.js"
STYLE_PRESETS = {
    1: "memory-weave",
    2: "tool-grounding",
    3: "service-blueprint",
    4: "memory-lifecycle",
    5: "agent-orchestration",
    6: "governed-runtime",
    7: "token-stream",
    8: "golden-circuit",
    9: "review-trace",
    10: "cloud-flow",
    11: "event-transit",
    12: "ops-pulse",
}
PRESET_STYLES = {preset: style for style, preset in STYLE_PRESETS.items()}
MOTION_PRESETS = tuple(STYLE_PRESETS.values())
REVIEWED_STYLE_IDS = frozenset(range(1, 13))
STYLE_MOTION_ROLES = {
    1: {"ingress", "reason", "extract", "transform", "resolve", "memory-write", "memory-read", "response-context"},
    2: {"ingress", "delegate", "tool-call", "inspect", "index", "grounding", "answer"},
    3: {"ingress", "policy", "fanout", "data-write", "event", "telemetry"},
    4: {"sample", "attend", "invoke", "remember", "consolidate", "recall"},
    5: {"ingress", "delegate", "evidence", "artifact", "context", "deliver", "approval"},
    6: {"ingress", "dispatch", "runtime-branch", "foundation", "promote"},
    7: {"connect", "prepare", "invoke", "tool-call", "token-stream", "govern", "measure", "promote"},
    8: {"primary", "memory-read", "tool-call", "data", "trace", "feedback"},
    9: {"review-entry", "review-request", "review-async", "review-state", "review-external"},
    10: {"global-route", "regional-write", "cross-region"},
    11: {"topic-rail", "dead-letter", "state-project"},
    12: {"critical-request", "telemetry-export"},
}
CHECKS = ("xml", "markers", "geometry", "composition")
MAX_INPUT_BYTES = 20 * 1024 * 1024
MAX_RENDERED_PIXELS = 600_000_000
MOTION_FORMAT = {"suffix": ".gif", "name": "gif", "mime": "image/gif", "loop_playback": "embedded-infinite"}
MOTION_TIMEOUTS = {"runtime_probe": 15, "render": 120, "encode": 120, "media_probe": 30}
MOTION_SIZE_TARGET_BYTES = 500_000
DEFAULT_MOTION_DURATION = 5.75
DEFAULT_MOTION_FPS = 20
DEFAULT_MOTION_FRAME_COUNT = 115
MINIMUM_MOTION_FRAME_COUNT = 55
MOTION_GRAMMAR_VERSION = "3.4"
APPROVED_BASELINE_DURATION = 3.75
APPROVED_BASELINE_FRAME_COUNT = 75
TIMING_REVISION_ID = "+2s-settled-flow"
TIMING_REVISION_APPROVED_AT = "2026-07-17"
MOTION_CURVES = {
    "draw": "linear",
    "persistent-data-flow": "linear",
    "reset": "linear",
}
SCENE_SIGNATURES = {
    1: {
        "name": "memory-weave-draw-on-persistent-data-flow",
        "distinctive_primitives": [
            "semantic-route-build",
            "settled-marker-arrival",
            "eight-route-persistent-data-flow",
        ],
    },
    2: {
        "name": "tool-grounding-terminal-evidence-trace",
        "distinctive_primitives": [
            "semantic-route-build",
            "settled-marker-arrival",
            "eight-route-terminal-evidence-stream",
            "terminal-prompt-cursor",
        ],
    },
    3: {
        "name": "service-blueprint-distribution-wave",
        "distinctive_primitives": [
            "semantic-route-build",
            "settled-marker-arrival",
            "ten-route-blueprint-distribution-wave",
            "blueprint-registration-bead",
        ],
    },
    4: {
        "name": "memory-lifecycle-notion-card-handoff",
        "distinctive_primitives": [
            "semantic-route-build",
            "settled-marker-arrival",
            "six-route-notion-memory-rail",
            "notion-memory-card",
        ],
    },
    5: {
        "name": "glassmorphism-multi-agent-task-capsules",
        "distinctive_primitives": ["semantic-route-build", "glass-handoff-rail", "glass-task-capsule", "coordinator-halo"],
    },
    6: {
        "name": "claude-official-governed-runtime-policy-seals",
        "distinctive_primitives": ["semantic-route-build", "governance-thread", "policy-seal"],
    },
    7: {
        "name": "openai-official-api-token-train",
        "distinctive_primitives": ["semantic-route-build", "api-token-rail", "token-train"],
    },
    8: {
        "name": "dark-luxury-golden-gem-circuit",
        "distinctive_primitives": ["semantic-route-build", "luxury-circuit-rail", "gem-tracer"],
    },
    9: {
        "name": "c4-review-canvas-moving-review-cursors",
        "distinctive_primitives": ["semantic-route-build", "review-trace-rail", "review-cursor"],
    },
    10: {
        "name": "cloud-fabric-active-active-synchronized-regions",
        "distinctive_primitives": ["semantic-route-build", "cloud-flow-rail", "region-chevron-pair", "replication-capsule", "availability-pulse"],
    },
    11: {
        "name": "event-transit-three-car-event-trains",
        "distinctive_primitives": ["semantic-route-build", "event-transit-rail", "event-train", "exception-car", "projection-car", "station-dwell-ring"],
    },
    12: {
        "name": "ops-pulse-incident-path-waterfall-scanner",
        "distinctive_primitives": ["semantic-route-build", "incident-pulse-rail", "ecg-head", "telemetry-export-packet", "trace-span-reveal", "waterfall-scanner"],
    },
}
STYLE_1_DRAW_SCHEDULE = [
    {"role": "ingress", "frames": [1, 8]},
    {"role": "reason", "frames": [5, 12]},
    {"role": "extract", "frames": [9, 16]},
    {"role": "transform", "frames": [13, 20]},
    {"role": "resolve", "frames": [17, 24]},
    {"role": "memory-write", "frames": [21, 28]},
    {"role": "memory-read", "frames": [25, 32]},
    {"role": "response-context", "frames": [29, 36]},
]
STYLE_2_DRAW_SCHEDULE = [
    {"role": "ingress", "stage": 1, "order": 0, "frames": [1, 8]},
    {"role": "delegate", "stage": 2, "order": 0, "frames": [5, 12]},
    {"role": "tool-call", "stage": 3, "order": 0, "frames": [9, 16]},
    {"role": "inspect", "stage": 4, "order": 0, "frames": [13, 20]},
    {"role": "index", "stage": 5, "order": 0, "frames": [17, 24]},
    {"role": "grounding", "stage": 6, "order": 0, "frames": [21, 28]},
    {"role": "grounding", "stage": 6, "order": 1, "frames": [25, 32]},
    {"role": "answer", "stage": 7, "order": 0, "frames": [29, 36]},
]
STYLE_3_DRAW_SCHEDULE = [
    {"role": "ingress", "stage": 1, "order": 0, "frames": [1, 6]},
    {"role": "policy", "stage": 2, "order": 0, "frames": [4, 9]},
    {"role": "fanout", "stage": 3, "order": 0, "frames": [8, 13]},
    {"role": "fanout", "stage": 3, "order": 1, "frames": [11, 16]},
    {"role": "fanout", "stage": 3, "order": 2, "frames": [14, 19]},
    {"role": "data-write", "stage": 4, "order": 0, "frames": [18, 23]},
    {"role": "data-write", "stage": 4, "order": 1, "frames": [21, 26]},
    {"role": "data-write", "stage": 4, "order": 2, "frames": [24, 29]},
    {"role": "event", "stage": 5, "order": 0, "frames": [28, 33]},
    {"role": "telemetry", "stage": 6, "order": 0, "frames": [31, 36]},
]
STYLE_4_DRAW_SCHEDULE = [
    {"role": "sample", "stage": 1, "order": 0, "frames": [1, 4]},
    {"role": "attend", "stage": 2, "order": 0, "frames": [5, 8]},
    {"role": "invoke", "stage": 3, "order": 0, "frames": [9, 12]},
    {"role": "remember", "stage": 4, "order": 0, "frames": [13, 22]},
    {"role": "consolidate", "stage": 5, "order": 0, "frames": [23, 26]},
    {"role": "recall", "stage": 6, "order": 0, "frames": [27, 36]},
]
STYLE_5_DRAW_SCHEDULE = [
    {"role": "ingress", "stage": 1, "order": 0, "frames": [1, 6]},
    {"role": "delegate", "stage": 2, "order": 0, "frames": [5, 12]},
    {"role": "delegate", "stage": 2, "order": 1, "frames": [8, 15]},
    {"role": "delegate", "stage": 2, "order": 2, "frames": [11, 18]},
    {"role": "evidence", "stage": 3, "order": 0, "frames": [17, 24]},
    {"role": "artifact", "stage": 3, "order": 1, "frames": [20, 27]},
    {"role": "context", "stage": 4, "order": 0, "frames": [25, 30]},
    {"role": "deliver", "stage": 5, "order": 0, "frames": [29, 36]},
    {"role": "approval", "stage": 5, "order": 1, "frames": [29, 36]},
]
STYLE_6_DRAW_SCHEDULE = [
    {"role": "ingress", "stage": 1, "order": 0, "frames": [1, 6]},
    {"role": "dispatch", "stage": 2, "order": 0, "frames": [5, 12]},
    {"role": "runtime-branch", "stage": 3, "order": 0, "frames": [10, 17]},
    {"role": "runtime-branch", "stage": 3, "order": 1, "frames": [13, 20]},
    {"role": "runtime-branch", "stage": 3, "order": 2, "frames": [16, 23]},
    {"role": "foundation", "stage": 4, "order": 0, "frames": [21, 28]},
    {"role": "foundation", "stage": 4, "order": 1, "frames": [24, 31]},
    {"role": "foundation", "stage": 4, "order": 2, "frames": [27, 34]},
    {"role": "promote", "stage": 5, "order": 0, "frames": [31, 36]},
]
STYLE_7_DRAW_SCHEDULE = [
    {"role": "connect", "stage": 1, "order": 0, "frames": [1, 6]},
    {"role": "prepare", "stage": 2, "order": 0, "frames": [5, 12]},
    {"role": "invoke", "stage": 3, "order": 0, "frames": [10, 17]},
    {"role": "tool-call", "stage": 4, "order": 0, "frames": [15, 22]},
    {"role": "token-stream", "stage": 4, "order": 1, "frames": [18, 27]},
    {"role": "govern", "stage": 5, "order": 0, "frames": [25, 32]},
    {"role": "measure", "stage": 5, "order": 1, "frames": [25, 32]},
    {"role": "promote", "stage": 6, "order": 0, "frames": [31, 36]},
]
STYLE_8_DRAW_SCHEDULE = [
    {"role": "primary", "stage": 1, "order": 0, "frames": [1, 6]},
    {"role": "primary", "stage": 2, "order": 0, "frames": [5, 10]},
    {"role": "memory-read", "stage": 3, "order": 0, "frames": [9, 18]},
    {"role": "tool-call", "stage": 3, "order": 1, "frames": [12, 21]},
    {"role": "data", "stage": 4, "order": 0, "frames": [20, 25]},
    {"role": "trace", "stage": 5, "order": 0, "frames": [24, 29]},
    {"role": "feedback", "stage": 6, "order": 0, "frames": [28, 36]},
]
STYLE_9_DRAW_SCHEDULE = [
    {"role": "review-entry", "stage": 1, "order": 0, "frames": [1, 7]},
    {"role": "review-request", "stage": 2, "order": 0, "frames": [7, 13]},
    {"role": "review-async", "stage": 3, "order": 0, "frames": [13, 22]},
    {"role": "review-state", "stage": 4, "order": 0, "frames": [22, 30]},
    {"role": "review-external", "stage": 4, "order": 1, "frames": [28, 36]},
]
STYLE_10_DRAW_SCHEDULE = [
    {"role": "global-route", "stage": 1, "order": 0, "frames": [1, 12]},
    {"role": "global-route", "stage": 1, "order": 1, "frames": [1, 12]},
    {"role": "regional-write", "stage": 2, "order": 0, "frames": [13, 22]},
    {"role": "regional-write", "stage": 2, "order": 1, "frames": [13, 22]},
    {"role": "cross-region", "stage": 3, "order": 0, "frames": [23, 36]},
]
STYLE_11_DRAW_SCHEDULE = [
    {"role": "topic-rail", "stage": 1, "order": 0, "frames": [1, 6]},
    {"role": "topic-rail", "stage": 2, "order": 0, "frames": [7, 12]},
    {"role": "topic-rail", "stage": 3, "order": 0, "frames": [13, 18]},
    {"role": "topic-rail", "stage": 4, "order": 0, "frames": [19, 24]},
    {"role": "dead-letter", "stage": 5, "order": 0, "frames": [25, 32]},
    {"role": "state-project", "stage": 5, "order": 1, "frames": [29, 36]},
]
STYLE_12_DRAW_SCHEDULE = [
    {"role": "critical-request", "stage": 1, "order": 0, "frames": [1, 6]},
    {"role": "critical-request", "stage": 2, "order": 0, "frames": [7, 12]},
    {"role": "critical-request", "stage": 3, "order": 0, "frames": [13, 18]},
    {"role": "telemetry-export", "stage": 4, "order": 0, "frames": [19, 26]},
]
# Backwards-compatible public name for the approved Style 1 report contract.
DRAW_SCHEDULE = STYLE_1_DRAW_SCHEDULE
RESET_OPACITY_SAMPLES = [1.0, 0.7575, 0.515, 0.2725, 0.03]

STYLE_SCENE_CONTRACTS = {
    1: {
        "preset": "memory-weave",
        "draw_schedule": STYLE_1_DRAW_SCHEDULE,
        "schedule_keys": [
            ("ingress", 0),
            ("reason", 0),
            ("extract", 0),
            ("transform", 0),
            ("resolve", 1),
            ("memory-write", 0),
            ("memory-read", 0),
            ("response-context", 0),
        ],
        "expected_stages": [1, 2, 3, 4, 4, 5, 6, 7],
        "stream_primitive": "persistent-data-flow-stream",
        "packet_head_primitive": "persistent-data-flow-head",
        "signature_primitive": None,
        "route_label_count": 8,
    },
    2: {
        "preset": "tool-grounding",
        "draw_schedule": STYLE_2_DRAW_SCHEDULE,
        "schedule_keys": [
            ("ingress", 0),
            ("delegate", 0),
            ("tool-call", 0),
            ("inspect", 0),
            ("index", 0),
            ("grounding", 0),
            ("grounding", 1),
            ("answer", 0),
        ],
        "expected_stages": [1, 2, 3, 4, 5, 6, 6, 7],
        "stream_primitive": "terminal-evidence-stream",
        "packet_head_primitive": "terminal-command-head",
        "signature_primitive": "terminal-prompt-cursor",
        "route_label_count": 8,
    },
    3: {
        "preset": "service-blueprint",
        "draw_schedule": STYLE_3_DRAW_SCHEDULE,
        "schedule_keys": [
            ("ingress", 0),
            ("policy", 0),
            ("fanout", 0),
            ("fanout", 1),
            ("fanout", 2),
            ("data-write", 0),
            ("data-write", 1),
            ("data-write", 2),
            ("event", 0),
            ("telemetry", 0),
        ],
        "expected_stages": [1, 2, 3, 3, 3, 4, 4, 4, 5, 6],
        "stream_primitive": "blueprint-distribution-wave",
        "packet_head_primitive": None,
        "registration_bead_primitive": "blueprint-registration-bead",
        "signature_primitive": None,
        "route_label_count": 7,
        "source_sha256": "b8f55d9ea0c6111176d8ff50d2e844b2001ee5087a3940621e635e1b875d470d",
    },
    4: {
        "preset": "memory-lifecycle",
        "draw_schedule": STYLE_4_DRAW_SCHEDULE,
        "schedule_keys": [
            ("sample", 0),
            ("attend", 0),
            ("invoke", 0),
            ("remember", 0),
            ("consolidate", 0),
            ("recall", 0),
        ],
        "expected_stages": [1, 2, 3, 4, 5, 6],
        "stream_primitive": "notion-memory-rail",
        "packet_head_primitive": None,
        "memory_card_primitive": "notion-memory-card",
        "signature_primitive": None,
        "route_label_count": 2,
        "source_sha256": "04cf833659e82c3e1743db4042cacf839a6d784a99c32d076e36fd4776e70c1b",
    },
    5: {
        "preset": "agent-orchestration",
        "draw_schedule": STYLE_5_DRAW_SCHEDULE,
        "schedule_keys": [("ingress", 0), ("delegate", 0), ("delegate", 1), ("delegate", 2), ("evidence", 0), ("artifact", 1), ("context", 0), ("deliver", 0), ("approval", 1)],
        "expected_stages": [1, 2, 2, 2, 3, 3, 4, 5, 5],
        "stream_primitive": "glass-handoff-rail",
        "signature_primitive": "glass-task-capsule",
        "route_label_count": 9,
        "maximum_concurrent_draws": 2,
        "fixture_sha256": "f4664045331c73179c312482b4d68d474513a059ede426891e097e615722b6a9",
        "source_sha256": "52bf52e8ac0b129fcfad8dcd06e93468b3b79e29e9e0f919be80cb58046c0991",
    },
    6: {
        "preset": "governed-runtime",
        "draw_schedule": STYLE_6_DRAW_SCHEDULE,
        "schedule_keys": [("ingress", 0), ("dispatch", 0), ("runtime-branch", 0), ("runtime-branch", 1), ("runtime-branch", 2), ("foundation", 0), ("foundation", 1), ("foundation", 2), ("promote", 0)],
        "expected_stages": [1, 2, 3, 3, 3, 4, 4, 4, 5],
        "stream_primitive": "governance-thread",
        "signature_primitive": "policy-seal",
        "route_label_count": 9,
        "maximum_concurrent_draws": 2,
        "fixture_sha256": "427127297757d7672ab365e37983a2a09bc55b26a420be9b44dc67e7ac5b9553",
        "source_sha256": "25847c17def77f9b9da1b9320504e31841c28cbcf35f56602d2f4dd76a40c772",
    },
    7: {
        "preset": "token-stream",
        "draw_schedule": STYLE_7_DRAW_SCHEDULE,
        "schedule_keys": [("connect", 0), ("prepare", 0), ("invoke", 0), ("tool-call", 0), ("token-stream", 1), ("govern", 0), ("measure", 1), ("promote", 0)],
        "expected_stages": [1, 2, 3, 4, 4, 5, 5, 6],
        "stream_primitive": "api-token-rail",
        "signature_primitive": "token-train",
        "route_label_count": 8,
        "maximum_concurrent_draws": 3,
        "fixture_sha256": "4d03096787cceb3e2be61567cf12996291dd46d2289bd5394cb30360b48a4473",
        "source_sha256": "ce07fd5279c5709b4546c59007068ca678b92122ed740d43270ecd22f7bbf82b",
    },
    8: {
        "preset": "golden-circuit",
        "draw_schedule": STYLE_8_DRAW_SCHEDULE,
        "schedule_keys": [("primary", 1, 0), ("primary", 2, 0), ("memory-read", 3, 0), ("tool-call", 3, 1), ("data", 4, 0), ("trace", 5, 0), ("feedback", 6, 0)],
        "expected_stages": [1, 2, 3, 3, 4, 5, 6],
        "stage_aware_schedule_key": True,
        "stream_primitive": "luxury-circuit-rail",
        "signature_primitive": "gem-tracer",
        "route_label_count": 7,
        "maximum_concurrent_draws": 2,
        "fixture_sha256": "6ade2db83f0fa772c4791e1a09a2c128373125da3bcfa2624434daff72316122",
        "source_sha256": "6ade2db83f0fa772c4791e1a09a2c128373125da3bcfa2624434daff72316122",
    },
    9: {
        "preset": "review-trace",
        "draw_schedule": STYLE_9_DRAW_SCHEDULE,
        "schedule_keys": [("review-entry", 0), ("review-request", 0), ("review-async", 0), ("review-state", 0), ("review-external", 1)],
        "expected_stages": [1, 2, 3, 4, 4],
        "stream_primitive": "review-trace-rail",
        "signature_primitive": "review-cursor",
        "route_label_count": 5,
        "maximum_concurrent_draws": 2,
        "fixture_sha256": "a8a0bccddc4b9b762286f3a7f21a5c1fdb98ea32f436d573bb7d3c14d7be27a9",
        "source_sha256": "b45264d17910fda296a7b52e7a338f361d8272ff14da55d2cad8c6e8dabe2717",
    },
    10: {
        "preset": "cloud-flow",
        "draw_schedule": STYLE_10_DRAW_SCHEDULE,
        "schedule_keys": [("global-route", 0), ("global-route", 1), ("regional-write", 0), ("regional-write", 1), ("cross-region", 0)],
        "expected_stages": [1, 1, 2, 2, 3],
        "stream_primitive": "cloud-flow-rail",
        "signature_primitive": "region-chevron-pair-or-replication-capsule",
        "route_label_count": 3,
        "maximum_concurrent_draws": 2,
        "fixture_sha256": "8c180ef8cecdc2419c79912245329ac9fffcc9ff08f8faeeb65541d21c747d4e",
        "source_sha256": "a739db50e30e0669dcfc926f4ce141c655f0b8f2e4e71c6a45444c9b3e61074a",
    },
    11: {
        "preset": "event-transit",
        "draw_schedule": STYLE_11_DRAW_SCHEDULE,
        "schedule_keys": [("topic-rail", 1, 0), ("topic-rail", 2, 0), ("topic-rail", 3, 0), ("topic-rail", 4, 0), ("dead-letter", 5, 0), ("state-project", 5, 1)],
        "expected_stages": [1, 2, 3, 4, 5, 5],
        "stage_aware_schedule_key": True,
        "stream_primitive": "event-transit-rail",
        "signature_primitive": "event-train-or-branch-car",
        "route_label_count": 0,
        "maximum_concurrent_draws": 2,
        "fixture_sha256": "a9fbd96129c9fc54b97b80024cb954d03d471d4c2963f99e71eff276d15d6140",
        "source_sha256": "e4ca3aa987b2495868765a0b66d11763a67890f13079c513f58451a40d5432e4",
    },
    12: {
        "preset": "ops-pulse",
        "draw_schedule": STYLE_12_DRAW_SCHEDULE,
        "schedule_keys": [("critical-request", 1, 0), ("critical-request", 2, 0), ("critical-request", 3, 0), ("telemetry-export", 4, 0)],
        "expected_stages": [1, 2, 3, 4],
        "stage_aware_schedule_key": True,
        "stream_primitive": "incident-pulse-rail-or-telemetry-export-rail",
        "signature_primitive": "ecg-head-or-telemetry-export-packet",
        "route_label_count": 0,
        "maximum_concurrent_draws": 1,
        "fixture_sha256": "2ea1d7a153ca8a39c37e9f5c5fd18a47c98a59f35694cd2bd68919d6b86b132c",
        "source_sha256": "f6170771acdd376fd78fa103214ff3125155754b6e19b87971d11c0afbab5a21",
    },
}

STYLE_SPECIALIZED_LIVE_SPECS: dict[int, dict[str, object]] = {
    5: {
        "body_primitive": "glass-handoff-rail", "signature_primitive": "glass-task-capsule",
        "dash_pattern": [13, 30], "dash_period": 43, "step": 6, "body_opacity": 0.88,
        "maximum_live_width": 2.2, "endpoint_clearance": 8,
        "phase_policy": "(motionStage * 7 + motionOrder * 3) mod 43",
        "expected_initial_phases": [7, 14, 17, 20, 21, 24, 28, 35, 38],
        "resolved_widths": [2.2] * 9,
        "geometry": {
            "shape": "rounded-translucent-plate", "width": 14, "height": 9, "rx": 3,
            "highlight_stroke_width": 1, "work_item_dot_radius": 2, "work_item_dot_count": 2,
            "tangent_aware_rotation": True,
        },
        "auxiliary": {"primitive": "coordinator-halo", "node_id": "coordinator", "period_frames": 16, "opacity_range": [0.12, 0.32], "movement": "opacity-only"},
        "direction_sentinels": [
            {"key": "ingress/0", "directions": ["right"]},
            {"key": "delegate/0", "directions": ["down", "left", "down"]},
            {"key": "delegate/2", "directions": ["down", "right", "down"]},
            {"key": "evidence/0", "directions": ["down"]},
            {"key": "context/0", "directions": ["right"]},
        ],
    },
    6: {
        "body_primitive": "governance-thread", "signature_primitive": "policy-seal",
        "dash_pattern": [11, 36], "dash_period": 47, "step": 6, "body_opacity": 0.82,
        "maximum_live_width": 2.8, "endpoint_clearance": 8,
        "phase_policy": "(motionStage * 7 + motionOrder * 3) mod 47",
        "expected_initial_phases": [7, 14, 21, 24, 27, 28, 31, 34, 35],
        "resolved_widths": [2.0] * 9,
        "geometry": {"shape": "warm-white-hexagonal-outline", "width": 12, "height": 12, "center_dot_diameter": 3, "approval_bar_width": 4, "shadow": False, "glow": False},
        "direction_sentinels": [
            {"key": "ingress/0", "directions": ["right"]}, {"key": "dispatch/0", "directions": ["down"]},
            {"key": "runtime-branch/0", "directions": ["left"]}, {"key": "runtime-branch/1", "directions": ["right"]},
            {"key": "foundation/0", "directions": ["down"]}, {"key": "promote/0", "directions": ["right"]},
        ],
    },
    7: {
        "body_primitive": "api-token-rail", "signature_primitive": "token-train",
        "dash_pattern": [10, 33], "dash_period": 43, "step": 6, "body_opacity": 0.86,
        "maximum_live_width": 2.5, "endpoint_clearance": 10,
        "phase_policy": "(motionStage * 7 + motionOrder * 3) mod 43",
        "expected_initial_phases": [7, 14, 21, 28, 31, 35, 38, 42],
        "resolved_widths": [2.0] * 8,
        "geometry": {"shape": "three-cell-token-train", "group_width": 18, "group_height": 8, "cell_width": 4, "cell_height": 4, "cell_gap": 2, "cell_opacities": [1.0, 0.72, 0.44], "tangent_aware_rotation": True},
        "direction_sentinels": [
            {"key": "connect/0", "directions": ["right"]}, {"key": "prepare/0", "directions": ["down", "left", "down"]},
            {"key": "tool-call/0", "directions": ["right"]}, {"key": "token-stream/1", "directions": ["down", "left", "down"]},
            {"key": "govern/0", "directions": ["down"]}, {"key": "promote/0", "directions": ["right"]},
        ],
    },
    8: {
        "body_primitive": "luxury-circuit-rail", "signature_primitive": "gem-tracer",
        "dash_pattern": [14, 33], "dash_period": 47, "step": 6, "body_opacity": 0.86,
        "maximum_live_width": 2.8, "endpoint_clearance": 8,
        "phase_policy": "(motionStage * 7 + motionOrder * 3) mod 47",
        "expected_initial_phases": [7, 14, 21, 24, 28, 35, 42],
        "resolved_widths": [2.0, 2.0, 1.7, 1.7, 1.7, 2.0, 1.7],
        "geometry": {"shape": "diamond-with-tapered-tail", "diamond_width": 7, "diamond_height": 7, "diamond_rotation": 45, "specular_diameter": 2, "tail_length": 12, "filtered_elements_per_tracer": 1},
        "direction_sentinels": [
            {"key": "primary/1/0", "directions": ["right"]}, {"key": "primary/2/0", "directions": ["right"]},
            {"key": "memory-read/3/0", "directions": ["down", "left", "down"]},
            {"key": "feedback/6/0", "directions": ["up", "left", "up"]},
        ],
    },
    9: {
        "body_primitive": "review-trace-rail", "signature_primitive": "review-cursor",
        "dash_pattern": [8, 33], "dash_period": 41, "step": 5, "body_opacity": 0.82,
        "maximum_live_width": 2.6, "endpoint_clearance": 9,
        "phase_policy": "(motionStage * 7 + motionOrder * 3) mod 41",
        "expected_initial_phases": [7, 14, 21, 28, 31],
        "resolved_widths": [2.0] * 5,
        "geometry": {"shape": "review-mark", "outline_circle_diameter": 11, "diagonal_handle_length": 5, "internal_check_extent": 3, "shadow": False, "glow": False},
        "direction_sentinels": [
            {"key": "review-entry/0", "directions": ["right"]}, {"key": "review-request/0", "directions": ["right"]},
            {"key": "review-async/0", "directions": ["down"]}, {"key": "review-state/0", "directions": ["left"]},
            {"key": "review-external/1", "directions": ["right"]},
        ],
    },
    10: {
        "body_primitive": "cloud-flow-rail", "signature_primitive": "region-chevron-pair-or-replication-capsule",
        "dash_pattern": [12, 31], "dash_period": 43, "step": 6, "body_opacity": 0.82,
        "maximum_live_width": 2.7, "endpoint_clearance": 8,
        "phase_policy": "motionStage * 7 mod 43; A/B orders are phase-locked",
        "expected_initial_phases": [7, 7, 14, 14, 21],
        "resolved_widths": [2.2] * 5,
        "geometry": {"routing_write": {"shape": "region-chevron-pair", "chevron_width": 6, "chevron_height": 5, "separation": 5}, "replication": {"shape": "replication-capsule", "width": 14, "height": 7, "data_cell_count": 2, "direction": "left-to-right"}},
        "auxiliary": {"primitive": "availability-pulse", "container_ids": ["region-a", "region-b"], "movement": "opacity-only", "phase_locked": True},
        "direction_sentinels": [
            {"key": "global-route/0", "directions": ["down"]}, {"key": "global-route/1", "directions": ["down"]},
            {"key": "regional-write/0", "directions": ["down"]}, {"key": "regional-write/1", "directions": ["down"]},
            {"key": "cross-region/0", "directions": ["right"]},
        ],
    },
    11: {
        "body_primitive": "event-transit-rail", "signature_primitive": "event-train-or-branch-car",
        "dash_pattern": [8, 33], "dash_period": 41, "step": 5, "body_opacity": 0.78,
        "maximum_live_width": 2.2, "endpoint_clearance": 7,
        "phase_policy": "(motionStage * 5 + motionOrder * 3) mod 41",
        "expected_initial_phases": [5, 10, 15, 20, 25, 28],
        "resolved_widths": [2.2] * 6,
        "geometry": {"main": {"shape": "three-car-event-train", "car_diameter": 5, "car_gap": 3, "car_count": 3}, "dead_letter": {"shape": "red-outlined-exception-car"}, "state_project": {"shape": "teal-two-cell-projection-car", "cell_count": 2}},
        "auxiliary": {"primitive": "station-dwell-ring", "period_frames": 10, "movement": "opacity-only", "geometry_expansion": 0, "count": 4},
        "direction_sentinels": [
            {"key": "topic-rail/1/0", "directions": ["right"]}, {"key": "topic-rail/2/0", "directions": ["right"]},
            {"key": "topic-rail/3/0", "directions": ["right"]}, {"key": "topic-rail/4/0", "directions": ["right"]},
            {"key": "dead-letter/5/0", "directions": ["down"]}, {"key": "state-project/5/1", "directions": ["down"]},
        ],
    },
    12: {
        "body_primitive": "incident-pulse-rail-or-telemetry-export-rail", "signature_primitive": "ecg-head-or-telemetry-export-packet",
        "dash_pattern": [12, 31], "dash_period": 43, "step": 5, "body_opacity": 0.84,
        "maximum_live_width": 2.2, "endpoint_clearance": 8,
        "phase_policy": "motionStage * 5 mod 43",
        "expected_initial_phases": [5, 10, 15, 20],
        "resolved_widths": [2.2] * 4,
        "geometry": {"critical": {"shape": "compact-ecg-head", "stroke_width": 1.6}, "telemetry": {"shape": "cyan-three-dot-export-packet", "dot_count": 3, "dot_diameter": 4}},
        "trace_reveal": {"primitive": "trace-span-reveal", "span_ids": ["span-root", "span-api", "span-checkout", "span-payment"], "rendered_frames": [[24, 27], [27, 30], [30, 33], [33, 36]], "source_geometry_mutated": False},
        "scanner": {"primitive": "waterfall-scanner", "width": 2, "tail_width": 12, "period_frames": 34, "movement": "horizontal-within-trace-plot"},
        "auxiliary": {"primitive": "checkout-degraded-halo", "node_id": "checkout-service", "period_frames": 18, "opacity_range": [0.10, 0.28], "movement": "opacity-only"},
        "direction_sentinels": [
            {"key": "critical-request/1/0", "directions": ["right"]}, {"key": "critical-request/2/0", "directions": ["right"]},
            {"key": "critical-request/3/0", "directions": ["right"]}, {"key": "telemetry-export/4/0", "directions": ["down"]},
        ],
    },
}


def _style_1_persistent_stream_contract(frame_count: int) -> dict[str, object]:
    reset_start = frame_count - len(RESET_OPACITY_SAMPLES)
    dash_period = 41
    dash_step = 6
    stream_interval_frame_count = frame_count - 36
    return {
        "primitive": "persistent-data-flow-stream",
        "packet_head_primitive": "persistent-data-flow-head",
        "stream_count": 8,
        "packet_head_count": 8,
        "roles": [entry["role"] for entry in DRAW_SCHEDULE],
        "rendered_frames": [36, frame_count - 1],
        "fade_in_frames": [36, 38],
        "fade_in_factors": [0.30, 0.65, 1.0],
        "full_opacity_frames": [38, reset_start - 1],
        "body": {
            "primitive": "persistent-data-flow-stream",
            "stroke_width": "min(4.0, max(3.0, source_stroke * 1.60))",
            "resolved_style_1_source_stroke_width": 2.4,
            "resolved_style_1_stroke_width": 3.84,
            "color": "#06b6d4",
            "opacity": 0.90,
            "dash_pattern": [16, 25],
            "linecap": "round",
            "linejoin": "round",
            "marker_free": True,
            "filter_free": True,
        },
        "packet_head": {
            "primitive": "persistent-data-flow-head",
            "stroke_width": 2.20,
            "color": "#e0f2fe",
            "opacity": 0.98,
            "dash_pattern": [6, 35],
            "dash_offset_from_body": -10,
            "linecap": "round",
            "linejoin": "round",
            "marker_free": True,
            "filter_free": True,
            "appended_immediately_after_body": True,
        },
        "dash_period": dash_period,
        "dash_offset_per_rendered_frame": -6.0,
        "travel_user_units_per_rendered_frame": dash_step,
        "travel_pixels_per_frame_at_960px": 6,
        "travel_pixels_per_second_at_960px_20fps": 120,
        "travel_pixels_per_frame_at_50_percent": 3,
        "travel_pixels_per_second_at_50_percent_20fps": 60,
        "phase_policy": "(motionStage * 7 + motionOrder * 3) mod 41",
        "expected_initial_phases": [7, 14, 21, 28, 31, 35, 1, 8],
        "period_step_coprime": math.gcd(dash_period, dash_step) == 1,
        "stream_interval_frame_count": stream_interval_frame_count,
        "phase_repeat_within_stream_interval": stream_interval_frame_count > dash_period,
        "direction": "source-to-target",
        "direction_sentinels": {
            "ingress": ["right"],
            "resolve": ["left"],
            "memory-write": ["down", "left", "down"],
        },
        "travel_easing": "linear",
        "minimum_review_scale": "50%",
        "reset_range": [reset_start, frame_count - 1],
        "reset_opacity_samples": RESET_OPACITY_SAMPLES,
        "reset_behavior": "all eight body/head pairs keep advancing while topology, labels, and both flow layers fade together",
    }


def _style_2_persistent_stream_contract(frame_count: int) -> dict[str, object]:
    reset_start = frame_count - len(RESET_OPACITY_SAMPLES)
    dash_period = 41
    dash_step = 6
    stream_interval_frame_count = frame_count - 36
    return {
        "primitive": "terminal-evidence-stream",
        "packet_head_primitive": "terminal-command-head",
        "stream_count": 8,
        "packet_head_count": 8,
        "route_keys": [
            {"role": role, "order": order}
            for role, order in STYLE_SCENE_CONTRACTS[2]["schedule_keys"]
        ],
        "roles": [entry["role"] for entry in STYLE_2_DRAW_SCHEDULE],
        "rendered_frames": [36, frame_count - 1],
        "fade_in_frames": [36, 38],
        "fade_in_factors": [0.30, 0.65, 1.0],
        "full_opacity_frames": [38, reset_start - 1],
        "body": {
            "primitive": "terminal-evidence-stream",
            "stroke_width": "min(3.8, max(3.0, source_stroke * 1.50))",
            "resolved_style_2_source_stroke_width": 2.3,
            "resolved_style_2_stroke_width": 3.45,
            "color": "inherit-source-stroke",
            "source_colors_in_schedule_order": [
                "#a855f7",
                "#a855f7",
                "#38bdf8",
                "#38bdf8",
                "#22c55e",
                "#fb7185",
                "#fb7185",
                "#f97316",
            ],
            "semantic_colors": {
                "control": "#a855f7",
                "tool_read": "#38bdf8",
                "index_write": "#22c55e",
                "grounding_data": "#fb7185",
                "answer": "#f97316",
            },
            "opacity": 0.94,
            "dash_pattern": [15, 26],
            "linecap": "round",
            "linejoin": "round",
            "marker_free": True,
            "filter_free": True,
        },
        "packet_head": {
            "primitive": "terminal-command-head",
            "stroke_width": 2.00,
            "color": "#f8fafc",
            "opacity": 1.00,
            "dash_pattern": [5, 36],
            "dash_offset_from_body": -10,
            "linecap": "round",
            "linejoin": "round",
            "marker_free": True,
            "filter_free": True,
            "appended_immediately_after_body": True,
        },
        "dash_period": dash_period,
        "dash_offset_per_rendered_frame": -6.0,
        "travel_user_units_per_rendered_frame": dash_step,
        "travel_pixels_per_frame_at_960px": 6,
        "travel_pixels_per_second_at_960px_20fps": 120,
        "travel_pixels_per_frame_at_50_percent": 3,
        "travel_pixels_per_second_at_50_percent_20fps": 60,
        "phase_policy": "(motionStage * 6 + motionOrder * 3) mod 41",
        "expected_initial_phases": [6, 12, 18, 24, 30, 36, 39, 1],
        "period_step_coprime": math.gcd(dash_period, dash_step) == 1,
        "stream_interval_frame_count": stream_interval_frame_count,
        "phase_repeat_within_stream_interval": stream_interval_frame_count > dash_period,
        "direction": "source-to-target",
        "direction_sentinels": {
            "ingress/0": ["right"],
            "delegate/0": ["down", "left", "down"],
            "tool-call/0": ["right"],
            "inspect/0": ["down"],
            "index/0": ["right", "up"],
            "grounding/0": ["up", "left", "up"],
            "grounding/1": ["right"],
            "answer/0": ["right"],
        },
        "travel_easing": "linear",
        "minimum_review_scale": "50%",
        "reset_range": [reset_start, frame_count - 1],
        "reset_opacity_samples": RESET_OPACITY_SAMPLES,
        "reset_behavior": "all eight body/head pairs keep advancing while topology, labels, cursor, and both flow layers fade together",
    }


def _style_3_persistent_stream_contract(frame_count: int) -> dict[str, object]:
    reset_start = frame_count - len(RESET_OPACITY_SAMPLES)
    dash_period = 43
    dash_step = 6
    stream_interval_frame_count = frame_count - 36
    return {
        "primitive": "blueprint-distribution-wave",
        "registration_bead_primitive": "blueprint-registration-bead",
        "stream_count": 10,
        "registration_bead_count": 10,
        "route_keys": [
            {"role": role, "order": order}
            for role, order in STYLE_SCENE_CONTRACTS[3]["schedule_keys"]
        ],
        "roles": [entry["role"] for entry in STYLE_3_DRAW_SCHEDULE],
        "rendered_frames": [36, frame_count - 1],
        "fade_in_frames": [36, 38],
        "fade_in_factors": [0.30, 0.65, 1.0],
        "full_opacity_frames": [38, reset_start - 1],
        "body": {
            "primitive": "blueprint-distribution-wave",
            "stroke_width": "min(3.4, max(2.8, source_stroke * 1.40))",
            "resolved_style_3_source_stroke_width": 2.1,
            "resolved_style_3_stroke_width": 2.94,
            "resolved_style_3_stroke_width_at_50_percent": 1.47,
            "color": "inherit-source-stroke",
            "source_colors_in_schedule_order": [
                "#38bdf8",
                "#67e8f9",
                "#38bdf8",
                "#38bdf8",
                "#38bdf8",
                "#fde047",
                "#fde047",
                "#fde047",
                "#fb7185",
                "#fb7185",
            ],
            "opacity": 0.92,
            "dash_pattern": [12, 31],
            "linecap": "round",
            "linejoin": "round",
            "marker_free": True,
            "filter_free": True,
        },
        "registration_bead": {
            "primitive": "blueprint-registration-bead",
            "shape": "circle",
            "radius": 3.0,
            "diameter_at_960px": 6,
            "diameter_at_50_percent": 3,
            "fill": "#e0f2fe",
            "stroke": "inherit-source-stroke",
            "stroke_width": 1.2,
            "opacity": 0.98,
            "initial_path_distance": "stage-locked-phase",
            "path_advance_per_rendered_frame": 6.0,
            "direction": "source-to-target",
            "wrap": "target-end-to-source-start",
            "animated_attributes": ["cx", "cy", "opacity"],
            "marker_free": True,
            "filter_free": True,
        },
        "dash_period": dash_period,
        "dash_offset_per_rendered_frame": -6.0,
        "bead_advance_per_rendered_frame": 6.0,
        "travel_user_units_per_rendered_frame": dash_step,
        "travel_pixels_per_frame_at_960px": 6,
        "travel_pixels_per_second_at_960px_20fps": 120,
        "travel_pixels_per_frame_at_50_percent": 3,
        "travel_pixels_per_second_at_50_percent_20fps": 60,
        "phase_policy": "(motionStage * 7 + motionOrder * 0) mod 43",
        "expected_initial_phases": [7, 14, 21, 21, 21, 28, 28, 28, 35, 42],
        "stage_locks": {
            "fanout": {"orders": [0, 1, 2], "phase": 21},
            "data-write": {"orders": [0, 1, 2], "phase": 28, "equal_length_paths": True},
        },
        "period_step_coprime": math.gcd(dash_period, dash_step) == 1,
        "stream_interval_frame_count": stream_interval_frame_count,
        "phase_repeat_within_stream_interval": stream_interval_frame_count > dash_period,
        "direction": "source-to-target",
        "direction_sentinels": {
            "ingress/0": ["right"],
            "policy/0": ["right"],
            "fanout/0": ["down", "left", "down"],
            "fanout/1": ["down"],
            "fanout/2": ["down", "right", "down"],
            "data-write/0": ["down"],
            "data-write/1": ["down"],
            "data-write/2": ["down"],
            "event/0": ["right"],
            "telemetry/0": ["down"],
        },
        "travel_easing": "linear",
        "minimum_review_scale": "50%",
        "reset_range": [reset_start, frame_count - 1],
        "reset_opacity_samples": RESET_OPACITY_SAMPLES,
        "reset_behavior": (
            "all ten bodies and registration beads keep advancing while topology, labels, "
            "and both Blueprint flow layers fade together"
        ),
    }


def _style_4_persistent_stream_contract(frame_count: int) -> dict[str, object]:
    reset_start = frame_count - len(RESET_OPACITY_SAMPLES)
    dash_period = 47
    dash_step = 6
    stream_interval_frame_count = frame_count - 36
    semantic_colors = [
        "#3b82f6",
        "#3b82f6",
        "#7c3aed",
        "#059669",
        "#ea580c",
        "#ea580c",
    ]
    progress_vector = [0.08, 0.22, 0.36, 0.50, 0.64, 0.78]
    return {
        "primitive": "notion-memory-rail",
        "memory_card_primitive": "notion-memory-card",
        "stream_count": 6,
        "memory_card_count": 6,
        "route_keys": [
            {"role": role, "order": order}
            for role, order in STYLE_SCENE_CONTRACTS[4]["schedule_keys"]
        ],
        "roles": [entry["role"] for entry in STYLE_4_DRAW_SCHEDULE],
        "rendered_frames": [36, frame_count - 1],
        "fade_in_frames": [36, 38],
        "fade_in_factors": [0.30, 0.65, 1.0],
        "full_opacity_frames": [38, reset_start - 1],
        "body": {
            "primitive": "notion-memory-rail",
            "stroke_width": "min(3.0, max(2.4, source_stroke * 1.50))",
            "resolved_style_4_source_stroke_width": 1.8,
            "resolved_style_4_stroke_width": 2.70,
            "resolved_style_4_stroke_width_at_50_percent": 1.35,
            "color": "semantic-memory-destination",
            "source_colors_in_schedule_order": ["#3b82f6"] * 6,
            "semantic_colors_in_schedule_order": semantic_colors,
            "semantic_color_meanings": {
                "active_context": "#3b82f6",
                "procedural_memory": "#7c3aed",
                "episodic_memory": "#059669",
                "semantic_memory": "#ea580c",
            },
            "opacity": 0.88,
            "dash_pattern": [12, 35],
            "linecap": "round",
            "linejoin": "round",
            "marker_free": True,
            "filter_free": True,
            "appended_below_labels_and_nodes": True,
        },
        "memory_card": {
            "primitive": "notion-memory-card",
            "shape": "group",
            "outer_rect": {
                "x": -7,
                "y": -5,
                "width": 14,
                "height": 10,
                "rx": 2,
                "fill": "#ffffff",
                "stroke": "semantic-memory-destination",
                "stroke_width": 1.4,
            },
            "ink_lines": [
                {"x1": -4.5, "y1": -2, "x2": 4, "y2": -2},
                {"x1": -4.5, "y1": 2, "x2": 0.5, "y2": 2},
            ],
            "ink_stroke": "semantic-memory-destination",
            "ink_stroke_width": 2.0,
            "ink_linecap": "butt",
            "ink_shape_rendering": "crispEdges",
            "opacity": 0.98,
            "semantic_colors_in_schedule_order": semantic_colors,
            "initial_normalized_progress_by_stage": progress_vector,
            "initial_path_distance": "8 + progress * (pathLength - 16)",
            "endpoint_clearance": 8,
            "path_advance_per_rendered_frame": 6.0,
            "direction": "source-to-target",
            "wrap": "target-clearance-to-source-clearance",
            "tangent_rotations_in_schedule_order": [0, 0, 0, 90, 0, -90],
            "animated_attributes": ["transform", "opacity"],
            "marker_free": True,
            "filter_free": True,
            "shadow_free": True,
            "appended_below_labels_and_nodes": True,
        },
        "dash_period": dash_period,
        "dash_offset_per_rendered_frame": -6.0,
        "card_advance_per_rendered_frame": 6.0,
        "travel_user_units_per_rendered_frame": dash_step,
        "travel_pixels_per_frame_at_960px": 6,
        "travel_pixels_per_second_at_960px_20fps": 120,
        "travel_pixels_per_frame_at_50_percent": 3,
        "travel_pixels_per_second_at_50_percent_20fps": 60,
        "phase_policy": "(motionStage * 7 + motionOrder * 0) mod 47",
        "expected_initial_phases": [7, 14, 21, 28, 35, 42],
        "initial_normalized_progress_by_stage": progress_vector,
        "period_step_coprime": math.gcd(dash_period, dash_step) == 1,
        "stream_interval_frame_count": stream_interval_frame_count,
        "phase_repeat_within_stream_interval": stream_interval_frame_count > dash_period,
        "direction": "source-to-target",
        "direction_sentinels": {
            "sample/0": {"directions": ["right"], "tangent_rotation": 0},
            "attend/0": {"directions": ["right"], "tangent_rotation": 0},
            "invoke/0": {"directions": ["right"], "tangent_rotation": 0},
            "remember/0": {"directions": ["down"], "tangent_rotation": 90},
            "consolidate/0": {"directions": ["right"], "tangent_rotation": 0},
            "recall/0": {"directions": ["up"], "tangent_rotation": -90},
        },
        "travel_easing": "linear",
        "minimum_review_scale": "50%",
        "reset_range": [reset_start, frame_count - 1],
        "reset_opacity_samples": RESET_OPACITY_SAMPLES,
        "reset_behavior": (
            "all six rails and six memory cards keep advancing while topology, labels, "
            "rails, and cards fade together"
        ),
    }


def _terminal_signature_contract(frame_count: int) -> dict[str, object]:
    reset_start = frame_count - len(RESET_OPACITY_SAMPLES)
    return {
        "primitive": "terminal-prompt-cursor",
        "count": 1,
        "node_id": "terminal",
        "source_text": "_",
        "source_text_hidden": False,
        "source_text_mutated": False,
        "geometry": "2.2px-high rectangle derived from underscore getBBox",
        "height": 2.2,
        "fill": "#a7f3d0",
        "marker_free": True,
        "filter_free": True,
        "movement": "opacity-only",
        "visible_after_route": {"role": "tool-call", "order": 0, "settled_frame": 16},
        "cadence_frames": [16, reset_start - 1],
        "period_frames": 16,
        "bright_frames_per_period": 8,
        "absent_frames_per_period": 8,
        "bright_opacity": 0.95,
        "reset_range": [reset_start, frame_count - 1],
        "reset_behavior": "bright opacity multiplied by shared reset opacity",
    }


def _specialized_persistent_stream_contract(frame_count: int, style_id: int) -> dict[str, object]:
    spec = STYLE_SPECIALIZED_LIVE_SPECS[style_id]
    scene = STYLE_SCENE_CONTRACTS[style_id]
    reset_start = frame_count - len(RESET_OPACITY_SAMPLES)
    step = int(spec["step"])
    stage_aware = bool(scene.get("stage_aware_schedule_key"))
    route_keys = []
    for key in scene["schedule_keys"]:
        if stage_aware:
            role, stage, order = key
            route_keys.append({"role": role, "stage": stage, "order": order})
        else:
            role, order = key
            route_keys.append({"role": role, "order": order})
    contract: dict[str, object] = {
        "primitive": spec["body_primitive"],
        "signature_primitive": spec["signature_primitive"],
        "stream_count": len(scene["schedule_keys"]),
        "signature_count": len(scene["schedule_keys"]),
        "route_keys": route_keys,
        "rendered_frames": [36, frame_count - 1],
        "fade_in_frames": [36, 38],
        "fade_in_factors": [0.30, 0.65, 1.0],
        "full_opacity_frames": [38, reset_start - 1],
        "body": {
            "primitive": spec["body_primitive"],
            "stroke_width": "min(maximum_live_width, source_stroke)",
            "maximum_live_width": spec["maximum_live_width"],
            "resolved_widths_in_schedule_order": spec["resolved_widths"],
            "color": "inherit-source-stroke",
            "opacity": spec["body_opacity"],
            "dash_pattern": spec["dash_pattern"],
            "linecap": "round",
            "linejoin": "round",
            "marker_free": True,
            "filter_free": True,
            "appended_below_labels_and_nodes": True,
        },
        "signature": {
            "primitive": spec["signature_primitive"],
            "geometry": spec["geometry"],
            "endpoint_clearance": spec["endpoint_clearance"],
            "path_advance_per_rendered_frame": step,
            "direction": "source-to-target",
            "wrap": "target-clearance-to-source-clearance",
            "tangent_aware_rotation": True,
            "animated_attributes": ["transform", "opacity"],
            "appended_below_labels_and_nodes": True,
        },
        "dash_period": spec["dash_period"],
        "dash_offset_per_rendered_frame": -step,
        "signature_advance_per_rendered_frame": step,
        "travel_user_units_per_rendered_frame": step,
        "travel_pixels_per_frame_at_100_percent": step,
        "travel_pixels_per_frame_at_50_percent": step / 2,
        "phase_policy": spec["phase_policy"],
        "expected_initial_phases": spec["expected_initial_phases"],
        "period_step_coprime": math.gcd(int(spec["dash_period"]), step) == 1,
        "direction": "source-to-target",
        "direction_sentinels": spec["direction_sentinels"],
        "travel_easing": "linear",
        "minimum_review_scale": "50%",
        "reset_range": [reset_start, frame_count - 1],
        "reset_opacity_samples": RESET_OPACITY_SAMPLES,
        "reset_behavior": "live rails, signatures, and scene auxiliaries keep advancing while the shared reset opacity fades to 0.03",
    }
    for optional in ("auxiliary", "trace_reveal", "scanner"):
        if optional in spec:
            contract[optional] = spec[optional]
    return contract


def _persistent_stream_contract(frame_count: int, style_id: int = 1) -> dict[str, object]:
    if style_id == 1:
        return _style_1_persistent_stream_contract(frame_count)
    if style_id == 2:
        return _style_2_persistent_stream_contract(frame_count)
    if style_id == 3:
        return _style_3_persistent_stream_contract(frame_count)
    if style_id == 4:
        return _style_4_persistent_stream_contract(frame_count)
    if style_id in STYLE_SPECIALIZED_LIVE_SPECS:
        return _specialized_persistent_stream_contract(frame_count, style_id)
    raise ValueError(f"MOTION_STYLE_REVIEW: Style {style_id} has no reviewed stream contract")


def _draw_on_contract(frame_count: int, style_id: int = 1) -> dict[str, object]:
    reset_start = frame_count - len(RESET_OPACITY_SAMPLES)
    contract: dict[str, object] = {
        "primitive": "connector-draw-on-with-persistent-data-flow",
        "empty_opening_frame": 0,
        "draw_schedule": STYLE_SCENE_CONTRACTS[style_id]["draw_schedule"],
        "reset_range": [reset_start, frame_count - 1],
        "reset_opacity_samples": RESET_OPACITY_SAMPLES,
        "connectors_visible_at_opening": False,
        "nodes_visible_every_frame": True,
        "topology_draw_on": True,
        "settled_topology_dynamic": True,
        "source_edges_hidden_by_transient_css": True,
        "draw_easing": "linear",
        "settled_markers": "original marker appears only after route arrival",
        "persistent_data_flow": _persistent_stream_contract(frame_count, style_id),
        "route_label_opacity_states": STYLE_SCENE_CONTRACTS[style_id]["route_label_count"],
        "text_geometry_motion": 0,
        "maximum_concurrent_draws": STYLE_SCENE_CONTRACTS[style_id].get("maximum_concurrent_draws", 2),
        "forbidden": ["node-motion", "text-motion", "halo", "ripple", "zoom", "breathing"],
    }
    if style_id == 2:
        contract["schedule_key"] = "(data-motion-role, data-motion-order)"
        contract["terminal_signature"] = _terminal_signature_contract(frame_count)
        contract["forbidden"] = [
            "node-motion",
            "text-motion",
            "terminal-text-typing",
            "scan-line",
            "halo",
            "ripple",
            "zoom",
            "camera-motion",
            "animated-background",
            "breathing",
        ]
    elif style_id == 3:
        contract["schedule_key"] = "(data-motion-role, data-motion-order)"
        contract["maximum_concurrent_draws"] = 2
        contract["forbidden"] = [
            "node-motion",
            "text-motion",
            "glow",
            "blur",
            "shadow",
            "scan-line",
            "halo",
            "ripple",
            "terminal-cursor",
            "camera-motion",
            "animated-background",
            "breathing",
        ]
    elif style_id == 4:
        contract["schedule_key"] = "(data-motion-role, data-motion-order)"
        contract["maximum_concurrent_draws"] = 1
        contract["forbidden"] = [
            "node-motion",
            "text-motion",
            "glow",
            "blur",
            "shadow",
            "scan-line",
            "halo",
            "ripple",
            "terminal-cursor",
            "circular-bead",
            "camera-motion",
            "animated-background",
            "breathing",
        ]
    elif style_id >= 5:
        contract["schedule_key"] = (
            "(data-motion-role, data-motion-stage, data-motion-order)"
            if STYLE_SCENE_CONTRACTS[style_id].get("stage_aware_schedule_key")
            else "(data-motion-role, data-motion-order)"
        )
        contract["forbidden"] = [
            "source-node-motion",
            "source-text-motion",
            "source-geometry-mutation",
            "source-marker-mutation",
            "camera-motion",
            "animated-background",
        ]
    return contract


PERSISTENT_STREAM_CONTRACT = _persistent_stream_contract(DEFAULT_MOTION_FRAME_COUNT)
DRAW_ON_CONTRACT = _draw_on_contract(DEFAULT_MOTION_FRAME_COUNT)
STYLE_2_PERSISTENT_STREAM_CONTRACT = _persistent_stream_contract(DEFAULT_MOTION_FRAME_COUNT, 2)
STYLE_2_DRAW_ON_CONTRACT = _draw_on_contract(DEFAULT_MOTION_FRAME_COUNT, 2)
STYLE_3_PERSISTENT_STREAM_CONTRACT = _persistent_stream_contract(DEFAULT_MOTION_FRAME_COUNT, 3)
STYLE_3_DRAW_ON_CONTRACT = _draw_on_contract(DEFAULT_MOTION_FRAME_COUNT, 3)
STYLE_4_PERSISTENT_STREAM_CONTRACT = _persistent_stream_contract(DEFAULT_MOTION_FRAME_COUNT, 4)
STYLE_4_DRAW_ON_CONTRACT = _draw_on_contract(DEFAULT_MOTION_FRAME_COUNT, 4)
STYLE_5_DRAW_ON_CONTRACT = _draw_on_contract(DEFAULT_MOTION_FRAME_COUNT, 5)
STYLE_6_DRAW_ON_CONTRACT = _draw_on_contract(DEFAULT_MOTION_FRAME_COUNT, 6)
STYLE_7_DRAW_ON_CONTRACT = _draw_on_contract(DEFAULT_MOTION_FRAME_COUNT, 7)
STYLE_8_DRAW_ON_CONTRACT = _draw_on_contract(DEFAULT_MOTION_FRAME_COUNT, 8)
STYLE_9_DRAW_ON_CONTRACT = _draw_on_contract(DEFAULT_MOTION_FRAME_COUNT, 9)
STYLE_10_DRAW_ON_CONTRACT = _draw_on_contract(DEFAULT_MOTION_FRAME_COUNT, 10)
STYLE_11_DRAW_ON_CONTRACT = _draw_on_contract(DEFAULT_MOTION_FRAME_COUNT, 11)
STYLE_12_DRAW_ON_CONTRACT = _draw_on_contract(DEFAULT_MOTION_FRAME_COUNT, 12)
TERMINAL_SIGNATURE_CONTRACT = _terminal_signature_contract(DEFAULT_MOTION_FRAME_COUNT)


def _motion_grammar(duration: float, fps: int, frame_count: int, style_id: int = 1) -> dict[str, object]:
    reset_start = frame_count - len(RESET_OPACITY_SAMPLES)
    phase_ranges = {
        "empty": (0, 0),
        "draw": (1, 36),
        "stream": (36, frame_count - 1),
        "full_opacity": (38, reset_start - 1),
        "reset": (reset_start, frame_count - 1),
    }
    phases: dict[str, object] = {}
    for name, (start_frame, end_frame) in phase_ranges.items():
        phases[name] = {
            "frames": [start_frame, end_frame],
            "seconds": [
                round((start_frame + 0.5) / fps, 6),
                round((end_frame + 0.5) / fps, 6),
            ],
        }
    return {
        "version": MOTION_GRAMMAR_VERSION,
        "phases": phases,
        "curves": MOTION_CURVES,
        "draw_on": _draw_on_contract(frame_count, style_id),
        "frame_duration_ms": round(1000 / fps, 6),
        "sampling": "uniform-frame-centers",
        "sample_index_expression": "time * fps - 0.5",
        "duration_seconds": duration,
    }


def _timing_revision(duration: float, fps: int, frame_count: int) -> dict[str, object]:
    if (
        fps == DEFAULT_MOTION_FPS
        and frame_count == DEFAULT_MOTION_FRAME_COUNT
        and math.isclose(duration, DEFAULT_MOTION_DURATION, rel_tol=0, abs_tol=1e-9)
    ):
        return {
            "id": TIMING_REVISION_ID,
            "status": "user-approved",
            "approved_at": TIMING_REVISION_APPROVED_AT,
            "only_pending_item": False,
            "baseline": {
                "duration_seconds": APPROVED_BASELINE_DURATION,
                "frame_count": APPROVED_BASELINE_FRAME_COUNT,
            },
            "candidate": {
                "duration_seconds": DEFAULT_MOTION_DURATION,
                "frame_count": DEFAULT_MOTION_FRAME_COUNT,
                "added_full_opacity_frames": [70, 109],
            },
        }
    if (
        fps == DEFAULT_MOTION_FPS
        and frame_count == APPROVED_BASELINE_FRAME_COUNT
        and math.isclose(duration, APPROVED_BASELINE_DURATION, rel_tol=0, abs_tol=1e-9)
    ):
        return {
            "id": "approved-3.75s-baseline",
            "status": "user-approved",
            "only_pending_item": False,
        }
    return {
        "id": "explicit-custom-timing",
        "status": "custom_timing",
        "only_pending_item": False,
    }


def _sha256(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def _sha256_bytes(value: bytes) -> str:
    return hashlib.sha256(value).hexdigest()


def _elements(root: ET.Element, role: str) -> list[ET.Element]:
    return [element for element in root.iter() if element.get("data-graph-role") == role]


def _parse_viewbox(root: ET.Element) -> tuple[float, float, float, float]:
    raw = root.get("viewBox") or root.get("viewbox")
    if not raw:
        raise ValueError("MOTION_VIEWBOX: generated SVG must define a viewBox")
    try:
        values = tuple(float(value) for value in raw.replace(",", " ").split())
    except ValueError as error:
        raise ValueError("MOTION_VIEWBOX: viewBox values must be numeric") from error
    if len(values) != 4 or values[2] <= 0 or values[3] <= 0:
        raise ValueError("MOTION_VIEWBOX: viewBox must contain four positive dimensions")
    return values  # type: ignore[return-value]


def _validate_settings(duration: float, fps: int, width: int) -> int:
    if isinstance(duration, bool) or not isinstance(duration, (int, float)) or not math.isfinite(duration):
        raise ValueError("MOTION_DURATION: duration must be a finite number")
    if duration < 0.5 or duration > 20:
        raise ValueError("MOTION_DURATION: duration must be between 0.5 and 20 seconds")
    if isinstance(fps, bool) or not isinstance(fps, int):
        raise ValueError("MOTION_FPS: fps must be a whole number")
    if fps < 1 or fps > 25:
        raise ValueError("MOTION_FPS: fps must be between 1 and 25")
    if isinstance(width, bool) or not isinstance(width, int):
        raise ValueError("MOTION_WIDTH: width must be a whole number")
    if width < 320 or width > 4096:
        raise ValueError("MOTION_WIDTH: width must be between 320 and 4096 pixels")
    requested_frames = duration * fps
    frame_count = round(requested_frames)
    if abs(requested_frames - frame_count) > 1e-9:
        raise ValueError("MOTION_TIMELINE: duration multiplied by fps must be a whole number of frames")
    if frame_count < MINIMUM_MOTION_FRAME_COUNT:
        raise ValueError(
            f"MOTION_TIMELINE: semantic GIF motion requires at least {MINIMUM_MOTION_FRAME_COUNT} rendered frames"
        )
    if frame_count > 500:
        raise ValueError("MOTION_FRAME_BUDGET: animation may not exceed 500 frames")
    return frame_count


def _validate_render_budget(width: int, height: int, frame_count: int) -> int:
    rendered_pixels = width * height * frame_count
    if rendered_pixels > MAX_RENDERED_PIXELS:
        raise ValueError(
            "MOTION_TOTAL_PIXEL_BUDGET: output dimensions multiplied by frame count may not exceed 600 million pixels"
        )
    return rendered_pixels


def _read_input(path: Path) -> bytes:
    if not path.is_file():
        raise ValueError(f"MOTION_INPUT: input does not exist: {path}")
    size = path.stat().st_size
    if size <= 0:
        raise ValueError("MOTION_INPUT: input is empty")
    if size > MAX_INPUT_BYTES:
        raise ValueError("MOTION_INPUT_SIZE: input may not exceed 20 MiB")
    return path.read_bytes()


def _input_kind(path: Path, source: bytes) -> str:
    if path.suffix.lower() == ".svg" and b"<svg" in source[:4096].lower():
        return "svg"
    raise ValueError("MOTION_INPUT_TYPE: input must be a generated .svg file")


def _validate_semantics(style_id: int, edges: list[ET.Element], nodes: list[ET.Element]) -> dict[str, object]:
    if style_id not in REVIEWED_STYLE_IDS:
        raise ValueError(f"MOTION_STYLE: Style {style_id} does not have a motion preset yet")
    if not edges or not nodes:
        raise ValueError(f"MOTION_METADATA: Style {style_id} requires semantic nodes and edges")
    incomplete = [
        edge.get("data-edge-id", "unknown")
        for edge in edges
        if not edge.get("data-source") or not edge.get("data-target")
    ]
    if incomplete:
        raise ValueError(
            "MOTION_METADATA: semantic edges require source and target metadata: "
            + ", ".join(incomplete)
        )
    explicit: list[ET.Element] = []
    partial: list[str] = []
    unknown_roles: set[str] = set()
    supported_roles = STYLE_MOTION_ROLES[style_id]
    for edge in edges:
        values = (
            edge.get("data-motion-role", ""),
            edge.get("data-motion-stage", ""),
            edge.get("data-motion-order", ""),
        )
        populated = sum(bool(value) for value in values)
        if 0 < populated < 3:
            partial.append(edge.get("data-edge-id", "unknown"))
        elif populated == 3:
            explicit.append(edge)
            if values[0] not in supported_roles:
                unknown_roles.add(values[0])
    if partial:
        raise ValueError(
            "MOTION_METADATA: motion role, stage, and order must be provided together: " + ", ".join(partial)
        )
    if unknown_roles:
        raise ValueError(
            f"MOTION_METADATA: Style {style_id} has unsupported motion roles: "
            + ", ".join(sorted(unknown_roles))
        )
    if not explicit:
        metadata_mode = "topology-fallback"
    elif len(explicit) == len(edges):
        metadata_mode = "explicit"
    else:
        metadata_mode = "hybrid"
    result: dict[str, object] = {
        "edges": len(edges),
        "nodes": len(nodes),
        "motion_metadata": metadata_mode,
        "staged_edges": len(explicit),
        "edges_without_flow": sum(not edge.get("data-flow") for edge in edges),
    }
    if style_id == 10:
        flows = [edge.get("data-flow", "") for edge in edges]
        missing = sorted({"read", "write", "async"} - set(flows))
        if missing:
            raise ValueError(f'MOTION_METADATA: Style 10 is missing flow metadata: {", ".join(missing)}')
        result["flows"] = {flow: flows.count(flow) for flow in sorted(set(flows))}
    elif style_id == 11:
        stations = [node for node in nodes if node.get("data-station-order", "")]
        rails = [edge for edge in edges if edge.get("data-edge-kind") == "rail"]
        branch_kinds = {edge.get("data-edge-kind") for edge in edges}
        if len(stations) < 2 or not rails:
            raise ValueError("MOTION_METADATA: Style 11 requires ordered stations and rail edges")
        if not {"dead_letter", "state"}.issubset(branch_kinds):
            raise ValueError("MOTION_METADATA: Style 11 requires dead-letter and state branches")
        orders = sorted(int(node.get("data-station-order", "-1")) for node in stations)
        if orders != list(range(len(orders))):
            raise ValueError("MOTION_METADATA: Style 11 station order must be contiguous from zero")
        result.update({"stations": len(stations), "rails": len(rails), "branches": 2})
    elif style_id == 12:
        critical = [edge for edge in edges if edge.get("data-critical") == "true"]
        spans = [node for node in nodes if node.get("data-span-id")]
        if not critical or any(not edge.get("data-critical-hop") for edge in critical):
            raise ValueError("MOTION_METADATA: Style 12 critical edges require hop metadata")
        if not spans or any(not node.get("data-duration-ms") for node in spans):
            raise ValueError("MOTION_METADATA: Style 12 trace spans require timing metadata")
        result.update({"critical_hops": len(critical), "trace_spans": len(spans)})
    return result


def _validate_scene_schedule(style_id: int, edges: list[ET.Element]) -> list[dict[str, object]]:
    scene_contract = STYLE_SCENE_CONTRACTS[style_id]
    expected_keys = list(scene_contract["schedule_keys"])
    expected_stages = list(scene_contract["expected_stages"])
    stage_aware = bool(scene_contract.get("stage_aware_schedule_key"))
    keyed_edges: dict[tuple[object, ...], list[ET.Element]] = {}
    for edge in edges:
        role = edge.get("data-motion-role", "")
        try:
            stage = int(edge.get("data-motion-stage", ""))
            order = int(edge.get("data-motion-order", ""))
        except ValueError as error:
            raise ValueError(
                f'MOTION_METADATA: edge {edge.get("data-edge-id", "unknown")} has a non-integer motion stage or order'
            ) from error
        key: tuple[object, ...] = (role, stage, order) if stage_aware else (role, order)
        keyed_edges.setdefault(key, []).append(edge)

    actual_keys = set(keyed_edges)
    expected_key_set = set(expected_keys)
    duplicate_keys = [key for key, values in keyed_edges.items() if len(values) != 1]
    if len(edges) != len(expected_keys) or actual_keys != expected_key_set or duplicate_keys:
        missing = sorted(expected_key_set - actual_keys)
        unexpected = sorted(actual_keys - expected_key_set)
        raise ValueError(
            f"MOTION_METADATA: Style {style_id} schedule must resolve exactly one edge per "
            f"{'(role, stage, order)' if stage_aware else '(role, order)'}; "
            f"missing={missing}, unexpected={unexpected}, duplicate={sorted(duplicate_keys)}"
        )

    resolved: list[dict[str, object]] = []
    for expected_key, expected_stage in zip(expected_keys, expected_stages):
        if stage_aware:
            role, key_stage, order = expected_key
            if key_stage != expected_stage:
                raise ValueError(f"MOTION_CONTRACT: Style {style_id} stage-aware schedule is internally inconsistent")
        else:
            role, order = expected_key
        edge = keyed_edges[expected_key][0]
        actual_stage = int(edge.get("data-motion-stage", ""))
        if actual_stage != expected_stage:
            raise ValueError(
                f"MOTION_METADATA: Style {style_id} route {role}/{order} must use motion stage {expected_stage}"
            )
        resolved.append(
            {
                "edge_id": edge.get("data-edge-id", ""),
                "role": role,
                "stage": actual_stage,
                "order": order,
                "source_stroke": edge.get("stroke", ""),
            }
        )

    if style_id in {2, 3, 4}:
        expected_colors = _persistent_stream_contract(DEFAULT_MOTION_FRAME_COUNT, style_id)["body"][
            "source_colors_in_schedule_order"
        ]
        actual_colors = [entry["source_stroke"] for entry in resolved]
        if actual_colors != expected_colors:
            raise ValueError(
                f"MOTION_METADATA: Style {style_id} semantic route colors changed: "
                f"expected {expected_colors}, got {actual_colors}"
            )
    if style_id == 4:
        semantic_colors = _persistent_stream_contract(DEFAULT_MOTION_FRAME_COUNT, 4)["body"][
            "semantic_colors_in_schedule_order"
        ]
        for entry, semantic_color in zip(resolved, semantic_colors):
            entry["semantic_color"] = semantic_color
    return resolved


def build_motion_plan(
    svg_path: Path,
    preset: str = "auto",
    duration: float = DEFAULT_MOTION_DURATION,
    fps: int = DEFAULT_MOTION_FPS,
    width: Optional[int] = None,
) -> tuple[dict[str, object], object]:
    """Validate a semantic SVG and return its focused GIF motion plan."""

    source_bytes = _read_input(svg_path)
    _input_kind(svg_path, source_bytes)
    resolved_width = 960 if width is None else width
    frame_count = _validate_settings(duration, fps, resolved_width)
    try:
        source_text = source_bytes.decode("utf-8")
    except UnicodeDecodeError as error:
        raise ValueError("MOTION_INPUT: SVG must be UTF-8 encoded") from error
    safe_svg = sanitize_svg(source_text)
    root = ET.fromstring(safe_svg)
    if root.get("data-generator") != "fireworks-tech-graph":
        raise ValueError("MOTION_INPUT: only fireworks-tech-graph generated SVGs are supported")
    if root.get("data-semantic-valid") != "true":
        raise ValueError("MOTION_INPUT: SVG must carry a valid semantic contract")
    try:
        style_id = int(root.get("data-style-id", "0"))
    except ValueError as error:
        raise ValueError("MOTION_STYLE: SVG has an invalid style id") from error
    if style_id not in REVIEWED_STYLE_IDS:
        raise ValueError(
            f"MOTION_STYLE_REVIEW: Style {style_id} is unsupported; enabled styles are 1-12"
        )
    scene_contract = STYLE_SCENE_CONTRACTS[style_id]
    resolved_preset = STYLE_PRESETS.get(style_id) if preset == "auto" else preset
    if not resolved_preset or resolved_preset not in PRESET_STYLES:
        raise ValueError(f"MOTION_PRESET: unsupported preset: {preset}")
    expected_style = PRESET_STYLES[resolved_preset]
    if style_id != expected_style:
        raise ValueError(
            f"MOTION_PRESET: {resolved_preset} belongs to Style {expected_style}, input is Style {style_id}"
        )

    edges = _elements(root, "edge")
    nodes = _elements(root, "node")
    semantics = _validate_semantics(style_id, edges, nodes)
    if semantics.get("motion_metadata") != "explicit":
        raise ValueError("MOTION_METADATA: reviewed styles require explicit motion metadata on every edge")
    resolved_schedule = _validate_scene_schedule(style_id, edges)
    if style_id in set(range(2, 13)):
        semantics["schedule_key"] = "(data-motion-role, data-motion-order)"
        if scene_contract.get("stage_aware_schedule_key"):
            semantics["schedule_key"] = "(data-motion-role, data-motion-stage, data-motion-order)"
        semantics["resolved_schedule"] = resolved_schedule
    check_results: dict[str, object] = {}
    for check in CHECKS:
        passed, details = run_check(svg_path, check)
        check_results[check] = {"ok": passed, "details": details}
        if not passed:
            raise ValueError(f"MOTION_SOURCE_CHECK: {check} failed: {details}")

    viewbox = _parse_viewbox(root)
    height = max(1, round(resolved_width * viewbox[3] / viewbox[2]))
    if height > 4096 or resolved_width * height > 16_777_216:
        raise ValueError("MOTION_DIMENSIONS: output must stay within 4096px per side and 16 megapixels")
    rendered_pixels = _validate_render_budget(resolved_width, height, frame_count)
    timing_revision = _timing_revision(duration, fps, frame_count)
    style_contract: dict[str, object] = {
        "status": "user-approved",
        "scope": "signature-speed-path-geometry-and-construction-schedule",
        "source_policy": "semantic-schedule-and-geometry-not-exact-byte-hash",
    }
    if style_id >= 5:
        style_contract["approval_recorded_on"] = "2026-07-16"
    plan: dict[str, object] = {
        "ok": True,
        "dry_run": True,
        "source": str(svg_path),
        "source_sha256": _sha256_bytes(source_bytes),
        "review_reference_source_sha256": scene_contract.get("source_sha256"),
        "fixture_sha256": scene_contract.get("fixture_sha256"),
        "input_kind": "svg",
        "style_id": style_id,
        "visual_theme": root.get("data-visual-theme"),
        "semantic_profile": root.get("data-semantic-profile"),
        "motion_scene": root.get("data-motion-scene") or resolved_preset,
        "motion_class": "semantic",
        "preset": resolved_preset,
        "motion_grammar_version": MOTION_GRAMMAR_VERSION,
        "motion_grammar": _motion_grammar(duration, fps, frame_count, style_id),
        "scene_signature": SCENE_SIGNATURES[style_id],
        "review_status": timing_revision["status"],
        "style_contract_status": "user-approved",
        "style_contract": style_contract,
        "timing_revision": timing_revision,
        "animation_contract": _draw_on_contract(frame_count, style_id),
        "duration_seconds": duration,
        "fps": fps,
        "frame_count": frame_count,
        "width": resolved_width,
        "height": height,
        "rendered_pixels": rendered_pixels,
        "seamless_loop_contract": (
            "75 frames or fewer: every raster is unique; more than 75 frames: non-adjacent "
            "repeats are allowed within the full-opacity interval, plus the sole intentional "
            "reset-boundary exception at reset_start with opacity 1.00, with at least 75 unique "
            "rasters; seam MAD must not exceed p95 adjacent-frame MAD"
        ),
        "frame_uniqueness_contract": {
            "strict_all_unique_through_frame_count": APPROVED_BASELINE_FRAME_COUNT,
            "minimum_unique_rasters_for_long_timeline": APPROVED_BASELINE_FRAME_COUNT,
            "long_timeline_repeat_scope": [38, frame_count - len(RESET_OPACITY_SAMPLES) - 1],
            "intentional_reset_boundary_repeat_frame": (
                frame_count - len(RESET_OPACITY_SAMPLES)
            ),
            "intentional_reset_boundary_opacity": RESET_OPACITY_SAMPLES[0],
            "adjacent_duplicates_allowed": False,
            "opening_construction_and_reset_tail_globally_distinct": True,
        },
        "semantics": semantics,
        "source_checks": check_results,
    }
    return plan, safe_svg


def probe_motion_runtime() -> dict[str, object]:
    node = shutil.which("node")
    ffmpeg = shutil.which("ffmpeg")
    ffprobe = shutil.which("ffprobe")
    result: dict[str, object] = {
        "ok": False,
        "node": node,
        "ffmpeg": ffmpeg,
        "ffprobe": ffprobe,
        "worker": str(MOTION_WORKER),
    }
    if not node or not MOTION_WORKER.is_file():
        result["error"] = "Node.js or scripts/svg2gif.js is unavailable"
        return result
    try:
        process = subprocess.run(
            [node, str(MOTION_WORKER), "--probe"],
            text=True,
            capture_output=True,
            check=False,
            timeout=MOTION_TIMEOUTS["runtime_probe"],
        )
    except subprocess.TimeoutExpired:
        result["error"] = "MOTION_RUNTIME_TIMEOUT: renderer probe exceeded 15 seconds"
        return result
    try:
        worker = json.loads(process.stdout.strip().splitlines()[-1]) if process.stdout.strip() else {}
    except (IndexError, json.JSONDecodeError):
        worker = {"ok": False, "error": process.stderr.strip() or "motion runtime probe returned invalid JSON"}
    encoders: set[str] = set()
    encoder_error: Optional[str] = None
    if ffmpeg:
        try:
            encoder_process = subprocess.run(
                [ffmpeg, "-nostdin", "-hide_banner", "-encoders"],
                text=True,
                capture_output=True,
                check=False,
                timeout=MOTION_TIMEOUTS["runtime_probe"],
            )
            if encoder_process.returncode == 0:
                for line in encoder_process.stdout.splitlines():
                    match = re.match(r"^\s*[VASFXBD\.]{6}\s+([A-Za-z0-9_][^\s]*)", line)
                    if match:
                        encoders.add(match.group(1))
            else:
                encoder_error = encoder_process.stderr.strip() or "FFmpeg encoder discovery failed"
        except subprocess.TimeoutExpired:
            encoder_error = "MOTION_RUNTIME_TIMEOUT: FFmpeg encoder probe exceeded 15 seconds"
    result["renderer"] = worker
    result["encoders"] = sorted(encoders.intersection({"gif"}))
    result["format"] = {"gif": bool(ffmpeg and "gif" in encoders)}
    if encoder_error:
        result["encoder_probe_error"] = encoder_error
    result["ok"] = bool(
        ffmpeg
        and ffprobe
        and "gif" in encoders
        and process.returncode == 0
        and worker.get("ok")
    )
    if not result["ok"] and "error" not in result:
        result["error"] = (
            worker.get("error")
            or process.stderr.strip()
            or ("FFprobe is unavailable" if not ffprobe else "motion dependencies are incomplete")
        )
    return result


def _last_json_line(output: str, label: str) -> dict[str, object]:
    for line in reversed(output.splitlines()):
        try:
            value = json.loads(line)
        except json.JSONDecodeError:
            continue
        if isinstance(value, dict):
            return value
    raise RuntimeError(f"{label} did not return a JSON result")


def _stage_json(path: Path, value: dict[str, object]) -> Path:
    path.parent.mkdir(parents=True, exist_ok=True)
    if os.path.lexists(path) and path.is_dir():
        raise ValueError("MOTION_REPORT: report target must be a file path")
    temporary = path.with_name(f".{path.name}.{uuid.uuid4().hex}.tmp")
    try:
        temporary.write_text(json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
        return temporary
    except Exception:
        temporary.unlink(missing_ok=True)
        raise


def _commit_artifacts(staged_targets: list[tuple[Path, Path]]) -> None:
    """Install related artifacts together and restore every previous target on failure."""

    for staged, target in staged_targets:
        if not staged.is_file():
            raise RuntimeError(f"MOTION_COMMIT: staged artifact is unavailable: {staged}")
        target.parent.mkdir(parents=True, exist_ok=True)
        if os.path.lexists(target) and target.is_dir():
            raise ValueError(f"MOTION_COMMIT: artifact target must be a file path: {target}")

    backups: dict[Path, Optional[Path]] = {}
    installed: set[Path] = set()
    try:
        for staged, target in staged_targets:
            backup: Optional[Path] = None
            if os.path.lexists(target):
                backup = target.with_name(f".{target.name}.{uuid.uuid4().hex}.rollback")
                backups[target] = backup
                os.replace(target, backup)
            else:
                backups[target] = None
            os.replace(staged, target)
            installed.add(target)
    except Exception as error:
        rollback_errors: list[str] = []
        for _staged, target in reversed(staged_targets):
            backup = backups.get(target)
            try:
                if target in installed and os.path.lexists(target):
                    target.unlink()
                if backup is not None and os.path.lexists(backup):
                    os.replace(backup, target)
            except OSError as rollback_error:
                rollback_errors.append(f"{target}: {rollback_error}")
        if rollback_errors:
            raise RuntimeError(
                "MOTION_COMMIT_ROLLBACK: artifact commit failed and rollback was incomplete: "
                + "; ".join(rollback_errors)
            ) from error
        raise
    else:
        for backup in backups.values():
            if backup is not None:
                backup.unlink(missing_ok=True)
    finally:
        for staged, _target in staged_targets:
            staged.unlink(missing_ok=True)


def _probe_media(path: Path) -> dict[str, object]:
    ffprobe = shutil.which("ffprobe")
    if not ffprobe:
        raise RuntimeError("MOTION_MEDIA_PROBE: FFprobe is required to validate encoded motion")
    try:
        process = subprocess.run(
            [
                ffprobe,
                "-v",
                "error",
                "-count_frames",
                "-select_streams",
                "v:0",
                "-show_entries",
                "stream=codec_name,width,height,duration,nb_frames,nb_read_frames,r_frame_rate,pix_fmt:format=format_name,duration",
                "-of",
                "json",
                "-protocol_whitelist",
                "file,pipe",
                str(path.resolve()),
            ],
            text=True,
            capture_output=True,
            check=False,
            timeout=MOTION_TIMEOUTS["media_probe"],
        )
    except subprocess.TimeoutExpired as error:
        raise RuntimeError("MOTION_MEDIA_PROBE_TIMEOUT: FFprobe exceeded 30 seconds") from error
    if process.returncode != 0:
        return {"error": process.stderr.strip()}
    try:
        payload = json.loads(process.stdout)
        streams = payload.get("streams", [])
    except (AttributeError, json.JSONDecodeError):
        return {"error": "ffprobe returned invalid JSON"}
    if not streams:
        return {"error": "ffprobe found no video stream"}
    result = dict(streams[0])
    if isinstance(payload.get("format"), dict):
        result["container"] = payload["format"]
        if not result.get("duration") and payload["format"].get("duration"):
            result["duration"] = payload["format"]["duration"]
    return result


def _validate_encoded_gif(
    media_probe: dict[str, object],
    expected_width: int,
    expected_height: int,
    expected_frames: int,
    expected_duration: float,
) -> None:
    if media_probe.get("error"):
        raise RuntimeError(f'MOTION_MEDIA_PROBE: {media_probe["error"]}')
    if media_probe.get("codec_name") != "gif":
        raise RuntimeError(
            f'MOTION_CODEC: expected gif, encoded {media_probe.get("codec_name", "unknown")}'
        )
    if int(str(media_probe.get("width", -1))) != expected_width:
        raise RuntimeError("MOTION_DIMENSIONS: encoded media width differs from the motion plan")
    if int(str(media_probe.get("height", -1))) != expected_height:
        raise RuntimeError("MOTION_DIMENSIONS: encoded media height differs from the motion plan")
    encoded_frames = media_probe.get("nb_read_frames") or media_probe.get("nb_frames")
    if encoded_frames is None:
        raise RuntimeError("MOTION_FRAMES: media probe did not report an encoded frame count")
    encoded_frame_count = int(str(encoded_frames))
    if encoded_frame_count != expected_frames:
        raise RuntimeError("MOTION_FRAMES: encoded media frame count differs from the motion plan")
    if media_probe.get("duration") is None:
        raise RuntimeError("MOTION_DURATION: media probe did not report encoded duration")
    encoded_duration = float(str(media_probe["duration"]))
    if abs(encoded_duration - expected_duration) > 0.03:
        raise RuntimeError(
            f"MOTION_DURATION: requested {expected_duration}s, encoder produced {encoded_duration}s"
        )


def _read_gif_loop_count(path: Path) -> Optional[int]:
    """Return the Netscape/ANIMEXTS repeat count embedded in a GIF."""

    data = path.read_bytes()
    if data[:6] not in (b"GIF87a", b"GIF89a"):
        return None
    for application in (b"NETSCAPE2.0", b"ANIMEXTS1.0"):
        marker = b"\x21\xff\x0b" + application + b"\x03\x01"
        offset = data.find(marker)
        if offset < 0:
            continue
        value_offset = offset + len(marker)
        if value_offset + 3 > len(data) or data[value_offset + 2] != 0:
            return None
        return int.from_bytes(data[value_offset : value_offset + 2], "little")
    return None


def _validate_gif_loop(path: Path) -> int:
    loop_count = _read_gif_loop_count(path)
    if loop_count is None:
        raise RuntimeError("MOTION_LOOP: encoded GIF has no readable loop extension")
    if loop_count != 0:
        raise RuntimeError(
            f"MOTION_LOOP: encoded GIF repeats {loop_count} time(s), expected infinite looping"
        )
    return loop_count


def _run_encoder(command: list[str], label: str) -> subprocess.CompletedProcess[str]:
    try:
        process = subprocess.run(
            command,
            text=True,
            capture_output=True,
            check=False,
            timeout=MOTION_TIMEOUTS["encode"],
        )
    except subprocess.TimeoutExpired as error:
        raise RuntimeError(f"MOTION_ENCODE_TIMEOUT: {label} exceeded 120 seconds") from error
    if process.returncode != 0:
        raise RuntimeError(f"MOTION_ENCODE: {process.stderr.strip() or process.stdout.strip()}")
    return process


def _signalstats_values(output: str) -> list[float]:
    return [float(value) for value in re.findall(r"lavfi\.signalstats\.YAVG=([0-9.eE+-]+)", output)]


def _summarize_delta_quality(
    adjacent_yavg: list[float],
    seam_yavg: float,
    changed_area_yavg: float,
) -> dict[str, object]:
    values = sorted(adjacent_yavg)
    if not values or any(not math.isfinite(value) or value < 0 for value in (*values, seam_yavg, changed_area_yavg)):
        raise RuntimeError("MOTION_DELTA: frame-delta metrics are missing or invalid")
    p95 = values[max(0, math.ceil(len(values) * 0.95) - 1)]
    median = values[len(values) // 2]
    seam_within_p95 = seam_yavg <= p95
    result: dict[str, object] = {
        "metric": "normalized-luma-mean-absolute-difference",
        "adjacent_min": values[0] / 255,
        "adjacent_median": median / 255,
        "adjacent_p95": p95 / 255,
        "adjacent_max": values[-1] / 255,
        "seam": seam_yavg / 255,
        "seam_within_adjacent_p95": seam_within_p95,
        "changed_area_ratio": min(1.0, changed_area_yavg / 255),
        "changed_area_method": "accumulated adjacent luma delta greater than 2 percent",
    }
    if not seam_within_p95:
        raise RuntimeError("MOTION_LOOP: loop seam exceeds the p95 adjacent-frame delta")
    return result


def _probe_frame_deltas(ffmpeg: str, frames: Path, fps: int, frame_count: int) -> dict[str, object]:
    pattern = str(frames / "frame-%06d.png")
    adjacent = _run_encoder(
        [
            ffmpeg,
            "-nostdin",
            "-hide_banner",
            "-loglevel",
            "error",
            "-framerate",
            str(fps),
            "-start_number",
            "0",
            "-i",
            pattern,
            "-vf",
            "tblend=all_mode=difference,signalstats,metadata=print:file=-",
            "-f",
            "null",
            "-",
        ],
        "adjacent frame-delta probe",
    )
    adjacent_values = _signalstats_values(adjacent.stdout)
    if len(adjacent_values) != frame_count - 1:
        raise RuntimeError("MOTION_DELTA: adjacent frame-delta probe returned an unexpected sample count")

    seam = _run_encoder(
        [
            ffmpeg,
            "-nostdin",
            "-hide_banner",
            "-loglevel",
            "error",
            "-i",
            str(frames / f"frame-{frame_count - 1:06d}.png"),
            "-i",
            str(frames / "frame-000000.png"),
            "-filter_complex",
            "[0:v][1:v]blend=all_mode=difference,signalstats,metadata=print:file=-",
            "-frames:v",
            "1",
            "-f",
            "null",
            "-",
        ],
        "loop-seam frame-delta probe",
    )
    seam_values = _signalstats_values(seam.stdout)
    if len(seam_values) != 1:
        raise RuntimeError("MOTION_DELTA: loop-seam probe returned an unexpected sample count")

    delta_frames = frame_count - 1
    weights = " ".join("1" for _ in range(delta_frames))
    coverage_filter = (
        "tblend=all_mode=difference,format=gray,"
        f"tmix=frames={delta_frames}:weights='{weights}':scale=1,"
        "lut=y='if(gt(val,5.1),255,0)',signalstats,metadata=print:file=-"
    )
    coverage = _run_encoder(
        [
            ffmpeg,
            "-nostdin",
            "-hide_banner",
            "-loglevel",
            "error",
            "-framerate",
            str(fps),
            "-start_number",
            "0",
            "-i",
            pattern,
            "-vf",
            coverage_filter,
            "-f",
            "null",
            "-",
        ],
        "changed-area probe",
    )
    coverage_values = _signalstats_values(coverage.stdout)
    if len(coverage_values) != delta_frames:
        raise RuntimeError("MOTION_DELTA: changed-area probe returned an unexpected sample count")
    return _summarize_delta_quality(adjacent_values, seam_values[0], coverage_values[-1])


def _summarize_frame_hashes(
    hashes: list[str],
    full_opacity_frames: tuple[int, int],
    *,
    algorithm: str,
    raster_kind: str,
) -> dict[str, object]:
    groups: dict[str, list[int]] = {}
    for index, frame_hash in enumerate(hashes):
        groups.setdefault(frame_hash, []).append(index)
    repeated = [
        {"hash": frame_hash, "frame_indices": indices}
        for frame_hash, indices in groups.items()
        if len(indices) > 1
    ]
    adjacent_duplicates = [
        [index, index + 1]
        for index in range(len(hashes) - 1)
        if hashes[index] == hashes[index + 1]
    ]
    if adjacent_duplicates:
        raise RuntimeError(
            f"MOTION_LOOP: {raster_kind} contains adjacent duplicate frames: {adjacent_duplicates}"
        )

    unique_count = len(groups)
    strict_all_unique = len(hashes) <= APPROVED_BASELINE_FRAME_COUNT
    minimum_unique = len(hashes) if strict_all_unique else APPROVED_BASELINE_FRAME_COUNT
    if strict_all_unique and repeated:
        raise RuntimeError(
            f"MOTION_LOOP: every {raster_kind} frame must be unique for timelines of "
            f"{APPROVED_BASELINE_FRAME_COUNT} frames or fewer"
        )
    if unique_count < minimum_unique:
        raise RuntimeError(
            f"MOTION_LOOP: {raster_kind} requires at least {minimum_unique} unique frames; "
            f"found {unique_count}"
        )

    repeat_scope_start, repeat_scope_end = full_opacity_frames
    reset_boundary_frame = repeat_scope_end + 1
    classified_repeats = []
    for evidence in repeated:
        indices = evidence["frame_indices"]
        if all(repeat_scope_start <= index <= repeat_scope_end for index in indices):
            classification = "steady_state_periodic_repeat"
        elif (
            reset_boundary_frame in indices
            and all(repeat_scope_start <= index <= reset_boundary_frame for index in indices)
        ):
            classification = "intentional_reset_boundary_repeat"
        else:
            classification = "outside_permitted_repeat_scope"
        classified_repeats.append({**evidence, "classification": classification})
    outside_scope = [
        evidence
        for evidence in classified_repeats
        if evidence["classification"] == "outside_permitted_repeat_scope"
    ]
    if outside_scope:
        raise RuntimeError(
            f"MOTION_LOOP: repeated {raster_kind} frames must stay inside full-opacity frames "
            f"{repeat_scope_start}-{repeat_scope_end}, except reset boundary frame "
            f"{reset_boundary_frame} at opacity 1.00: {outside_scope}"
        )

    intentional_reset_boundary_repeats = [
        evidence
        for evidence in classified_repeats
        if evidence["classification"] == "intentional_reset_boundary_repeat"
    ]
    repeats_confined_to_full_opacity = not intentional_reset_boundary_repeats

    return {
        "algorithm": algorithm,
        "frame_count": len(hashes),
        "unique_frame_count": unique_count,
        "minimum_unique_frame_count": minimum_unique,
        "all_frames_unique": not repeated,
        "adjacent_duplicate_count": 0,
        "adjacent_duplicate_pairs": [],
        "repeat_scope": [repeat_scope_start, repeat_scope_end],
        "intentional_reset_boundary_repeat_frame": reset_boundary_frame,
        "intentional_reset_boundary_opacity": RESET_OPACITY_SAMPLES[0],
        "repeat_scope_policy": (
            "all-frames-unique"
            if strict_all_unique
            else "non-adjacent-repeats-inside-full-opacity-plus-reset-start-boundary"
        ),
        "repeated_frame_group_count": len(classified_repeats),
        "repeated_frame_index_evidence": classified_repeats,
        "intentional_reset_boundary_repeat_count": len(intentional_reset_boundary_repeats),
        "intentional_reset_boundary_repeat_evidence": intentional_reset_boundary_repeats,
        "repeats_confined_to_full_opacity": repeats_confined_to_full_opacity,
        "repeats_confined_to_permitted_scope": True,
        "opening_construction_and_reset_tail_globally_distinct": True,
        "first_frame_hash": hashes[0],
        "last_frame_hash": hashes[-1],
    }


def _probe_encoded_frame_hashes(
    ffmpeg: str,
    gif: Path,
    expected_frames: int,
    full_opacity_frames: tuple[int, int],
) -> dict[str, object]:
    process = _run_encoder(
        [
            ffmpeg,
            "-nostdin",
            "-hide_banner",
            "-loglevel",
            "error",
            "-i",
            str(gif),
            "-map",
            "0:v:0",
            "-f",
            "framemd5",
            "-",
        ],
        "encoded GIF frame-hash probe",
    )
    hashes = re.findall(r",\s*([0-9a-f]{32})\s*$", process.stdout, flags=re.MULTILINE)
    if len(hashes) != expected_frames:
        raise RuntimeError("MOTION_FRAMES: encoded frame-hash probe returned an unexpected frame count")
    return _summarize_frame_hashes(
        hashes,
        full_opacity_frames,
        algorithm="framemd5",
        raster_kind="encoded GIF",
    )


def _gif_command(ffmpeg: str, frames: Path, fps: int, output: Path) -> list[str]:
    return [
        ffmpeg,
        "-nostdin",
        "-hide_banner",
        "-loglevel",
        "error",
        "-y",
        "-framerate",
        str(fps),
        "-i",
        str(frames / "frame-%06d.png"),
        "-filter_complex",
        "[0:v]split[a][b];[a]palettegen=stats_mode=full:max_colors=256[p];"
        "[b][p]paletteuse=dither=bayer:bayer_scale=3:diff_mode=rectangle",
        "-loop",
        "0",
        "-map_metadata",
        "-1",
        str(output.resolve()),
    ]


def _encode_gif(
    frames: Path,
    fps: int,
    output: Path,
    runtime: dict[str, object],
) -> str:
    ffmpeg = str(runtime["ffmpeg"])
    _run_encoder(_gif_command(ffmpeg, frames, fps, output), "GIF encoder")
    return "ffmpeg/gif-palette"


def _size_guidance(
    artifact_bytes: int,
) -> dict[str, object]:
    warnings: list[str] = []
    if artifact_bytes > MOTION_SIZE_TARGET_BYTES:
        warnings.append("GIF exceeds 500 KB; reduce width, duration, or frame rate before distribution.")
    return {
        "artifact_target_bytes": MOTION_SIZE_TARGET_BYTES,
        "artifact_within_target": artifact_bytes <= MOTION_SIZE_TARGET_BYTES,
        "warnings": warnings,
    }


def render_motion_gif(
    svg_path: Path,
    output_path: Path,
    report_path: Optional[Path] = None,
    preset: str = "auto",
    duration: float = DEFAULT_MOTION_DURATION,
    fps: int = DEFAULT_MOTION_FPS,
    width: Optional[int] = None,
    dry_run: bool = False,
) -> dict[str, object]:
    """Render one generated semantic SVG to one validated, atomic GIF artifact."""

    source_target = svg_path.resolve()
    gif_target = output_path.resolve()
    if source_target == gif_target:
        raise ValueError("MOTION_OUTPUT: output path must differ from the input path")
    if output_path.suffix.lower() != MOTION_FORMAT["suffix"]:
        raise ValueError("MOTION_OUTPUT: output path must end in .gif")
    if report_path and report_path.resolve() in {source_target, gif_target}:
        raise ValueError("MOTION_REPORT: report path must differ from the input and GIF paths")

    plan, prepared_source = build_motion_plan(
        svg_path,
        preset=preset,
        duration=duration,
        fps=fps,
        width=width,
    )
    encoded_width = int(plan["width"])
    encoded_height = int(plan["height"])
    plan.update(
        {
            "output": str(output_path),
            "report": str(report_path) if report_path else None,
            "output_format": MOTION_FORMAT["name"],
            "mime_type": MOTION_FORMAT["mime"],
            "loop_playback": MOTION_FORMAT["loop_playback"],
            "encoded_dimensions": {"width": encoded_width, "height": encoded_height},
            "required_encoder": "ffmpeg/gif",
        }
    )
    if dry_run:
        return plan

    runtime = probe_motion_runtime()
    if not runtime.get("ok"):
        raise RuntimeError(f'MOTION_RUNTIME: {runtime.get("error", "motion dependencies are unavailable")}')
    available = runtime.get("format")
    if not isinstance(available, dict) or not available.get("gif"):
        raise RuntimeError("MOTION_FORMAT_UNAVAILABLE: GIF encoder is unavailable")

    node = str(runtime["node"])
    output_path.parent.mkdir(parents=True, exist_ok=True)
    temporary_gif = output_path.with_name(f".{output_path.stem}.{uuid.uuid4().hex}.tmp.gif")
    temporary_report: Optional[Path] = None
    source_hash = str(plan["source_sha256"])

    try:
        with tempfile.TemporaryDirectory(prefix="fireworks-motion-") as directory:
            work = Path(directory)
            source = work / "source.svg"
            frames = work / "frames"
            source.write_text(str(prepared_source), encoding="utf-8")
            frames.mkdir()
            try:
                render_process = subprocess.run(
                    [
                        node,
                        str(MOTION_WORKER),
                        "--input",
                        str(source),
                        "--frames-dir",
                        str(frames),
                        "--preset",
                        str(plan["preset"]),
                        "--duration",
                        str(duration),
                        "--fps",
                        str(fps),
                        "--width",
                        str(plan["width"]),
                        "--height",
                        str(plan["height"]),
                    ],
                    text=True,
                    capture_output=True,
                    check=False,
                    timeout=MOTION_TIMEOUTS["render"],
                )
            except subprocess.TimeoutExpired as error:
                raise RuntimeError("MOTION_RENDER_TIMEOUT: frame rendering exceeded 120 seconds") from error
            if render_process.returncode != 0:
                raise RuntimeError(f"MOTION_RENDER: {render_process.stderr.strip() or render_process.stdout.strip()}")
            worker = _last_json_line(render_process.stdout, "motion renderer")
            frame_files = sorted(frames.glob("frame-*.png"))
            if len(frame_files) != plan["frame_count"]:
                raise RuntimeError(f'MOTION_FRAMES: expected {plan["frame_count"]} frames, rendered {len(frame_files)}')
            frame_hashes = [_sha256(frame) for frame in frame_files]
            first_hash = frame_hashes[0]
            last_hash = frame_hashes[-1]
            stream_contract = plan["animation_contract"]["persistent_data_flow"]
            full_opacity_values = stream_contract["full_opacity_frames"]
            full_opacity_frames = (int(full_opacity_values[0]), int(full_opacity_values[1]))
            raw_frame_hashes = _summarize_frame_hashes(
                frame_hashes,
                full_opacity_frames,
                algorithm="sha256",
                raster_kind="raw Chromium raster",
            )
            delta_quality = _probe_frame_deltas(
                str(runtime["ffmpeg"]),
                frames,
                fps,
                int(plan["frame_count"]),
            )

            encoder = _encode_gif(frames, fps, temporary_gif, runtime)
            if not temporary_gif.is_file() or temporary_gif.stat().st_size == 0:
                raise RuntimeError("MOTION_ENCODE: encoder produced an empty GIF artifact")
            media_probe = _probe_media(temporary_gif)
            _validate_encoded_gif(
                media_probe,
                encoded_width,
                encoded_height,
                int(plan["frame_count"]),
                duration,
            )
            loop_count = _validate_gif_loop(temporary_gif)
            encoded_frame_hashes = _probe_encoded_frame_hashes(
                str(runtime["ffmpeg"]),
                temporary_gif,
                int(plan["frame_count"]),
                full_opacity_frames,
            )

        source_hash_after = _sha256(svg_path)
        if source_hash_after != source_hash:
            raise RuntimeError("MOTION_SOURCE_MUTATED: source SVG changed during GIF rendering")
        artifact_bytes = temporary_gif.stat().st_size
        result = dict(plan)
        result.update(
            {
                "dry_run": False,
                "runtime": runtime,
                "renderer": worker,
                "source_integrity": {
                    "sha256": source_hash,
                    "sha256_before": source_hash,
                    "sha256_after": source_hash_after,
                    "unchanged": True,
                },
                "loop": {
                    "first_frame_sha256": first_hash,
                    "last_frame_sha256": last_hash,
                    "unique_frame_count": raw_frame_hashes["unique_frame_count"],
                    "minimum_unique_frame_count": raw_frame_hashes["minimum_unique_frame_count"],
                    "all_frames_unique": raw_frame_hashes["all_frames_unique"],
                    "adjacent_duplicate_count": raw_frame_hashes["adjacent_duplicate_count"],
                    "repeat_scope": raw_frame_hashes["repeat_scope"],
                    "repeat_scope_policy": raw_frame_hashes["repeat_scope_policy"],
                    "repeated_frame_index_evidence": raw_frame_hashes[
                        "repeated_frame_index_evidence"
                    ],
                    "intentional_reset_boundary_repeat_count": raw_frame_hashes[
                        "intentional_reset_boundary_repeat_count"
                    ],
                    "intentional_reset_boundary_repeat_evidence": raw_frame_hashes[
                        "intentional_reset_boundary_repeat_evidence"
                    ],
                    "repeats_confined_to_full_opacity": raw_frame_hashes[
                        "repeats_confined_to_full_opacity"
                    ],
                    "repeats_confined_to_permitted_scope": raw_frame_hashes[
                        "repeats_confined_to_permitted_scope"
                    ],
                    "opening_construction_and_reset_tail_globally_distinct": raw_frame_hashes[
                        "opening_construction_and_reset_tail_globally_distinct"
                    ],
                    "sampling": "uniform-frame-centers",
                    "delta_quality": delta_quality,
                    "raw_frames": raw_frame_hashes,
                    "encoded_frames": encoded_frame_hashes,
                    "playback": MOTION_FORMAT["loop_playback"],
                    "embedded_loop_count": loop_count,
                },
                "artifact": {
                    "path": str(output_path),
                    "format": MOTION_FORMAT["name"],
                    "mime_type": MOTION_FORMAT["mime"],
                    "encoder": encoder,
                    "bytes": artifact_bytes,
                    "sha256": _sha256(temporary_gif),
                    "probe": media_probe,
                    "source_frame_count": plan["frame_count"],
                },
                "size_guidance": _size_guidance(artifact_bytes),
            }
        )
        if report_path:
            temporary_report = _stage_json(report_path, result)
        staged_targets = [(temporary_gif, output_path)]
        if report_path and temporary_report:
            staged_targets.append((temporary_report, report_path))
        _commit_artifacts(staged_targets)
        return result
    finally:
        temporary_gif.unlink(missing_ok=True)
        if temporary_report:
            temporary_report.unlink(missing_ok=True)
skills/fireworks-tech-graph/scripts/generate-from-template.py
#!/usr/bin/env python3
"""
Style-driven SVG diagram generator.

Usage:
  python3 generate-from-template.py <template-type> <output-path> [data-json]

This generator intentionally does more than "fill a template".
It encodes the visual language from the documented style guides so the output
tracks the showcase quality more closely than the previous generic renderer.
"""

from __future__ import annotations

import argparse
import copy
import hashlib
import heapq
import json
import math
import os
import re
import sys
from dataclasses import dataclass
from typing import Dict, List, Mapping, Optional, Sequence, Tuple
from xml.sax.saxutils import escape

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
if SCRIPT_DIR not in sys.path:
    sys.path.insert(0, SCRIPT_DIR)

import fireworks_geometry as geometry  # noqa: E402
import composition_quality as quality  # noqa: E402
from diagram_ir import normalize_diagram  # noqa: E402
from semantic_contracts import STYLE_NAMES, resolve_style_index  # noqa: E402

Point = Tuple[float, float]
Bounds = Tuple[float, float, float, float]

TEMPLATE_DIR = os.path.join(SCRIPT_DIR, "..", "templates")
DEFAULT_VIEWBOX = {
    "architecture": (960, 600),
    "data-flow": (960, 600),
    "flowchart": (960, 640),
    "sequence": (960, 700),
    "comparison": (960, 620),
    "timeline": (960, 520),
    "mind-map": (960, 620),
    "agent": (960, 700),
    "memory": (960, 720),
    "use-case": (960, 600),
    "class": (960, 700),
    "state-machine": (960, 620),
    "er-diagram": (960, 680),
    "network-topology": (960, 620),
}

FLOW_ALIASES = {
    "main": "control",
    "api": "control",
    "control": "control",
    "write": "write",
    "read": "read",
    "data": "data",
    "async": "async",
    "feedback": "feedback",
    "neutral": "neutral",
}

MARKER_IDS = {
    "control": "arrowA",
    "write": "arrowB",
    "read": "arrowC",
    "data": "arrowE",
    "async": "arrowF",
    "feedback": "arrowG",
    "neutral": "arrowH",
}

STYLE_PROFILES: Dict[int, Dict[str, object]] = {
    1: {
        "name": "Flat Icon",
        "font_family": "'Helvetica Neue', Helvetica, Arial, 'PingFang SC', 'Microsoft YaHei', 'Microsoft JhengHei', 'SimHei', sans-serif",
        "background": "#ffffff",
        "shadow": True,
        "title_align": "center",
        "title_fill": "#111827",
        "title_size": 30,
        "subtitle_fill": "#6b7280",
        "subtitle_size": 14,
        "node_fill": "#ffffff",
        "node_stroke": "#d1d5db",
        "node_radius": 10,
        "node_shadow": "url(#shadowSoft)",
        "section_fill": "none",
        "section_stroke": "#dbe5f1",
        "section_dash": "6 5",
        "section_label_fill": "#2563eb",
        "section_sub_fill": "#94a3b8",
        "title_divider": False,
        "section_upper": True,
        "arrow_width": 2.4,
        "arrow_colors": {
            "control": "#7c3aed",
            "write": "#10b981",
            "read": "#2563eb",
            "data": "#f97316",
            "async": "#7c3aed",
            "feedback": "#ef4444",
            "neutral": "#6b7280",
        },
        "arrow_label_bg": "#ffffff",
        "arrow_label_opacity": 0.94,
        "arrow_label_fill": "#6b7280",
        "type_label_fill": "#9ca3af",
        "type_label_size": 12,
        "text_primary": "#111827",
        "text_secondary": "#6b7280",
        "text_muted": "#94a3b8",
        "legend_fill": "#6b7280",
    },
    2: {
        "name": "Dark Terminal",
        "font_family": "'SF Mono', 'Fira Code', Menlo, 'Microsoft YaHei', 'SimHei', monospace",
        "background": "#0f172a",
        "shadow": False,
        "title_align": "center",
        "title_fill": "#e2e8f0",
        "title_size": 30,
        "subtitle_fill": "#94a3b8",
        "subtitle_size": 14,
        "node_fill": "#111827",
        "node_stroke": "#334155",
        "node_radius": 10,
        "node_shadow": "",
        "section_fill": "rgba(15,23,42,0.28)",
        "section_stroke": "#334155",
        "section_dash": "7 6",
        "section_label_fill": "#38bdf8",
        "section_sub_fill": "#64748b",
        "title_divider": False,
        "section_upper": True,
        "arrow_width": 2.3,
        "arrow_colors": {
            "control": "#a855f7",
            "write": "#22c55e",
            "read": "#38bdf8",
            "data": "#fb7185",
            "async": "#f59e0b",
            "feedback": "#f97316",
            "neutral": "#94a3b8",
        },
        "arrow_label_bg": "#0f172a",
        "arrow_label_opacity": 0.92,
        "arrow_label_fill": "#cbd5e1",
        "type_label_fill": "#64748b",
        "type_label_size": 12,
        "text_primary": "#e2e8f0",
        "text_secondary": "#94a3b8",
        "text_muted": "#64748b",
        "legend_fill": "#94a3b8",
    },
    3: {
        "name": "Blueprint",
        "font_family": "'SF Mono', 'Fira Code', Menlo, 'Microsoft YaHei', 'SimHei', monospace",
        "background": "#082f49",
        "shadow": False,
        "title_align": "center",
        "title_fill": "#e0f2fe",
        "title_size": 30,
        "subtitle_fill": "#7dd3fc",
        "subtitle_size": 14,
        "node_fill": "#0b3b5e",
        "node_stroke": "#67e8f9",
        "node_radius": 8,
        "node_shadow": "",
        "section_fill": "none",
        "section_stroke": "#0ea5e9",
        "section_dash": "6 4",
        "section_label_fill": "#67e8f9",
        "section_sub_fill": "#7dd3fc",
        "title_divider": False,
        "section_upper": True,
        "arrow_width": 2.1,
        "arrow_colors": {
            "control": "#67e8f9",
            "write": "#22d3ee",
            "read": "#38bdf8",
            "data": "#fde047",
            "async": "#c084fc",
            "feedback": "#fb7185",
            "neutral": "#bae6fd",
        },
        "arrow_label_bg": "#082f49",
        "arrow_label_opacity": 0.9,
        "arrow_label_fill": "#e0f2fe",
        "type_label_fill": "#7dd3fc",
        "type_label_size": 11,
        "text_primary": "#e0f2fe",
        "text_secondary": "#bae6fd",
        "text_muted": "#7dd3fc",
        "legend_fill": "#bae6fd",
    },
    4: {
        "name": "Notion Clean",
        "font_family": "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Arial, 'PingFang SC', 'Microsoft YaHei', 'Microsoft JhengHei', 'SimHei', sans-serif",
        "background": "#ffffff",
        "shadow": False,
        "title_align": "left",
        "title_fill": "#111827",
        "title_size": 18,
        "subtitle_fill": "#9ca3af",
        "subtitle_size": 13,
        "node_fill": "#f9fafb",
        "node_stroke": "#e5e7eb",
        "node_radius": 4,
        "node_shadow": "",
        "section_fill": "none",
        "section_stroke": "#e5e7eb",
        "section_dash": "",
        "section_label_fill": "#9ca3af",
        "section_sub_fill": "#d1d5db",
        "title_divider": True,
        "section_upper": True,
        "arrow_width": 1.8,
        "arrow_colors": {
            "control": "#3b82f6",
            "write": "#3b82f6",
            "read": "#3b82f6",
            "data": "#3b82f6",
            "async": "#9ca3af",
            "feedback": "#9ca3af",
            "neutral": "#d1d5db",
        },
        "arrow_label_bg": "#ffffff",
        "arrow_label_opacity": 0.96,
        "arrow_label_fill": "#6b7280",
        "type_label_fill": "#9ca3af",
        "type_label_size": 11,
        "text_primary": "#111827",
        "text_secondary": "#374151",
        "text_muted": "#9ca3af",
        "legend_fill": "#6b7280",
    },
    5: {
        "name": "Glassmorphism",
        "font_family": "'Helvetica Neue', Helvetica, Arial, 'PingFang SC', 'Microsoft YaHei', 'Microsoft JhengHei', 'SimHei', sans-serif",
        "background": "#0f172a",
        "shadow": True,
        "title_align": "center",
        "title_fill": "#f8fafc",
        "title_size": 30,
        "subtitle_fill": "#cbd5e1",
        "subtitle_size": 14,
        "node_fill": "rgba(255,255,255,0.12)",
        "node_stroke": "rgba(255,255,255,0.28)",
        "node_radius": 18,
        "node_shadow": "url(#shadowGlass)",
        "section_fill": "rgba(255,255,255,0.05)",
        "section_stroke": "rgba(255,255,255,0.18)",
        "section_dash": "7 6",
        "section_label_fill": "#e2e8f0",
        "section_sub_fill": "#94a3b8",
        "title_divider": False,
        "section_upper": True,
        "arrow_width": 2.2,
        "arrow_colors": {
            "control": "#c084fc",
            "write": "#34d399",
            "read": "#60a5fa",
            "data": "#fb923c",
            "async": "#f472b6",
            "feedback": "#f59e0b",
            "neutral": "#cbd5e1",
        },
        "arrow_label_bg": "rgba(15,23,42,0.7)",
        "arrow_label_opacity": 1,
        "arrow_label_fill": "#e2e8f0",
        "type_label_fill": "#cbd5e1",
        "type_label_size": 12,
        "text_primary": "#f8fafc",
        "text_secondary": "#cbd5e1",
        "text_muted": "#94a3b8",
        "legend_fill": "#cbd5e1",
    },
    6: {
        "name": "Claude Official",
        "font_family": "'Helvetica Neue', Helvetica, Arial, 'PingFang SC', 'Microsoft YaHei', 'Microsoft JhengHei', 'SimHei', sans-serif",
        "background": "#f8f6f3",
        "shadow": False,
        "title_align": "left",
        "title_fill": "#141413",
        "title_size": 24,
        "subtitle_fill": "#8f8a80",
        "subtitle_size": 13,
        "node_fill": "#fffcf7",
        "node_stroke": "#d9d0c3",
        "node_radius": 10,
        "node_shadow": "",
        "section_fill": "none",
        "section_stroke": "#ded8cf",
        "section_dash": "5 4",
        "section_label_fill": "#8b7355",
        "section_sub_fill": "#b4aba0",
        "title_divider": True,
        "section_upper": True,
        "arrow_width": 2.0,
        "arrow_colors": {
            "control": "#d97757",
            "write": "#7b8b5c",
            "read": "#8c6f5a",
            "data": "#b45309",
            "async": "#9a6fb0",
            "feedback": "#7c5c96",
            "neutral": "#8f8a80",
        },
        "arrow_label_bg": "#f8f6f3",
        "arrow_label_opacity": 0.96,
        "arrow_label_fill": "#6b6257",
        "type_label_fill": "#a29a8f",
        "type_label_size": 11,
        "text_primary": "#141413",
        "text_secondary": "#6b6257",
        "text_muted": "#a29a8f",
        "legend_fill": "#6b6257",
    },
    7: {
        "name": "OpenAI",
        "font_family": "'Helvetica Neue', Helvetica, Arial, 'PingFang SC', 'Microsoft YaHei', 'Microsoft JhengHei', 'SimHei', sans-serif",
        "background": "#ffffff",
        "shadow": False,
        "title_align": "left",
        "title_fill": "#0f172a",
        "title_size": 24,
        "subtitle_fill": "#64748b",
        "subtitle_size": 13,
        "node_fill": "#ffffff",
        "node_stroke": "#dce5e3",
        "node_radius": 14,
        "node_shadow": "",
        "section_fill": "none",
        "section_stroke": "#e2e8f0",
        "section_dash": "5 4",
        "section_label_fill": "#10a37f",
        "section_sub_fill": "#94a3b8",
        "title_divider": True,
        "section_upper": True,
        "arrow_width": 2.0,
        "arrow_colors": {
            "control": "#10a37f",
            "write": "#0f766e",
            "read": "#0891b2",
            "data": "#f59e0b",
            "async": "#64748b",
            "feedback": "#475569",
            "neutral": "#94a3b8",
        },
        "arrow_label_bg": "#ffffff",
        "arrow_label_opacity": 0.96,
        "arrow_label_fill": "#475569",
        "type_label_fill": "#94a3b8",
        "type_label_size": 11,
        "text_primary": "#0f172a",
        "text_secondary": "#475569",
        "text_muted": "#94a3b8",
        "legend_fill": "#475569",
    },
    9: {
        "name": "C4 Review Canvas",
        "font_family": "'Avenir Next', Avenir, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif",
        "background": "#f7f2e8",
        "shadow": False,
        "title_align": "left",
        "title_fill": "#24312f",
        "title_size": 27,
        "subtitle_fill": "#6f756f",
        "subtitle_size": 13,
        "node_fill": "#fffdf7",
        "node_stroke": "#365f56",
        "node_radius": 7,
        "node_shadow": "",
        "section_fill": "rgba(255,253,247,0.48)",
        "section_stroke": "#8c7d68",
        "section_dash": "9 6",
        "section_label_fill": "#5b5144",
        "section_sub_fill": "#8c7d68",
        "title_divider": False,
        "section_upper": False,
        "arrow_width": 2.0,
        "arrow_colors": {
            "control": "#365f56",
            "write": "#a44a3f",
            "read": "#356a8a",
            "data": "#c06b35",
            "async": "#7a5c99",
            "feedback": "#b13e53",
            "neutral": "#746b60",
        },
        "arrow_label_bg": "#f7f2e8",
        "arrow_label_opacity": 0.94,
        "arrow_label_fill": "#4b5563",
        "type_label_fill": "#8a6f43",
        "type_label_size": 10,
        "text_primary": "#24312f",
        "text_secondary": "#5f665f",
        "text_muted": "#8a8d86",
        "legend_fill": "#5f665f",
        "canvas_treatment": "review",
    },
    10: {
        "name": "Cloud Fabric",
        "font_family": "Inter, 'Helvetica Neue', 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif",
        "background": "#edf5fb",
        "shadow": True,
        "title_align": "left",
        "title_fill": "#102a43",
        "title_size": 27,
        "subtitle_fill": "#52718d",
        "subtitle_size": 13,
        "node_fill": "#ffffff",
        "node_stroke": "#9bb7cf",
        "node_radius": 12,
        "node_shadow": "url(#shadowSoft)",
        "section_fill": "rgba(255,255,255,0.54)",
        "section_stroke": "#7fa3c2",
        "section_dash": "7 5",
        "section_label_fill": "#315d7e",
        "section_sub_fill": "#7892a8",
        "title_divider": False,
        "section_upper": True,
        "arrow_width": 2.2,
        "arrow_colors": {
            "control": "#2563eb",
            "write": "#ea580c",
            "read": "#0891b2",
            "data": "#059669",
            "async": "#7c3aed",
            "feedback": "#db2777",
            "neutral": "#64748b",
        },
        "arrow_label_bg": "#f7fbfe",
        "arrow_label_opacity": 0.96,
        "arrow_label_fill": "#334e68",
        "type_label_fill": "#6b879d",
        "type_label_size": 10,
        "text_primary": "#102a43",
        "text_secondary": "#486581",
        "text_muted": "#829ab1",
        "legend_fill": "#486581",
        "canvas_treatment": "cloud",
    },
    11: {
        "name": "Event Transit",
        "font_family": "'Avenir Next', Avenir, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif",
        "background": "#fbf7ee",
        "shadow": False,
        "title_align": "left",
        "title_fill": "#17213c",
        "title_size": 27,
        "subtitle_fill": "#6e6a61",
        "subtitle_size": 13,
        "node_fill": "#fffdf8",
        "node_stroke": "#c9c2b4",
        "node_radius": 24,
        "node_shadow": "",
        "section_fill": "rgba(255,255,255,0.38)",
        "section_stroke": "#d4cbbb",
        "section_dash": "4 5",
        "section_label_fill": "#514c43",
        "section_sub_fill": "#8d867b",
        "title_divider": False,
        "section_upper": True,
        "arrow_width": 2.8,
        "arrow_colors": {
            "control": "#e4475b",
            "write": "#00897b",
            "read": "#2563eb",
            "data": "#f59e0b",
            "async": "#7c3aed",
            "feedback": "#c62828",
            "neutral": "#7a746a",
        },
        "arrow_label_bg": "#fbf7ee",
        "arrow_label_opacity": 0.96,
        "arrow_label_fill": "#4b5563",
        "type_label_fill": "#7a746a",
        "type_label_size": 10,
        "text_primary": "#17213c",
        "text_secondary": "#5e5a52",
        "text_muted": "#8d867b",
        "legend_fill": "#5e5a52",
        "canvas_treatment": "transit",
        "rail_casing": "#514c43",
    },
    12: {
        "name": "Ops Pulse",
        "font_family": "'SF Mono', 'Fira Code', Menlo, 'Microsoft YaHei', monospace",
        "background": "#07111f",
        "shadow": False,
        "title_align": "left",
        "title_fill": "#eff6ff",
        "title_size": 27,
        "subtitle_fill": "#8aa4bd",
        "subtitle_size": 13,
        "node_fill": "#0d1b2a",
        "node_stroke": "#29435d",
        "node_radius": 12,
        "node_shadow": "",
        "section_fill": "rgba(13,27,42,0.72)",
        "section_stroke": "#28445f",
        "section_dash": "6 5",
        "section_label_fill": "#38bdf8",
        "section_sub_fill": "#6f8ba5",
        "title_divider": False,
        "section_upper": True,
        "arrow_width": 2.4,
        "arrow_colors": {
            "control": "#f59e0b",
            "write": "#22c55e",
            "read": "#38bdf8",
            "data": "#fb7185",
            "async": "#22d3ee",
            "feedback": "#f43f5e",
            "neutral": "#7892a8",
        },
        "arrow_label_bg": "#07111f",
        "arrow_label_opacity": 0.94,
        "arrow_label_fill": "#cbd5e1",
        "type_label_fill": "#6f8ba5",
        "type_label_size": 10,
        "text_primary": "#eff6ff",
        "text_secondary": "#9fb3c8",
        "text_muted": "#647f99",
        "legend_fill": "#9fb3c8",
        "canvas_treatment": "ops",
    },
}


@dataclass
class Node:
    node_id: str
    kind: str
    shape: str
    data: Dict[str, object]
    bounds: Bounds
    cx: float
    cy: float


@dataclass
class ArrowRender:
    edge_id: str
    path_svg: str
    label_svg: str
    label_bounds: Optional[Bounds]
    route: List[Point]
    report: Dict[str, object]


def style_value(style: Dict[str, object], key: str) -> object:
    return style[key]


def to_float(value: object, default: float = 0.0) -> float:
    try:
        return float(value)
    except (TypeError, ValueError):
        return default


def normalize_text(value: object) -> str:
    return escape(str(value)) if value is not None else ""


def normalize_attribute(value: object) -> str:
    return escape(str(value), {'"': "&quot;"}) if value is not None else ""


def safe_identifier(value: object, fallback: str) -> str:
    cleaned = re.sub(r"[^A-Za-z0-9_.:-]+", "-", str(value or fallback)).strip("-")
    return cleaned or fallback


_RESERVED_DOM_IDS = frozenset(
    set(MARKER_IDS.values())
    | {
        "blueprint-title-block",
        "blueprintGrid",
        "cloudGradient",
        "cloudGrid",
        "footer",
        "glowBlue",
        "glowGreen",
        "glowOrange",
        "glowPurple",
        "legend",
        "legend-zone",
        "opsGradient",
        "opsGrid",
        "pulseGlow",
        "reviewGrid",
        "shadowGlass",
        "shadowSoft",
        "style-signature",
        "terminalGradient",
        "transitDots",
    }
)
_EDGE_DOM_SUFFIXES = (
    "-bridge-mask",
    "-critical-glow",
    "-direction",
    "-hop",
    "-label",
    "-rail-casing",
    "-review-stroke",
)


def allocate_dom_identifier(base: str, used: set[str], suffixes: Sequence[str] = ()) -> str:
    """Allocate one deterministic SVG id while preserving readable ids when safe."""

    candidate = safe_identifier(base, "element")
    sequence = 2
    while any(identifier in used for identifier in (candidate, *(candidate + suffix for suffix in suffixes))):
        candidate = f"{safe_identifier(base, 'element')}-{sequence}"
        sequence += 1
    used.add(candidate)
    used.update(candidate + suffix for suffix in suffixes)
    return candidate


# Style 8 is AI-authored: the AI reads references/style-8-dark-luxury.md and hand-crafts
# the SVG directly. It cannot be driven by this template generator.
_AI_AUTHORED_STYLES: Dict[int, str] = {8: "Style 8 (Dark Luxury)"}
_AI_AUTHORED_MSG = (
    "{name} is an AI-authored style and cannot be used with generate-from-template.py. "
    "Load references/style-8-dark-luxury.md for the full spec and hand-craft the SVG directly."
)


def parse_style(raw: object) -> Tuple[int, Dict[str, object]]:
    index = resolve_style_index({"style": raw}) if raw is not None else 1
    if index in _AI_AUTHORED_STYLES:
        raise ValueError(_AI_AUTHORED_MSG.format(name=_AI_AUTHORED_STYLES[index]))
    if index not in STYLE_PROFILES:
        raise ValueError(f"Unsupported style: {raw}")
    return index, copy.deepcopy(STYLE_PROFILES[index])


def parse_template_viewbox(template_type: str) -> Tuple[float, float]:
    template_path = os.path.join(TEMPLATE_DIR, f"{template_type}.svg")
    if os.path.exists(template_path):
        with open(template_path, "r", encoding="utf-8") as handle:
            content = handle.read()
        match = re.search(r'viewBox="0 0 ([0-9.]+) ([0-9.]+)"', content)
        if match:
            return float(match.group(1)), float(match.group(2))
    return DEFAULT_VIEWBOX.get(template_type, (960, 600))


def render_defs(style_index: int, style: Dict[str, object]) -> str:
    marker_size = "8" if style_index == 4 else "12" if style_index == 11 else "10"
    marker_height = "6" if style_index == 4 else "9" if style_index == 11 else "7"
    ref_x = "7" if style_index == 4 else "11" if style_index == 11 else "9"
    ref_y = "3" if style_index == 4 else "4.5" if style_index == 11 else "3.5"
    marker_units = ' markerUnits="userSpaceOnUse"' if style_index == 11 else ""
    color_map = style_value(style, "arrow_colors")
    marker_lines = []
    for key, color in color_map.items():
        marker_id = MARKER_IDS.get(key, "arrowA")
        marker_lines.append(
            f'    <marker id="{marker_id}" markerWidth="{marker_size}" markerHeight="{marker_height}" '
            f'refX="{ref_x}" refY="{ref_y}" orient="auto"{marker_units}>'
        )
        if style_index == 4:
            marker_lines.append(f'      <polygon points="0 0, 8 3, 0 6" fill="{color}"/>')
        elif style_index == 11:
            marker_lines.append(f'      <polygon points="0 0, 12 4.5, 0 9" fill="{color}"/>')
        else:
            marker_lines.append(f'      <polygon points="0 0, 10 3.5, 0 7" fill="{color}"/>')
        marker_lines.append("    </marker>")

    filters = []
    if style_value(style, "shadow"):
        filters.extend(
            [
                '    <filter id="shadowSoft" x="-20%" y="-20%" width="140%" height="160%">',
                '      <feDropShadow dx="0" dy="3" stdDeviation="6" flood-color="#0f172a" flood-opacity="0.12"/>',
                "    </filter>",
                '    <filter id="shadowGlass" x="-20%" y="-20%" width="140%" height="160%">',
                '      <feDropShadow dx="0" dy="10" stdDeviation="16" flood-color="#020617" flood-opacity="0.28"/>',
                "    </filter>",
            ]
        )

    if style_index == 3:
        filters.extend(
            [
                '    <pattern id="blueprintGrid" width="32" height="32" patternUnits="userSpaceOnUse">',
                '      <path d="M 32 0 L 0 0 0 32" fill="none" stroke="#0ea5e9" stroke-opacity="0.12" stroke-width="1"/>',
                "    </pattern>",
            ]
        )
    if style_index == 2:
        filters.extend(
            [
                '    <linearGradient id="terminalGradient" x1="0%" y1="0%" x2="100%" y2="100%">',
                '      <stop offset="0%" stop-color="#0f0f1a"/>',
                '      <stop offset="100%" stop-color="#1a1a2e"/>',
                "    </linearGradient>",
                '    <filter id="glowBlue" x="-30%" y="-30%" width="160%" height="160%">',
                '      <feDropShadow dx="0" dy="0" stdDeviation="5" flood-color="#3b82f6" flood-opacity="0.65"/>',
                "    </filter>",
                '    <filter id="glowPurple" x="-30%" y="-30%" width="160%" height="160%">',
                '      <feDropShadow dx="0" dy="0" stdDeviation="5" flood-color="#a855f7" flood-opacity="0.72"/>',
                "    </filter>",
                '    <filter id="glowGreen" x="-30%" y="-30%" width="160%" height="160%">',
                '      <feDropShadow dx="0" dy="0" stdDeviation="5" flood-color="#22c55e" flood-opacity="0.62"/>',
                "    </filter>",
                '    <filter id="glowOrange" x="-30%" y="-30%" width="160%" height="160%">',
                '      <feDropShadow dx="0" dy="0" stdDeviation="5" flood-color="#f97316" flood-opacity="0.62"/>',
                "    </filter>",
            ]
        )
    if style_index == 9:
        filters.extend(
            [
                '    <pattern id="reviewGrid" width="24" height="24" patternUnits="userSpaceOnUse">',
                '      <circle cx="1" cy="1" r="0.8" fill="#8c7d68" fill-opacity="0.18"/>',
                "    </pattern>",
            ]
        )
    if style_index == 10:
        filters.extend(
            [
                '    <linearGradient id="cloudGradient" x1="0%" y1="0%" x2="100%" y2="100%">',
                '      <stop offset="0%" stop-color="#f8fcff"/>',
                '      <stop offset="100%" stop-color="#dfedf7"/>',
                "    </linearGradient>",
                '    <pattern id="cloudGrid" width="32" height="32" patternUnits="userSpaceOnUse">',
                '      <path d="M 32 0 L 0 0 0 32" fill="none" stroke="#7fa3c2" stroke-opacity="0.10" stroke-width="1"/>',
                "    </pattern>",
            ]
        )
    if style_index == 11:
        filters.extend(
            [
                '    <pattern id="transitDots" width="28" height="28" patternUnits="userSpaceOnUse">',
                '      <circle cx="2" cy="2" r="0.9" fill="#8d867b" fill-opacity="0.12"/>',
                "    </pattern>",
            ]
        )
    if style_index == 12:
        filters.extend(
            [
                '    <linearGradient id="opsGradient" x1="0%" y1="0%" x2="100%" y2="100%">',
                '      <stop offset="0%" stop-color="#07111f"/>',
                '      <stop offset="100%" stop-color="#0b1b2e"/>',
                "    </linearGradient>",
                '    <pattern id="opsGrid" width="36" height="36" patternUnits="userSpaceOnUse">',
                '      <path d="M 36 0 L 0 0 0 36" fill="none" stroke="#38bdf8" stroke-opacity="0.055" stroke-width="1"/>',
                "    </pattern>",
                '    <filter id="pulseGlow" x="-30%" y="-30%" width="160%" height="160%">',
                '      <feDropShadow dx="0" dy="0" stdDeviation="4" flood-color="#f59e0b" flood-opacity="0.62"/>',
                "    </filter>",
            ]
        )

    styles = [
        f"    text {{ font-family: {style_value(style, 'font_family')}; }}",
        f"    .title {{ font-size: {style_value(style, 'title_size')}px; font-weight: 700; fill: {style_value(style, 'title_fill')}; }}",
        f"    .subtitle {{ font-size: {style_value(style, 'subtitle_size')}px; font-weight: 500; fill: {style_value(style, 'subtitle_fill')}; }}",
        f"    .section {{ font-size: 13px; font-weight: 700; fill: {style_value(style, 'section_label_fill')}; letter-spacing: 1.4px; }}",
        f"    .section-sub {{ font-size: 12px; font-weight: 500; fill: {style_value(style, 'section_sub_fill')}; }}",
        f"    .node-title {{ font-weight: 700; fill: {style_value(style, 'text_primary')}; }}",
        f"    .node-sub {{ font-size: 12px; font-weight: 500; fill: {style_value(style, 'text_secondary')}; }}",
        f"    .node-type {{ font-size: {style_value(style, 'type_label_size')}px; font-weight: 700; fill: {style_value(style, 'type_label_fill')}; letter-spacing: 0.08em; }}",
        f"    .arrow-label {{ font-size: 12px; font-weight: 600; fill: {style_value(style, 'arrow_label_fill')}; }}",
        f"    .legend {{ font-size: 12px; font-weight: 500; fill: {style_value(style, 'legend_fill')}; }}",
        f"    .footnote {{ font-size: 12px; font-weight: 500; fill: {style_value(style, 'text_muted')}; }}",
        f"    .metric-label {{ font-size: 8.5px; font-weight: 700; fill: {style_value(style, 'text_muted')}; text-transform: uppercase; }}",
        f"    .metric-value {{ font-size: 9.5px; font-weight: 700; fill: {style_value(style, 'text_primary')}; }}",
    ]
    return "\n".join(
        ["  <defs>"] + marker_lines + filters + ["    <style>"] + styles + ["    </style>", "  </defs>"]
    )


def render_canvas(style_index: int, style: Dict[str, object], width: float, height: float) -> str:
    background = str(style_value(style, "background"))
    if style_index == 2:
        parts = [f'  <rect data-graph-role="background" width="{width}" height="{height}" fill="url(#terminalGradient)"/>']
    elif style_index == 9:
        parts = [
            f'  <rect data-graph-role="background" width="{width}" height="{height}" fill="{background}"/>',
            f'  <rect data-graph-role="decoration" width="{width}" height="{height}" fill="url(#reviewGrid)"/>',
        ]
    elif style_index == 10:
        parts = [
            f'  <rect data-graph-role="background" width="{width}" height="{height}" fill="url(#cloudGradient)"/>',
            f'  <rect data-graph-role="decoration" width="{width}" height="{height}" fill="url(#cloudGrid)"/>',
        ]
    elif style_index == 11:
        parts = [
            f'  <rect data-graph-role="background" width="{width}" height="{height}" fill="{background}"/>',
            f'  <rect data-graph-role="decoration" width="{width}" height="{height}" fill="url(#transitDots)"/>',
        ]
    elif style_index == 12:
        parts = [
            f'  <rect data-graph-role="background" width="{width}" height="{height}" fill="url(#opsGradient)"/>',
            f'  <rect data-graph-role="decoration" width="{width}" height="{height}" fill="url(#opsGrid)"/>',
        ]
    else:
        parts = [f'  <rect data-graph-role="background" width="{width}" height="{height}" fill="{background}"/>']

    return "\n".join(parts)


def title_position(style: Dict[str, object], width: float) -> Tuple[float, str]:
    if style_value(style, "title_align") == "left":
        return 48.0, "start"
    return width / 2.0, "middle"


def render_title_block(style: Dict[str, object], data: Dict[str, object], width: float) -> Tuple[str, float]:
    title = normalize_text(data.get("title", "Diagram"))
    subtitle = normalize_text(data.get("subtitle", ""))
    x, anchor = title_position(style, width)
    if anchor == "middle":
        parts = [f'  <text x="{x}" y="56" text-anchor="{anchor}" class="title">{title}</text>']
        cursor_y = 82
        if subtitle:
            parts.append(f'  <text x="{x}" y="{cursor_y}" text-anchor="{anchor}" class="subtitle">{subtitle}</text>')
            cursor_y += 24
        return "\n".join(parts), cursor_y + 10

    parts = [f'  <text x="{x}" y="48" text-anchor="{anchor}" class="title">{title}</text>']
    cursor_y = 72
    if subtitle:
        parts.append(f'  <text x="{x}" y="{cursor_y}" text-anchor="{anchor}" class="subtitle">{subtitle}</text>')
        cursor_y += 18
    if style_value(style, "title_divider"):
        parts.append(
            f'  <line x1="48" y1="{cursor_y + 10}" x2="{width - 48}" y2="{cursor_y + 10}" '
            f'stroke="{style_value(style, "section_stroke")}" stroke-width="1"/>'
        )
        cursor_y += 26
    return "\n".join(parts), cursor_y + 8


def render_window_controls(data: Dict[str, object], style_index: int, width: float) -> str:
    controls = data.get("window_controls")
    if not controls:
        return ""
    if controls is True:
        controls = ["#ef4444", "#f59e0b", "#10b981"]
    if style_index != 2:
        return ""
    cursor_x = 20.0
    lines = []
    for color in controls:
        lines.append(f'  <circle cx="{cursor_x}" cy="20" r="5.5" fill="{color}"/>')
        cursor_x += 18
    return "\n".join(lines)


def render_header_meta(data: Dict[str, object], style: Dict[str, object], width: float) -> str:
    meta_left = normalize_text(data.get("meta_left", ""))
    meta_center = normalize_text(data.get("meta_center", ""))
    meta_right = normalize_text(data.get("meta_right", ""))
    if not any([meta_left, meta_center, meta_right]):
        return ""
    fill = str(data.get("meta_fill", style_value(style, "text_muted")))
    size = to_float(data.get("meta_size", 11))
    lines = []
    if meta_left:
        lines.append(f'  <text x="28" y="24" font-size="{size}" font-weight="600" fill="{fill}">{meta_left}</text>')
    if meta_center:
        lines.append(f'  <text x="{width / 2}" y="24" text-anchor="middle" font-size="{size}" font-weight="600" fill="{fill}">{meta_center}</text>')
    if meta_right:
        lines.append(f'  <text x="{width - 28}" y="24" text-anchor="end" font-size="{size}" font-weight="600" fill="{fill}">{meta_right}</text>')
    return "\n".join(lines)


def render_style_signature(style_index: int, data: Dict[str, object], width: float) -> str:
    """Expose each engineering style's domain evidence as a compact visual fingerprint."""

    if style_index not in {9, 10, 11, 12}:
        return ""
    badge_width = 176.0
    badge_height = 34.0
    x = width - 48 - badge_width
    y = 22.0
    if style_index == 9:
        level = str(data.get("c4_level", "review")).upper()
        state = str(data.get("review_state", "REVIEW READY")).upper()
        top_raw = f"C4 · {level} VIEW"
        top_text, top_size = fit_single_line_text(top_raw, badge_width - 46, preferred=8.5, minimum=6.2)
        state_text, state_size = fit_single_line_text(state, badge_width - 46, preferred=8, minimum=6.2)
        return "\n".join(
            [
            '  <g id="style-signature" data-graph-role="decoration" data-style-signature="c4-review-board">',
                f'    <rect x="{x}" y="{y}" width="{badge_width}" height="{badge_height}" rx="7" fill="#fffdf7" stroke="#8c7d68" stroke-width="1.2" stroke-dasharray="6 4"/>',
                f'    <path d="M {x + 12} {y + 17} l 5 5 9 -11" fill="none" stroke="#365f56" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
                f'    <text data-full-text="{normalize_attribute(top_raw)}" x="{x + 34}" y="{y + 14}" font-size="{top_size}" font-weight="800" fill="#8a6f43">{normalize_text(top_text)}</text>',
                f'    <text data-full-text="{normalize_attribute(state)}" x="{x + 34}" y="{y + 27}" font-size="{state_size}" font-weight="700" fill="#5f665f">{normalize_text(state_text)}</text>',
                "  </g>",
            ]
        )
    if style_index == 10:
        platform = str(data.get("platform_profile", "cloud")).upper()
        mode = str(data.get("deployment_mode", "DEPLOYMENT MAP")).upper()
        regions = sum(
            1
            for container in data.get("containers", [])
            if isinstance(container, Mapping) and container.get("deployment_kind") == "region"
        )
        top_raw = f"{platform} · {regions} REGIONS"
        top_text, top_size = fit_single_line_text(top_raw, badge_width - 55, preferred=8.5, minimum=6.2)
        mode_text, mode_size = fit_single_line_text(mode, badge_width - 55, preferred=8, minimum=6.2)
        return "\n".join(
            [
            '  <g id="style-signature" data-graph-role="decoration" data-style-signature="cloud-ownership-map">',
                f'    <rect x="{x}" y="{y}" width="{badge_width}" height="{badge_height}" rx="9" fill="#ffffff" fill-opacity="0.82" stroke="#7fa3c2" stroke-width="1.1"/>',
                f'    <rect x="{x + 11}" y="{y + 9}" width="14" height="14" rx="4" fill="#dbeafe" stroke="#2563eb" stroke-width="1"/>',
                f'    <rect x="{x + 20}" y="{y + 13}" width="14" height="14" rx="4" fill="#dcfce7" stroke="#059669" stroke-width="1"/>',
                f'    <text data-full-text="{normalize_attribute(top_raw)}" x="{x + 43}" y="{y + 14}" font-size="{top_size}" font-weight="800" fill="#315d7e">{normalize_text(top_text)}</text>',
                f'    <text data-full-text="{normalize_attribute(mode)}" x="{x + 43}" y="{y + 27}" font-size="{mode_size}" font-weight="700" fill="#52718d">{normalize_text(mode_text)}</text>',
                "  </g>",
            ]
        )
    if style_index == 11:
        topics = data.get("topics", [])
        line_count = len(topics) if isinstance(topics, list) else 0
        line_code = str(data.get("line_code", "EVENT METRO")).upper()
        signature_width = 226.0
        signature_x = width - 48 - signature_width
        line_code_text, line_code_size = fit_single_line_text(
            line_code, signature_width - 58, preferred=8.5, minimum=6.2
        )
        detail_raw = f"{line_count} TOPIC LINES · DECLARED STOPS"
        detail_text, detail_size = fit_single_line_text(
            detail_raw, signature_width - 58, preferred=8, minimum=6.2
        )
        return "\n".join(
            [
            '  <g id="style-signature" data-graph-role="decoration" data-style-signature="event-metro-map">',
                f'    <rect x="{signature_x}" y="{y}" width="{signature_width}" height="{badge_height}" rx="7" fill="#17213c" stroke="#514c43" stroke-width="1"/>',
                f'    <line x1="{signature_x + 12}" y1="{y + 17}" x2="{signature_x + 36}" y2="{y + 17}" stroke="#e4475b" stroke-width="3"/>',
                f'    <circle cx="{signature_x + 18}" cy="{y + 17}" r="4" fill="#fbf7ee" stroke="#e4475b" stroke-width="2"/>',
                f'    <circle cx="{signature_x + 31}" cy="{y + 17}" r="4" fill="#fbf7ee" stroke="#e4475b" stroke-width="2"/>',
                f'    <text data-full-text="{normalize_attribute(line_code)}" x="{signature_x + 46}" y="{y + 14}" font-size="{line_code_size}" font-weight="800" fill="#ffffff">{normalize_text(line_code_text)}</text>',
                f'    <text data-full-text="{normalize_attribute(detail_raw)}" x="{signature_x + 46}" y="{y + 27}" font-size="{detail_size}" font-weight="700" fill="#f3d5d9">{normalize_text(detail_text)}</text>',
                "  </g>",
            ]
        )

    services = [
        node
        for node in data.get("nodes", [])
        if isinstance(node, Mapping) and node.get("ops_role") == "service"
    ]
    rank = {"unknown": 0, "ok": 1, "warn": 2, "critical": 3}
    worst = max(
        (str(node.get("status", "unknown")) for node in services),
        key=lambda item: rank.get(item, 0),
        default="unknown",
    )
    window = str(data.get("observation_window", ""))
    if not window:
        for node in services:
            signals = node.get("signals")
            if isinstance(signals, Mapping):
                first_signal = next((value for value in signals.values() if isinstance(value, Mapping)), None)
                if first_signal:
                    window = str(first_signal.get("window", ""))
                    break
    status_color = {"ok": "#22c55e", "warn": "#f59e0b", "critical": "#f43f5e", "unknown": "#64748b"}.get(worst, "#64748b")
    top_raw = f'LIVE · {window.upper() or "WINDOW"}'
    detail_raw = f"{worst.upper()} · CORRELATED TRACE"
    top_text, top_size = fit_single_line_text(top_raw, badge_width - 70, preferred=8.5, minimum=6.2)
    detail_text, detail_size = fit_single_line_text(detail_raw, badge_width - 70, preferred=8, minimum=6.2)
    return "\n".join(
        [
            '  <g id="style-signature" data-graph-role="decoration" data-style-signature="ops-live-investigation">',
            f'    <rect x="{x}" y="{y}" width="{badge_width}" height="{badge_height}" rx="7" fill="#0d1b2a" stroke="#29435d" stroke-width="1.1"/>',
            f'    <circle cx="{x + 15}" cy="{y + 17}" r="4" fill="{status_color}"/>',
            f'    <path d="M {x + 25} {y + 18} h 5 l 3 -6 5 12 4 -8 h 7" fill="none" stroke="#38bdf8" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>',
            f'    <text data-full-text="{normalize_attribute(top_raw)}" x="{x + 58}" y="{y + 14}" font-size="{top_size}" font-weight="800" fill="#eff6ff">{normalize_text(top_text)}</text>',
            f'    <text data-full-text="{normalize_attribute(detail_raw)}" x="{x + 58}" y="{y + 27}" font-size="{detail_size}" font-weight="700" fill="{status_color}">{normalize_text(detail_text)}</text>',
            "  </g>",
        ]
    )


def render_blueprint_title_block(
    data: Dict[str, object],
    style: Dict[str, object],
    style_index: int,
    width: float,
    height: float,
) -> Tuple[str, Optional[Bounds]]:
    if style_index != 3:
        return "", None
    block = data.get("blueprint_title_block")
    if not block:
        return "", None
    block_width = to_float(block.get("width", 256))
    block_height = to_float(block.get("height", 92))
    x = to_float(block.get("x", width - block_width - 28))
    y = to_float(block.get("y", height - block_height - 18))
    title = normalize_text(block.get("title", data.get("title", "")))
    subtitle = normalize_text(block.get("subtitle", "SYSTEM ARCHITECTURE"))
    left_caption = normalize_text(block.get("left_caption", "REV: 1.0"))
    center_caption = normalize_text(block.get("center_caption", "AUTO-GENERATED"))
    right_caption = normalize_text(block.get("right_caption", "DWG: ARCH-001"))
    stroke = str(block.get("stroke", style_value(style, "section_stroke")))
    fill = str(block.get("fill", "#0b3552"))
    title_fill = str(block.get("title_fill", style_value(style, "text_primary")))
    sub_fill = str(block.get("subtitle_fill", style_value(style, "section_label_fill")))
    muted_fill = str(block.get("muted_fill", style_value(style, "text_muted")))
    footer_top = y + 54
    column_width = block_width / 3
    caption_width = max(24.0, column_width - 12)

    def caption_size(value: str) -> float:
        estimated = geometry.estimate_text_width(value, 9.5)
        return round(max(6.0, min(9.5, 9.5 * caption_width / max(estimated, 1.0))), 2)

    caption_y = min(y + block_height - 4, footer_top + max(12.0, block_height - 54) / 2 + 3)
    captions = (
        (left_caption, x + column_width / 2, muted_fill),
        (center_caption, x + block_width / 2, sub_fill),
        (right_caption, x + block_width - column_width / 2, muted_fill),
    )
    lines = [
        f'  <rect x="{x}" y="{y}" width="{block_width}" height="{block_height}" fill="{fill}" stroke="{stroke}" stroke-width="1.2"/>',
        f'  <line x1="{x}" y1="{y + 18}" x2="{x + block_width}" y2="{y + 18}" stroke="{stroke}" stroke-width="1"/>',
        f'  <line x1="{x}" y1="{y + 54}" x2="{x + block_width}" y2="{y + 54}" stroke="{stroke}" stroke-width="1"/>',
        f'  <line x1="{x + column_width}" y1="{footer_top}" x2="{x + column_width}" y2="{y + block_height}" stroke="{stroke}" stroke-width="0.7"/>',
        f'  <line x1="{x + 2 * column_width}" y1="{footer_top}" x2="{x + 2 * column_width}" y2="{y + block_height}" stroke="{stroke}" stroke-width="0.7"/>',
        f'  <text x="{x + block_width / 2}" y="{y + 13}" text-anchor="middle" font-size="10" font-weight="600" fill="{muted_fill}">{subtitle}</text>',
        f'  <text x="{x + block_width / 2}" y="{y + 42}" text-anchor="middle" font-size="18" font-weight="700" fill="{title_fill}">{title}</text>',
    ]
    lines.extend(
        f'  <text x="{caption_x}" y="{caption_y}" text-anchor="middle" font-size="{caption_size(caption)}" '
        f'font-weight="600" fill="{caption_fill}">{caption}</text>'
        for caption, caption_x, caption_fill in captions
    )
    return "\n".join(lines), rectangle_bounds(x - 6, y - 6, block_width + 12, block_height + 12)


def infer_shape(kind: str) -> str:
    mapping = {
        "rect": "rect",
        "double_rect": "rect",
        "cylinder": "rect",
        "document": "rect",
        "folder": "rect",
        "terminal": "rect",
        "hexagon": "rect",
        "circle_cluster": "cluster",
        "user_avatar": "rect",
        "bot": "rect",
        "speech": "rect",
        "icon_box": "rect",
    }
    return mapping.get(kind, "rect")


def node_bounds(data: Dict[str, object]) -> Bounds:
    kind = str(data.get("kind", data.get("shape", "rect")))
    x = to_float(data.get("x"))
    y = to_float(data.get("y"))
    if kind == "circle":
        r = to_float(data.get("r", 50))
        return (x - r, y - r, x + r, y + r)
    width = to_float(data.get("width", 180))
    height = to_float(data.get("height", 76))
    return (x, y, x + width, y + height)


def normalize_node(node_data: Dict[str, object], fallback_id: str) -> Node:
    kind = str(node_data.get("kind", node_data.get("shape", "rect")))
    bounds = node_bounds(node_data)
    left, top, right, bottom = bounds
    return Node(
        node_id=str(node_data.get("id", fallback_id)),
        kind=kind,
        shape=infer_shape(kind),
        data=node_data,
        bounds=bounds,
        cx=(left + right) / 2,
        cy=(top + bottom) / 2,
    )


def anchor_on_side(node: Node, side: str, offset: float = 0.0) -> Point:
    left, top, right, bottom = node.bounds
    cx, cy = node.cx, node.cy
    side = side.lower()
    safe_x = min(max(cx + offset, left + 12), right - 12)
    safe_y = min(max(cy + offset, top + 12), bottom - 12)
    if side == "left":
        return (left, safe_y)
    if side == "right":
        return (right, safe_y)
    if side == "top":
        return (safe_x, top)
    if side == "bottom":
        return (safe_x, bottom)
    if side == "top-left":
        return (left, top)
    if side == "top-right":
        return (right, top)
    if side == "bottom-left":
        return (left, bottom)
    if side == "bottom-right":
        return (right, bottom)
    return (cx, cy)


def anchor_point(node: Node, toward: Point, port: Optional[str] = None, offset: float = 0.0) -> Point:
    if port:
        return anchor_on_side(node, port, offset)
    left, top, right, bottom = node.bounds
    dx = toward[0] - node.cx
    dy = toward[1] - node.cy
    width = right - left
    height = bottom - top
    if abs(dx) * height >= abs(dy) * width:
        return (right, node.cy) if dx >= 0 else (left, node.cy)
    return (node.cx, bottom) if dy >= 0 else (node.cx, top)


def expand_bounds(bounds: Bounds, padding: float) -> Bounds:
    left, top, right, bottom = bounds
    return (left - padding, top - padding, right + padding, bottom + padding)


def segment_hits_bounds(p1: Point, p2: Point, bounds: Bounds) -> bool:
    x1, y1 = p1
    x2, y2 = p2
    left, top, right, bottom = bounds
    eps = 1e-6

    if abs(y1 - y2) < eps:
        y = y1
        if not (top + eps < y < bottom - eps):
            return False
        seg_left = min(x1, x2)
        seg_right = max(x1, x2)
        overlap_left = max(seg_left, left)
        overlap_right = min(seg_right, right)
        if overlap_right - overlap_left <= eps:
            return False
        if abs(overlap_left - x1) < eps and abs(overlap_right - x1) < eps:
            return False
        if abs(overlap_left - x2) < eps and abs(overlap_right - x2) < eps:
            return False
        return True

    if abs(x1 - x2) < eps:
        x = x1
        if not (left + eps < x < right - eps):
            return False
        seg_top = min(y1, y2)
        seg_bottom = max(y1, y2)
        overlap_top = max(seg_top, top)
        overlap_bottom = min(seg_bottom, bottom)
        if overlap_bottom - overlap_top <= eps:
            return False
        if abs(overlap_top - y1) < eps and abs(overlap_bottom - y1) < eps:
            return False
        if abs(overlap_top - y2) < eps and abs(overlap_bottom - y2) < eps:
            return False
        return True

    return False


def segment_axis(p1: Point, p2: Point) -> str:
    if abs(p1[1] - p2[1]) < 1e-6:
        return "horizontal"
    if abs(p1[0] - p2[0]) < 1e-6:
        return "vertical"
    return "other"


def port_axis(port: Optional[str]) -> Optional[str]:
    if not port:
        return None
    port = port.lower()
    if port in {"left", "right"}:
        return "horizontal"
    if port in {"top", "bottom"}:
        return "vertical"
    return None


def offset_point(point: Point, port: Optional[str], distance: float) -> Point:
    if not port:
        return point
    x, y = point
    port = port.lower()
    if port == "left":
        return (x - distance, y)
    if port == "right":
        return (x + distance, y)
    if port == "top":
        return (x, y - distance)
    if port == "bottom":
        return (x, y + distance)
    return point


def clear_port_point(
    endpoint: Point,
    port: Optional[str],
    desired_distance: float,
    obstacles: Sequence[Bounds],
    canvas_bounds: Optional[Bounds],
) -> Point:
    """Choose the longest safe straight lead from a node port."""

    distances = [desired_distance * fraction for fraction in (1.0, 0.75, 0.5, 0.35, 0.2, 0.0)]
    for distance in distances:
        candidate = offset_point(endpoint, port, distance)
        if canvas_bounds is not None and not geometry.point_in_bounds(candidate, canvas_bounds):
            continue
        if any(
            geometry.point_in_bounds(candidate, obstacle, interior=True)
            or segment_hits_bounds(endpoint, candidate, obstacle)
            for obstacle in obstacles
        ):
            continue
        return candidate
    return endpoint


def route_length(points: Sequence[Point]) -> float:
    return sum(abs(x1 - x2) + abs(y1 - y2) for (x1, y1), (x2, y2) in zip(points, points[1:]))


def route_uses_lane(points: Sequence[Point], value: float, axis: str, tolerance: float = 1.0) -> bool:
    if axis == "x":
        return any(abs(x - value) <= tolerance for x, _ in points)
    return any(abs(y - value) <= tolerance for _, y in points)


def collision_count(points: Sequence[Point], obstacles: Sequence[Bounds]) -> int:
    """Count how many (segment, obstacle) pairs collide."""
    return sum(
        1
        for p1, p2 in zip(points, points[1:])
        for obs in obstacles
        if segment_hits_bounds(p1, p2, obs)
    )


def route_is_orthogonal(points: Sequence[Point]) -> bool:
    return all(segment_axis(p1, p2) != "other" for p1, p2 in zip(points, points[1:]))


def route_crossing_count(points: Sequence[Point], existing_routes: Sequence[Sequence[Point]]) -> int:
    return geometry.route_crossing_count(points, existing_routes)


def route_score(
    points: Sequence[Point],
    hint_x: Sequence[float],
    hint_y: Sequence[float],
    source_port: Optional[str],
    target_port: Optional[str],
    existing_routes: Sequence[Sequence[Point]] = (),
) -> float:
    length = route_length(points)
    bends = max(0, len(points) - 2)
    score = length + bends * 22
    if len(points) >= 2 and source_port:
        first_axis = segment_axis(points[0], points[1])
        if first_axis != port_axis(source_port):
            score += 180
    if len(points) >= 2 and target_port:
        last_axis = segment_axis(points[-2], points[-1])
        if last_axis != port_axis(target_port):
            score += 180
    for lane in hint_x:
        score -= 28 if route_uses_lane(points, lane, "x") else 0
    for lane in hint_y:
        score -= 28 if route_uses_lane(points, lane, "y") else 0
    interactions = geometry.route_interactions(points, existing_routes)
    score += len(interactions.crossings) * 640
    score += interactions.overlap_count * 900 + interactions.overlap_length * 18
    return score


def simplify_points(points: Sequence[Point], protected: Sequence[Point] = ()) -> List[Point]:
    protected_points = {(round(point[0], 2), round(point[1], 2)) for point in protected}
    simplified: List[Point] = []
    for x, y in points:
        pt = (round(x, 2), round(y, 2))
        if simplified and pt == simplified[-1]:
            continue
        simplified.append(pt)

    collapsed: List[Point] = []
    for point in simplified:
        if len(collapsed) < 2:
            collapsed.append(point)
            continue
        x0, y0 = collapsed[-2]
        x1, y1 = collapsed[-1]
        x2, y2 = point
        # A same-axis reversal adds length and can create sub-pixel hairpins
        # when two port-clearance leads nearly meet.  The straight segment
        # from the first to the third point covers the same safe corridor, so
        # collapse both monotonic and reversing collinear triples unless the
        # middle point is an explicit user waypoint.
        collinear_vertical = x0 == x1 == x2
        collinear_horizontal = y0 == y1 == y2
        if (collinear_vertical or collinear_horizontal) and (x1, y1) not in protected_points:
            collapsed[-1] = point
        else:
            collapsed.append(point)
    return collapsed


def route_collides(points: Sequence[Point], obstacles: Sequence[Bounds]) -> bool:
    return collision_count(points, obstacles) > 0


def visibility_grid_route(
    start: Point,
    end: Point,
    obstacles: Sequence[Bounds],
    *,
    canvas_bounds: Optional[Bounds],
    hint_x: Sequence[float],
    hint_y: Sequence[float],
    existing_routes: Sequence[Sequence[Point]],
) -> Optional[List[Point]]:
    """Find a deterministic rectilinear route on an obstacle visibility grid."""

    if start == end:
        return [start]
    if canvas_bounds is None:
        all_x = [start[0], end[0], *[value for bounds in obstacles for value in (bounds[0], bounds[2])]]
        all_y = [start[1], end[1], *[value for bounds in obstacles for value in (bounds[1], bounds[3])]]
        canvas_bounds = (min(all_x) - 64, min(all_y) - 64, max(all_x) + 64, max(all_y) + 64)
    canvas_left, canvas_top, canvas_right, canvas_bottom = canvas_bounds
    inset = 4.0

    x_values = {
        round(start[0], 2),
        round(end[0], 2),
        round((start[0] + end[0]) / 2, 2),
        round(canvas_left + inset, 2),
        round(canvas_right - inset, 2),
        *[round(value, 2) for value in hint_x],
    }
    y_values = {
        round(start[1], 2),
        round(end[1], 2),
        round((start[1] + end[1]) / 2, 2),
        round(canvas_top + inset, 2),
        round(canvas_bottom - inset, 2),
        *[round(value, 2) for value in hint_y],
    }
    for left, top, right, bottom in obstacles:
        x_values.update((round(left, 2), round(right, 2)))
        y_values.update((round(top, 2), round(bottom, 2)))
    for route in existing_routes:
        for x, y in route:
            for delta in (-10.0, 0.0, 10.0):
                x_values.add(round(x + delta, 2))
                y_values.add(round(y + delta, 2))

    x_values = {value for value in x_values if canvas_left - 1e-6 <= value <= canvas_right + 1e-6}
    y_values = {value for value in y_values if canvas_top - 1e-6 <= value <= canvas_bottom + 1e-6}
    points = {
        (x, y)
        for x in sorted(x_values)
        for y in sorted(y_values)
        if not any(geometry.point_in_bounds((x, y), bounds, interior=True) for bounds in obstacles)
    }
    points.update((start, end))

    adjacency: Dict[Point, List[Point]] = {point: [] for point in points}
    by_y: Dict[float, List[Point]] = {}
    by_x: Dict[float, List[Point]] = {}
    for point in points:
        by_y.setdefault(point[1], []).append(point)
        by_x.setdefault(point[0], []).append(point)

    def connect(line: Sequence[Point], sort_key: int) -> None:
        ordered = sorted(line, key=lambda point: (point[sort_key], point[1 - sort_key]))
        for first, second in zip(ordered, ordered[1:]):
            if any(segment_hits_bounds(first, second, obstacle) for obstacle in obstacles):
                continue
            adjacency[first].append(second)
            adjacency[second].append(first)

    for line in by_y.values():
        connect(line, 0)
    for line in by_x.values():
        connect(line, 1)

    # State includes the incoming axis so bends have an explicit cost.
    start_state = (start, "")
    distances: Dict[Tuple[Point, str], float] = {start_state: 0.0}
    paths: Dict[Tuple[Point, str], Tuple[Point, ...]] = {start_state: (start,)}
    queue: List[Tuple[float, Tuple[Point, ...], Point, str]] = [(0.0, (start,), start, "")]
    best_end: Optional[Tuple[float, Tuple[Point, ...]]] = None

    while queue:
        cost, path_key, point, incoming = heapq.heappop(queue)
        state = (point, incoming)
        if cost > distances.get(state, float("inf")) + 1e-6 or path_key != paths.get(state):
            continue
        if point == end:
            best_end = (cost, path_key)
            break
        for neighbor in sorted(adjacency.get(point, [])):
            axis = segment_axis(point, neighbor)
            if axis == "other":
                continue
            distance = abs(neighbor[0] - point[0]) + abs(neighbor[1] - point[1])
            segment_route = [point, neighbor]
            interactions = geometry.route_interactions(segment_route, existing_routes)
            extra = distance
            if incoming and incoming != axis:
                extra += 22.0
            extra += len(interactions.crossings) * 640.0
            extra += interactions.overlap_count * 10000.0 + interactions.overlap_length * 30.0
            if axis == "vertical" and any(abs(point[0] - value) <= 1 for value in hint_x):
                extra -= min(18.0, distance * 0.08)
            if axis == "horizontal" and any(abs(point[1] - value) <= 1 for value in hint_y):
                extra -= min(18.0, distance * 0.08)
            next_state = (neighbor, axis)
            next_cost = cost + extra
            next_path = (*path_key, neighbor)
            old_cost = distances.get(next_state, float("inf"))
            old_path = paths.get(next_state)
            if next_cost < old_cost - 1e-6 or (
                abs(next_cost - old_cost) <= 1e-6 and (old_path is None or next_path < old_path)
            ):
                distances[next_state] = next_cost
                paths[next_state] = next_path
                heapq.heappush(queue, (next_cost, next_path, neighbor, axis))

    if best_end is None:
        return None
    return simplify_points(list(best_end[1]))


def build_orthogonal_route(
    start: Point,
    end: Point,
    obstacles: Sequence[Bounds],
    arrow_data: Dict[str, object],
    *,
    canvas_bounds: Optional[Bounds] = None,
    existing_routes: Sequence[Sequence[Point]] = (),
) -> List[Point]:
    raw_waypoints = arrow_data.get("route_points") or []
    if raw_waypoints:
        waypoints: List[Point] = []
        for index, raw_point in enumerate(raw_waypoints):
            if not isinstance(raw_point, (list, tuple)) or len(raw_point) != 2:
                raise ValueError(f"route waypoint {index + 1} must be [x, y]")
            waypoint = (to_float(raw_point[0]), to_float(raw_point[1]))
            if any(geometry.point_in_bounds(waypoint, bounds, interior=True) for bounds in obstacles):
                raise ValueError(f"route waypoint {index + 1} intersects an obstacle: {waypoint}")
            if canvas_bounds is not None and not geometry.point_in_bounds(waypoint, canvas_bounds):
                raise ValueError(f"route waypoint {index + 1} is outside the canvas: {waypoint}")
            waypoints.append(waypoint)

        mandatory = [start, *waypoints, end]
        assembled: List[Point] = []
        for index, (segment_start, segment_end) in enumerate(zip(mandatory, mandatory[1:])):
            segment_data = dict(arrow_data)
            segment_data.pop("route_points", None)
            if index > 0:
                segment_data.pop("source_port", None)
            if index < len(mandatory) - 2:
                segment_data.pop("target_port", None)
            occupied_routes: List[Sequence[Point]] = list(existing_routes)
            if len(assembled) >= 2:
                occupied_routes.append(assembled)
            segment_route = build_orthogonal_route(
                segment_start,
                segment_end,
                obstacles,
                segment_data,
                canvas_bounds=canvas_bounds,
                existing_routes=occupied_routes,
            )
            if assembled:
                assembled.extend(segment_route[1:])
            else:
                assembled.extend(segment_route)
        result = simplify_points(assembled, waypoints)
        if not route_is_orthogonal(result):
            raise ValueError("explicit route waypoints could not be connected orthogonally")
        if any(waypoint not in result for waypoint in waypoints):
            raise ValueError("explicit route waypoint was not preserved")
        if route_collides(result, obstacles):
            raise ValueError("explicit route waypoints cannot be connected without crossing an obstacle")
        return result

    sx, sy = start
    ex, ey = end
    routing_padding = to_float(arrow_data.get("routing_padding", 24))
    port_clearance = to_float(arrow_data.get("port_clearance", max(18, routing_padding * 0.85)))
    source_port = str(arrow_data.get("source_port", "")).strip().lower() or None
    target_port = str(arrow_data.get("target_port", "")).strip().lower() or None
    inner_start = clear_port_point(start, source_port, port_clearance, obstacles, canvas_bounds)
    inner_end = clear_port_point(end, target_port, port_clearance, obstacles, canvas_bounds)
    ssx, ssy = inner_start
    eex, eey = inner_end
    expanded: List[Bounds] = []
    for bounds in obstacles:
        padded = expand_bounds(bounds, routing_padding)
        # Mandatory waypoints are exact. If one sits inside a clearance halo,
        # keep the real obstacle hard while locally relaxing only that halo.
        if any(
            geometry.point_in_bounds(point, padded, interior=True)
            and not geometry.point_in_bounds(point, bounds, interior=True)
            for point in (start, end, inner_start, inner_end)
        ):
            expanded.append(bounds)
        else:
            expanded.append(padded)
    hint_x = [to_float(value) for value in arrow_data.get("corridor_x", [])]
    hint_y = [to_float(value) for value in arrow_data.get("corridor_y", [])]
    lane_x = sorted({ssx, eex, round((ssx + eex) / 2, 2), *hint_x, *[b[0] for b in expanded], *[b[2] for b in expanded]})
    lane_y = sorted({ssy, eey, round((ssy + eey) / 2, 2), *hint_y, *[b[1] for b in expanded], *[b[3] for b in expanded]})
    if expanded:
        left_rail = min(b[0] for b in expanded) - 24
        right_rail = max(b[2] for b in expanded) + 24
        top_rail = min(b[1] for b in expanded) - 24
        bottom_rail = max(b[3] for b in expanded) + 24
    else:
        left_rail = min(ssx, eex) - 48
        right_rail = max(ssx, eex) + 48
        top_rail = min(ssy, eey) - 48
        bottom_rail = max(ssy, eey) + 48

    candidates = [
        [start, inner_start, inner_end, end],
        [start, inner_start, (eex, ssy), inner_end, end],
        [start, inner_start, (ssx, eey), inner_end, end],
        [start, inner_start, ((ssx + eex) / 2, ssy), ((ssx + eex) / 2, eey), inner_end, end],
        [start, inner_start, (ssx, (ssy + eey) / 2), (eex, (ssy + eey) / 2), inner_end, end],
        [start, inner_start, (left_rail, ssy), (left_rail, eey), inner_end, end],
        [start, inner_start, (right_rail, ssy), (right_rail, eey), inner_end, end],
        [start, inner_start, (ssx, top_rail), (eex, top_rail), inner_end, end],
        [start, inner_start, (ssx, bottom_rail), (eex, bottom_rail), inner_end, end],
    ]
    for x in lane_x:
        candidates.append([start, inner_start, (x, ssy), (x, eey), inner_end, end])
    for y in lane_y:
        candidates.append([start, inner_start, (ssx, y), (eex, y), inner_end, end])
    for x in hint_x:
        for y in hint_y:
            candidates.append([start, inner_start, (x, ssy), (x, y), (eex, y), inner_end, end])

    visibility = visibility_grid_route(
        inner_start,
        inner_end,
        expanded,
        canvas_bounds=canvas_bounds,
        hint_x=hint_x,
        hint_y=hint_y,
        existing_routes=existing_routes,
    )
    if visibility:
        candidates.append([start, *visibility, end])

    best_route: Optional[List[Point]] = None
    best_score = float("inf")
    for candidate in candidates:
        simplified = simplify_points(candidate)
        if not route_is_orthogonal(simplified):
            continue
        if canvas_bounds is not None and not geometry.route_inside_canvas(simplified, canvas_bounds):
            continue
        coll = collision_count(simplified, expanded)
        score = route_score(simplified, hint_x, hint_y, source_port, target_port, existing_routes)
        if coll == 0:
            if score < best_score or (abs(score - best_score) < 1e-6 and (best_route is None or tuple(simplified) < tuple(best_route))):
                best_score = score
                best_route = simplified

    if best_route is not None:
        return best_route
    raise ValueError("no collision-free orthogonal route satisfies the current constraints")


def choose_label_position(points: Sequence[Point]) -> Point:
    segments = list(zip(points, points[1:]))
    if not segments:
        return points[0]
    best = max(segments, key=lambda seg: abs(seg[0][0] - seg[1][0]) + abs(seg[0][1] - seg[1][1]))
    return ((best[0][0] + best[1][0]) / 2, (best[0][1] + best[1][1]) / 2)


def color_for_flow(style: Dict[str, object], arrow_data: Dict[str, object]) -> str:
    if arrow_data.get("color"):
        return str(arrow_data["color"])
    flow = FLOW_ALIASES.get(str(arrow_data.get("flow", "control")).lower(), "control")
    return str(style_value(style, "arrow_colors")[flow])


def marker_for_color(style: Dict[str, object], color: str, arrow_data: Dict[str, object]) -> str:
    if arrow_data.get("marker"):
        return f"url(#{arrow_data['marker']})"
    colors = style_value(style, "arrow_colors")
    for name, token in colors.items():
        if token == color:
            return f"url(#{MARKER_IDS.get(name, 'arrowA')})"
    return "url(#arrowA)"


def render_label_badge(x: float, y: float, text: str, style: Dict[str, object], label_style: str = "offset") -> str:
    width = max(36.0, geometry.estimate_text_width(text, 12) + 14)
    parts: List[str] = []
    if label_style == "badge":
        bg = style_value(style, "arrow_label_bg")
        opacity = style_value(style, "arrow_label_opacity")
        parts.append(f'  <rect x="{round(x - width / 2, 2)}" y="{round(y - 10, 2)}" width="{width}" height="20" rx="6" fill="{bg}" opacity="{opacity}"/>')
    parts.append(f'  <text x="{round(x, 2)}" y="{round(y + 4, 2)}" text-anchor="middle" class="arrow-label">{normalize_text(text)}</text>')
    return "\n".join(parts)


def rectangle_bounds(x: float, y: float, width: float, height: float) -> Bounds:
    return (x, y, x + width, y + height)


def bounds_intersect(a: Bounds, b: Bounds, padding: float = 0.0) -> bool:
    ax1, ay1, ax2, ay2 = a
    bx1, by1, bx2, by2 = b
    return not (
        ax2 + padding <= bx1
        or bx2 + padding <= ax1
        or ay2 + padding <= by1
        or by2 + padding <= ay1
    )


def estimate_label_bounds(x: float, y: float, text: str) -> Bounds:
    width = max(36.0, geometry.estimate_text_width(text, 12) + 14)
    return rectangle_bounds(x - width / 2, y - 10, width, 20)


def render_dual_label_badge(x: float, y: float, primary: str, secondary: str, style: Dict[str, object]) -> str:
    width = max(
        42.0,
        geometry.estimate_text_width(primary, 11.5) + 16,
        geometry.estimate_text_width(secondary, 8.5) + 16,
    )
    bg = style_value(style, "arrow_label_bg")
    opacity = style_value(style, "arrow_label_opacity")
    return "\n".join(
        [
            f'  <rect x="{round(x - width / 2, 2)}" y="{round(y - 16, 2)}" width="{round(width, 2)}" height="32" rx="7" fill="{bg}" opacity="{opacity}"/>',
            f'  <text x="{round(x, 2)}" y="{round(y - 1, 2)}" text-anchor="middle" class="arrow-label" font-size="11.5">{normalize_text(primary)}</text>',
            f'  <text x="{round(x, 2)}" y="{round(y + 11, 2)}" text-anchor="middle" font-size="8.5" font-weight="800" fill="{style_value(style, "type_label_fill")}">[{normalize_text(secondary)}]</text>',
        ]
    )


def estimate_dual_label_bounds(x: float, y: float, primary: str, secondary: str) -> Bounds:
    width = max(
        42.0,
        geometry.estimate_text_width(primary, 11.5) + 16,
        geometry.estimate_text_width(secondary, 8.5) + 16,
    )
    return rectangle_bounds(x - width / 2, y - 16, width, 32)


def section_header_text(container: Dict[str, object], style: Dict[str, object]) -> str:
    if container.get("header_text"):
        text = str(container.get("header_text", ""))
    else:
        label = str(container.get("label", ""))
        prefix = str(container.get("header_prefix", "")).strip()
        separator = str(container.get("header_separator", " // " if prefix else ""))
        text = f"{prefix}{separator}{label}" if prefix else label
    if style_value(style, "section_upper") and not container.get("preserve_case"):
        text = text.upper()
    return text


def deterministic_jitter(seed: object, element_id: object, pass_index: int, amplitude: float = 1.5) -> float:
    """Return stable decorative jitter without relying on process-randomized hash()."""

    digest = hashlib.sha256(f"{seed}:{element_id}:{pass_index}".encode("utf-8")).digest()
    unit = int.from_bytes(digest[:4], "big") / float(2**32 - 1)
    return round((unit * 2.0 - 1.0) * amplitude, 3)


def render_section(container: Dict[str, object], style: Dict[str, object]) -> str:
    x = to_float(container["x"])
    y = to_float(container["y"])
    width = to_float(container["width"])
    height = to_float(container["height"])
    rx = to_float(container.get("rx", 16 if style_value(style, "name") != "Notion Clean" else 4))
    fill = str(container.get("fill", style_value(style, "section_fill")))
    stroke = str(container.get("stroke", style_value(style, "section_stroke")))
    dash = str(container.get("stroke_dasharray", style_value(style, "section_dash")))
    label = section_header_text(container, style)
    subtitle = str(container.get("subtitle", ""))
    side_label = str(container.get("side_label", "")).strip()
    side_label_fill = str(container.get("side_label_fill", style_value(style, "text_secondary")))
    side_label_size = to_float(container.get("side_label_size", 14))
    side_label_weight = str(container.get("side_label_weight", "600"))
    side_label_anchor = str(container.get("side_label_anchor", "end"))
    lines = [f'  <rect data-graph-role="container" x="{x}" y="{y}" width="{width}" height="{height}" rx="{rx}" fill="{fill}" stroke="{stroke}" stroke-width="1.4"']
    if dash:
        lines[-1] += f' stroke-dasharray="{dash}"'
    lines[-1] += "/>"
    treatment = str(style.get("canvas_treatment", ""))
    if treatment == "review":
        seed = container.get("_rough_seed", 0)
        element_id = container.get("id", label or "container")
        dx = deterministic_jitter(seed, element_id, 1)
        dy = deterministic_jitter(seed, element_id, 2)
        lines.append(
            f'  <rect data-graph-role="decoration" x="{x + 2 + dx}" y="{y + 2 + dy}" '
            f'width="{width - 4}" height="{height - 4}" rx="{max(3, rx - 1)}" fill="none" '
            f'stroke="{stroke}" stroke-width="0.9" stroke-dasharray="13 7" opacity="0.42"/>'
        )
    elif treatment == "cloud" and container.get("deployment_kind"):
        deployment_kind = str(container.get("deployment_kind", ""))
        badge = normalize_text(deployment_kind.upper())
        badge_width = max(54.0, geometry.estimate_text_width(str(container.get("deployment_kind", "")), 9) + 18)
        spine_color = {"global": "#2563eb", "region": "#0891b2", "network": "#7c3aed"}.get(deployment_kind, "#7fa3c2")
        lines.append(
            f'  <rect data-graph-role="decoration" x="{x + width - badge_width - 14}" y="{y + 10}" '
            f'width="{badge_width}" height="18" rx="9" fill="#dbeafe" stroke="#93c5fd" stroke-width="0.8"/>'
        )
        lines.append(
            f'  <text data-graph-role="decoration" x="{x + width - badge_width / 2 - 14}" y="{y + 22.5}" '
            f'text-anchor="middle" font-size="9" font-weight="700" fill="#315d7e">{badge}</text>'
        )
        lines.append(
            f'  <line data-graph-role="decoration" x1="{x + 8}" y1="{y + 42}" x2="{x + 8}" y2="{y + height - 14}" '
            f'stroke="{spine_color}" stroke-width="2.2" stroke-linecap="round" opacity="0.72"/>'
        )
    elif treatment == "transit":
        lines.append(
            f'  <line data-graph-role="decoration" x1="{x + 18}" y1="{y + 34}" x2="{x + width - 18}" y2="{y + 34}" '
            f'stroke="{stroke}" stroke-width="1" stroke-dasharray="2 7" opacity="0.35"/>'
        )
    elif treatment == "ops":
        lines.append(
            f'  <line data-graph-role="decoration" x1="{x + 14}" y1="{y + 34}" x2="{x + width - 14}" y2="{y + 34}" '
            f'stroke="#38bdf8" stroke-width="1" opacity="0.16"/>'
        )
        if "trace" in str(container.get("id", "")).lower():
            ruler_left = x + width - 244
            ruler_right = x + width - 24
            lines.append(
                f'  <line data-graph-role="decoration" x1="{ruler_left}" y1="{y + 22}" x2="{ruler_right}" y2="{y + 22}" '
                f'stroke="#38bdf8" stroke-width="1" opacity="0.42"/>'
            )
            for index in range(5):
                tick_x = ruler_left + (ruler_right - ruler_left) * index / 4
                lines.append(
                    f'  <line data-graph-role="decoration" x1="{tick_x}" y1="{y + 18}" x2="{tick_x}" y2="{y + 26}" '
                    f'stroke="#38bdf8" stroke-width="1" opacity="0.52"/>'
                )
                lines.append(
                    f'  <text data-graph-role="decoration" x="{tick_x}" y="{y + 14}" text-anchor="middle" '
                    f'font-size="7" font-weight="700" fill="#6f8ba5">{index * 25}%</text>'
                )
    if label:
        lines.append(f'  <text x="{x + 18}" y="{y + 24}" class="section">{normalize_text(label)}</text>')
    if subtitle:
        lines.append(f'  <text x="{x + 18}" y="{y + 44}" class="section-sub">{normalize_text(subtitle)}</text>')
    if side_label:
        side_x = to_float(container.get("side_label_x", max(28, x - 18)))
        side_y = to_float(container.get("side_label_y", y + height / 2))
        lines.append(
            f'  <text x="{side_x}" y="{side_y}" text-anchor="{side_label_anchor}" dominant-baseline="middle" '
            f'font-size="{side_label_size}" font-weight="{side_label_weight}" fill="{side_label_fill}">{normalize_text(side_label)}</text>'
        )
    return "\n".join(lines)


def container_header_bounds(container: Dict[str, object], style: Optional[Dict[str, object]] = None) -> Optional[Bounds]:
    label = section_header_text(container, style) if style is not None else str(container.get("header_text", "") or container.get("label", ""))
    label = label.strip()
    subtitle = str(container.get("subtitle", "")).strip()
    if not label and not subtitle:
        return None
    x = to_float(container["x"])
    y = to_float(container["y"])
    width = to_float(container["width"])
    header_height = to_float(container.get("header_height", 54 if subtitle else 30))
    label_width = geometry.estimate_text_width(label, 13) if label else 0.0
    subtitle_width = geometry.estimate_text_width(subtitle, 12) if subtitle else 0.0
    reserved_width = min(width - 12, max(label_width, subtitle_width) + 30)
    return rectangle_bounds(x + 8, y + 6, reserved_width, header_height)


def label_position_candidates(points: Sequence[Point], text: str = "") -> List[Point]:
    segments = list(zip(points, points[1:]))
    if not segments:
        return [points[0]]
    ranked_segments = sorted(
        segments,
        key=lambda seg: abs(seg[0][0] - seg[1][0]) + abs(seg[0][1] - seg[1][1]),
        reverse=True,
    )
    candidates: List[Point] = []
    horizontal_offset = 17.0
    vertical_offset = max(22.0, geometry.estimate_text_width(text, 12) / 2 + 10)
    global_x = (min(point[0] for point in points) + max(point[0] for point in points)) / 2
    global_y = (min(point[1] for point in points) + max(point[1] for point in points)) / 2
    for (x1, y1), (x2, y2) in ranked_segments:
        length = abs(x1 - x2) + abs(y1 - y2)
        if length < 34:
            continue
        centers = [
            ((x1 + x2) / 2, (y1 + y2) / 2),
            (x1 * 0.7 + x2 * 0.3, y1 * 0.7 + y2 * 0.3),
            (x1 * 0.3 + x2 * 0.7, y1 * 0.3 + y2 * 0.7),
        ]
        for mx, my in centers:
            if abs(y1 - y2) < 1e-6:
                candidates.extend([(mx, my - horizontal_offset), (mx, my + horizontal_offset), (mx, my - 30), (mx, my + 30), (mx, my)])
                candidates.extend([(global_x, my - horizontal_offset), (global_x, my + horizontal_offset)])
            elif abs(x1 - x2) < 1e-6:
                candidates.extend([(mx - vertical_offset, my), (mx + vertical_offset, my), (mx - vertical_offset - 14, my), (mx + vertical_offset + 14, my), (mx, my)])
                candidates.extend([(mx - vertical_offset, global_y), (mx + vertical_offset, global_y)])
            else:
                candidates.extend([(mx, my - 16), (mx, my + 16), (mx, my)])
    return candidates or [choose_label_position(points)]


def route_clearance_bounds(points: Sequence[Point], padding: float = 3.0) -> List[Bounds]:
    result: List[Bounds] = []
    for first, second in zip(points, points[1:]):
        left = min(first[0], second[0]) - padding
        right = max(first[0], second[0]) + padding
        top = min(first[1], second[1]) - padding
        bottom = max(first[1], second[1]) + padding
        result.append((left, top, right, bottom))
    return result


def choose_label_position_avoiding(
    points: Sequence[Point],
    text: str,
    occupied: Sequence[Bounds],
    *,
    routes: Sequence[Sequence[Point]] = (),
    canvas_bounds: Optional[Bounds] = None,
    dx: float = 0.0,
    dy: float = -4.0,
) -> Point:
    route_bounds = [bounds for route in routes for bounds in route_clearance_bounds(route)]
    offset_options = [
        (dx, dy),
        (0.0, -4.0),
        (0.0, 0.0),
        (0.0, -14.0),
        (0.0, 14.0),
        (-18.0, -4.0),
        (18.0, -4.0),
        (-32.0, 0.0),
        (32.0, 0.0),
    ]
    for candidate in label_position_candidates(points, text):
        for offset_x, offset_y in offset_options:
            adjusted = (candidate[0] + offset_x, candidate[1] + offset_y)
            label_box = estimate_label_bounds(adjusted[0], adjusted[1], text)
            if canvas_bounds is not None and not geometry.bounds_inside(label_box, canvas_bounds, 4):
                continue
            if any(bounds_intersect(label_box, other, 4) for other in occupied):
                continue
            if any(bounds_intersect(label_box, other, 1) for other in route_bounds):
                continue
            return adjusted
    raise ValueError(f"no collision-free label position for {text!r}")


def legend_layout(data: Dict[str, object], legend: Sequence[Dict[str, object]], width: float, height: float) -> Optional[Tuple[float, float, Bounds]]:
    if not legend:
        return None
    orientation = str(data.get("legend_orientation", "vertical")).strip().lower()
    if orientation not in {"vertical", "horizontal"}:
        raise ValueError("legend_orientation must be vertical or horizontal")
    x = to_float(data.get("_legend_x", data.get("legend_x", 42)))
    default_y = height - 82 if orientation == "horizontal" else height - (len(legend) * 22 + 34)
    y = to_float(data.get("_legend_y", data.get("legend_y", default_y)))
    position = str(data.get("legend_position", "bottom-left"))
    label_widths = [geometry.estimate_text_width(str(item.get("label", "")), 12) for item in legend]
    if orientation == "horizontal":
        block_width = sum(40 + label_width + 28 for label_width in label_widths) - 18
        block_height = 28
    else:
        block_width = 40 + max(label_widths, default=84) + 12
        block_height = len(legend) * 22 + 6
    if "_legend_x" not in data and position == "bottom-right":
        x = to_float(data.get("legend_x", width - block_width - 42))
    elif "_legend_x" not in data and position == "top-right":
        x = to_float(data.get("legend_x", width - block_width - 42))
        y = to_float(data.get("legend_y", 96))
    elif "_legend_x" not in data and position == "top-left":
        x = to_float(data.get("legend_x", 42))
        y = to_float(data.get("legend_y", 96))
    return (x, y, rectangle_bounds(x - 10, y - 14, block_width + 20, block_height + 18))


def route_hint_points(arrow: Dict[str, object], node_map: Dict[str, Node]) -> List[Point]:
    source = node_map.get(str(arrow.get("source"))) if arrow.get("source") else None
    target = node_map.get(str(arrow.get("target"))) if arrow.get("target") else None
    start_hint = (to_float(arrow.get("x1")), to_float(arrow.get("y1")))
    end_hint = (to_float(arrow.get("x2")), to_float(arrow.get("y2")))
    if source is not None:
        toward = end_hint if target is None else (target.cx, target.cy)
        start = anchor_point(source, toward, str(arrow.get("source_port")) if arrow.get("source_port") else None)
    else:
        start = start_hint
    if target is not None:
        toward = start_hint if source is None else (source.cx, source.cy)
        end = anchor_point(target, toward, str(arrow.get("target_port")) if arrow.get("target_port") else None)
    else:
        end = end_hint
    waypoints = [(to_float(point[0]), to_float(point[1])) for point in (arrow.get("route_points") or [])]
    return [start, *waypoints, end]


def resolve_legend_layout(
    data: Dict[str, object],
    legend: Sequence[Dict[str, object]],
    width: float,
    height: float,
    obstacles: Sequence[Bounds],
    arrows: Sequence[Dict[str, object]],
    node_map: Dict[str, Node],
) -> Optional[Dict[str, object]]:
    requested = legend_layout(data, legend, width, height)
    if requested is None:
        return None
    requested_x, requested_y, requested_bounds = requested
    hint_routes = [route_hint_points(arrow, node_map) for arrow in arrows if arrow.get("route_points")]
    canvas = (0.0, 0.0, width, height)

    def is_safe(bounds: Bounds) -> bool:
        if not geometry.bounds_inside(bounds, canvas, 8):
            return False
        if any(bounds_intersect(bounds, obstacle, 6) for obstacle in obstacles):
            return False
        return not any(
            segment_hits_bounds(first, second, bounds)
            for route in hint_routes
            for first, second in zip(route, route[1:])
        )

    if is_safe(requested_bounds):
        return {
            "requested": [round(requested_x, 2), round(requested_y, 2)],
            "actual": [round(requested_x, 2), round(requested_y, 2)],
            "moved": False,
            "bounds": requested_bounds,
        }
    if data.get("legend_locked"):
        raise ValueError("locked legend intersects diagram content or a mandatory route")

    block_width = requested_bounds[2] - requested_bounds[0]
    block_height = requested_bounds[3] - requested_bounds[1]
    candidates: List[Tuple[float, float]] = []
    for top in range(84, max(85, int(height - block_height - 8)) + 1, 8):
        for left in range(8, max(9, int(width - block_width - 8)) + 1, 8):
            x = left + 10
            y = top + 14
            candidates.append((float(x), float(y)))
    candidates.sort(key=lambda point: (abs(point[0] - requested_x) + abs(point[1] - requested_y), point[1], point[0]))
    for x, y in candidates:
        bounds = (x - 10, y - 14, x - 10 + block_width, y - 14 + block_height)
        if is_safe(bounds):
            data["_legend_x"] = x
            data["_legend_y"] = y
            return {
                "requested": [round(requested_x, 2), round(requested_y, 2)],
                "actual": [round(x, 2), round(y, 2)],
                "moved": True,
                "bounds": bounds,
            }
    raise ValueError("no collision-free legend position is available")


def footer_layout(data: Dict[str, object], width: float, height: float) -> Optional[Tuple[float, float, Bounds]]:
    text = str(data.get("footer", "")).strip()
    if not text:
        return None
    footer_width = max(140, len(text) * 7)
    x = to_float(data.get("footer_x", 42))
    y = to_float(data.get("footer_y", height - 16))
    position = str(data.get("footer_position", "bottom-left"))
    if position == "bottom-right":
        x = to_float(data.get("footer_x", width - footer_width - 42))
    return (x, y, rectangle_bounds(x, y - 12, footer_width, 16))


def render_tags(node: Dict[str, object], x: float, y: float, style: Dict[str, object]) -> List[str]:
    tags = node.get("tags", [])
    if not tags:
        return []
    cursor_x = x
    lines = []
    for tag in tags:
        label = normalize_text(tag.get("label", ""))
        width = max(62, len(str(tag.get("label", ""))) * 8 + 18)
        fill = tag.get("fill", "#eff6ff")
        stroke = tag.get("stroke", "#bfdbfe")
        text_fill = tag.get("text_fill", style_value(style, "arrow_colors")["read"])
        lines.append(
            f'  <rect x="{cursor_x}" y="{y}" width="{width}" height="16" rx="3" fill="{fill}" stroke="{stroke}" stroke-width="1"/>'
        )
        lines.append(
            f'  <text x="{cursor_x + width / 2}" y="{y + 11.5}" text-anchor="middle" font-size="11" font-weight="500" fill="{text_fill}">{label}</text>'
        )
        cursor_x += width + 8
    return lines


def fitted_text_size(text: str, available_width: float, *, preferred: float = 18.0, minimum: float = 12.0) -> float:
    """Keep a single-line node title inside its card without manual tuning."""

    estimated = geometry.estimate_text_width(text, preferred, weight=1.08)
    if estimated <= max(1.0, available_width):
        return preferred
    scaled = max(minimum, preferred * available_width / max(estimated, 1.0))
    return math.floor(scaled * 100) / 100


def fit_single_line_text(
    text: object,
    available_width: float,
    *,
    preferred: float = 18.0,
    minimum: float = 12.0,
) -> Tuple[str, float]:
    """Fit one line, then fail visually closed with an ellipsis at the minimum size."""

    value = " ".join(str(text or "").split())
    size = fitted_text_size(value, available_width, preferred=preferred, minimum=minimum)
    if geometry.estimate_text_width(value, size, weight=1.08) <= available_width:
        return value, size
    candidate = value
    while candidate and geometry.estimate_text_width(candidate + "…", size, weight=1.08) > available_width:
        candidate = candidate[:-1].rstrip()
    return (candidate + "…" if candidate else "…"), size


def wrap_text_lines(text: object, available_width: float, *, font_size: float = 11.5, max_lines: int = 2) -> List[str]:
    """Balance short card copy across lines and fail visually closed with an ellipsis."""

    value = " ".join(str(text or "").split())
    if not value:
        return []
    if geometry.estimate_text_width(value, font_size) <= available_width or max_lines <= 1:
        return [value]
    words = value.split()
    if max_lines == 2 and len(words) > 1:
        candidates: List[Tuple[Tuple[float, float], List[str]]] = []
        for index in range(1, len(words)):
            candidate = [" ".join(words[:index]), " ".join(words[index:])]
            widths = [geometry.estimate_text_width(line, font_size) for line in candidate]
            overflow = sum(max(0.0, width - available_width) for width in widths)
            candidates.append(((overflow, abs(widths[0] - widths[1])), candidate))
        lines = min(candidates, key=lambda item: item[0])[1]
    else:
        lines = []
        current = ""
        for word in words:
            candidate = f"{current} {word}".strip()
            if current and geometry.estimate_text_width(candidate, font_size) > available_width:
                lines.append(current)
                current = word
            else:
                current = candidate
        if current:
            lines.append(current)
        if len(lines) > max_lines:
            lines = lines[: max_lines - 1] + [" ".join(lines[max_lines - 1 :])]

    fitted: List[str] = []
    for line in lines[:max_lines]:
        candidate = line
        while len(candidate) > 1 and geometry.estimate_text_width(candidate, font_size) > available_width:
            candidate = candidate[:-1].rstrip()
        if candidate != line:
            candidate = candidate.rstrip(" …") + "…"
            while len(candidate) > 1 and geometry.estimate_text_width(candidate, font_size) > available_width:
                candidate = candidate[:-2].rstrip() + "…"
        fitted.append(candidate)
    return fitted


def render_review_node(node: Dict[str, object], style: Dict[str, object]) -> str:
    x = to_float(node["x"])
    y = to_float(node["y"])
    width = to_float(node.get("width", 180))
    height = to_float(node.get("height", 84))
    fill = str(node.get("fill", style_value(style, "node_fill")))
    stroke = str(node.get("stroke", style_value(style, "node_stroke")))
    seed = node.get("_rough_seed", 0)
    element_id = node.get("id", "review-card")
    dx = deterministic_jitter(seed, element_id, 1)
    dy = deterministic_jitter(seed, element_id, 2)
    raw_c4_type = str(node.get("c4_type", node.get("type_label", "element")))
    c4_type_raw = raw_c4_type.upper().replace("_", " ")
    c4_type_text, c4_type_size = fit_single_line_text(
        c4_type_raw, width - 28, preferred=to_float(style_value(style, "type_label_size"), 10), minimum=7.5
    )
    c4_type = normalize_text(c4_type_text)
    external_dash = ' stroke-dasharray="6 4"' if raw_c4_type == "external_system" else ""
    title_raw = str(node.get("label", ""))
    title_text, title_size = fit_single_line_text(title_raw, width - 28, preferred=15, minimum=10.5)
    title = normalize_text(title_text)
    description_raw = str(node.get("description", node.get("sublabel", "")))
    technology_raw = str(node.get("technology", ""))
    technology_text, technology_size = fit_single_line_text(
        technology_raw, width - 24, preferred=9.5, minimum=7.5
    )
    technology = normalize_text(technology_text)
    description_size = 11.5
    description_lines = wrap_text_lines(description_raw, width - 28, font_size=description_size, max_lines=2)
    lines = [
        f'  <rect x="{x}" y="{y}" width="{width}" height="{height}" rx="7" fill="{fill}" stroke="{stroke}" '
        f'stroke-width="1.8"{external_dash}/>',
        f'  <path data-graph-role="decoration" d="M {x + 7 + dx} {y + 2 + dy} H {x + width - 7 + dx} '
        f'Q {x + width - 2 + dx} {y + 2 + dy} {x + width - 2 + dx} {y + 8 + dy} '
        f'V {y + height - 7 + dy}" fill="none" stroke="{stroke}" stroke-width="0.8" opacity="0.42"/>',
        f'  <text data-text-role="type" data-full-text="{normalize_attribute(c4_type_raw)}" '
        f'data-text-max-width="{width - 28}" x="{x + 14}" y="{y + 18}" class="node-type" '
        f'font-size="{c4_type_size}">{c4_type}</text>',
        f'  <text data-text-role="title" data-full-text="{normalize_attribute(title_raw)}" '
        f'data-text-max-width="{width - 28}" x="{x + 14}" y="{y + 41}" class="node-title" '
        f'font-size="{title_size}" text-anchor="start">{title}</text>',
    ]
    if description_lines:
        description_y = y + (56 if len(description_lines) > 1 else 60)
        for index, description_line in enumerate(description_lines):
            lines.append(
                f'  <text data-text-role="description" data-line="{index + 1}" '
                f'data-full-text="{normalize_attribute(description_raw)}" x="{x + 14}" '
                f'y="{description_y + index * 14}" class="node-sub" font-size="{description_size}" '
                f'text-anchor="start">{normalize_text(description_line)}</text>'
            )
    if technology:
        lines.append(
            f'  <text data-text-role="technology" data-full-text="{normalize_attribute(technology_raw)}" '
            f'data-text-max-width="{width - 24}" x="{x + width - 12}" y="{y + height - 9}" '
            f'text-anchor="end" font-size="{technology_size}" font-weight="700" '
            f'fill="{style_value(style, "type_label_fill")}">{technology}</text>'
        )
    return "\n".join(lines)


def render_cloud_glyph(glyph: str, cx: float, cy: float, color: str) -> List[str]:
    """Render manifest-backed neutral geometry without bundling vendor logos."""

    common = f'fill="none" stroke="{color}" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"'
    if glyph == "globe":
        return [
            f'  <circle cx="{cx}" cy="{cy}" r="11" {common}/>',
            f'  <path d="M {cx - 11} {cy} H {cx + 11} M {cx} {cy - 11} C {cx - 6} {cy - 5} {cx - 6} {cy + 5} {cx} {cy + 11} M {cx} {cy - 11} C {cx + 6} {cy - 5} {cx + 6} {cy + 5} {cx} {cy + 11}" {common}/>',
        ]
    if glyph == "database":
        return [
            f'  <ellipse cx="{cx}" cy="{cy - 8}" rx="11" ry="4" {common}/>',
            f'  <path d="M {cx - 11} {cy - 8} V {cy + 8} C {cx - 11} {cy + 13} {cx + 11} {cy + 13} {cx + 11} {cy + 8} V {cy - 8}" {common}/>',
            f'  <path d="M {cx - 11} {cy} C {cx - 11} {cy + 5} {cx + 11} {cy + 5} {cx + 11} {cy}" {common}/>',
        ]
    if glyph == "gateway":
        return [
            f'  <path d="M {cx} {cy - 12} L {cx + 12} {cy} L {cx} {cy + 12} L {cx - 12} {cy} Z" {common}/>',
            f'  <path d="M {cx - 6} {cy} H {cx + 6} M {cx + 3} {cy - 3} L {cx + 6} {cy} L {cx + 3} {cy + 3}" {common}/>',
        ]
    if glyph == "stream":
        return [
            f'  <path d="M {cx - 12} {cy - 7} H {cx + 5} M {cx - 5} {cy} H {cx + 12} M {cx - 12} {cy + 7} H {cx + 5}" {common}/>',
            f'  <circle cx="{cx + 8}" cy="{cy - 7}" r="2.4" fill="{color}"/><circle cx="{cx - 8}" cy="{cy}" r="2.4" fill="{color}"/><circle cx="{cx + 8}" cy="{cy + 7}" r="2.4" fill="{color}"/>',
        ]
    if glyph == "observe":
        return [
            f'  <path d="M {cx - 13} {cy} Q {cx} {cy - 12} {cx + 13} {cy} Q {cx} {cy + 12} {cx - 13} {cy} Z" {common}/>',
            f'  <path d="M {cx - 6} {cy} H {cx - 2} L {cx + 1} {cy - 5} L {cx + 4} {cy + 5} L {cx + 7} {cy}" {common}/>',
        ]
    return [
        f'  <rect x="{cx - 11}" y="{cy - 9}" width="22" height="18" rx="4" {common}/>',
        f'  <path d="M {cx - 6} {cy - 3} H {cx + 6} M {cx - 6} {cy + 3} H {cx + 3}" {common}/>',
    ]


def render_cloud_node(node: Dict[str, object], style: Dict[str, object]) -> str:
    x = to_float(node["x"])
    y = to_float(node["y"])
    width = to_float(node.get("width", 160))
    height = to_float(node.get("height", 72))
    fill = str(node.get("fill", style_value(style, "node_fill")))
    stroke = str(node.get("stroke", style_value(style, "node_stroke")))
    icon_color = str(node.get("icon_color", "#2563eb"))
    glyph = str(node.get("glyph", "compute"))
    provider = normalize_text(str(node.get("provider", node.get("platform", "cloud"))).upper())
    title_raw = str(node.get("label", ""))
    subtitle_raw = str(node.get("sublabel", node.get("service", "")))
    available_text_width = max(28.0, width - 78)
    title_text, title_size = fit_single_line_text(title_raw, available_text_width, preferred=13.5, minimum=10.5)
    subtitle_text, subtitle_size = fit_single_line_text(subtitle_raw, available_text_width, preferred=11.5, minimum=9.5)
    title = normalize_text(title_text)
    subtitle = normalize_text(subtitle_text)
    icon_box_size = min(42.0, max(28.0, height - 16.0))
    icon_box_x = x + 12
    icon_box_y = y + (height - icon_box_size) / 2
    icon_center_x = icon_box_x + icon_box_size / 2
    icon_center_y = y + height / 2
    lines = [
        f'  <rect x="{x}" y="{y}" width="{width}" height="{height}" rx="12" fill="{fill}" stroke="{stroke}" stroke-width="1.4" filter="url(#shadowSoft)"/>',
        f'  <rect x="{icon_box_x}" y="{icon_box_y}" width="{icon_box_size}" height="{icon_box_size}" rx="11" fill="{icon_color}" opacity="0.12" stroke="{icon_color}" stroke-width="1.2"/>',
        f'  <text data-text-role="title" data-full-text="{normalize_attribute(title_raw)}" x="{x + 66}" y="{y + 31}" text-anchor="start" class="node-title" font-size="{title_size}">{title}</text>',
        f'  <text data-text-role="subtitle" data-full-text="{normalize_attribute(subtitle_raw)}" x="{x + 66}" y="{y + 50}" text-anchor="start" class="node-sub" font-size="{subtitle_size}">{subtitle}</text>',
        f'  <text x="{x + width - 10}" y="{y + 14}" text-anchor="end" font-size="8.5" font-weight="800" fill="{style_value(style, "type_label_fill")}">{provider}</text>',
    ]
    lines[2:2] = render_cloud_glyph(glyph, icon_center_x, icon_center_y, icon_color)
    return "\n".join(lines)


def render_transit_node(node: Dict[str, object], style: Dict[str, object]) -> str:
    x = to_float(node["x"])
    y = to_float(node["y"])
    width = to_float(node.get("width", 140))
    height = to_float(node.get("height", 72))
    role = str(node.get("transit_role", "station"))
    color = str(node.get("rail_color", node.get("stroke", style_value(style, "arrow_colors")["control"])))
    if role == "dlq":
        color = str(node.get("stroke", style_value(style, "arrow_colors")["feedback"]))
    fill = str(node.get("fill", style_value(style, "node_fill")))
    title_raw = str(node.get("label", ""))
    subtitle_raw = str(node.get("sublabel", node.get("operation", "")))
    text_width = max(36.0, width - 50)
    title_text, title_size = fit_single_line_text(title_raw, text_width, preferred=13.5, minimum=10.5)
    subtitle_text, subtitle_size = fit_single_line_text(subtitle_raw, text_width, preferred=11.5, minimum=9.2)
    title = normalize_text(title_text)
    subtitle = normalize_text(subtitle_text)
    badge = normalize_text(node.get("badge", node.get("partition_badge", "")))
    marker_x = x + 18
    dash = ' stroke-dasharray="5 3"' if role == "dlq" else ""
    lines = [
        f'  <rect x="{x}" y="{y}" width="{width}" height="{height}" rx="{height / 2}" fill="{fill}" stroke="{color}" stroke-width="1.6"{dash}/>',
        f'  <circle cx="{marker_x}" cy="{y + height / 2}" r="8" fill="{style_value(style, "background")}" stroke="{color}" stroke-width="2.4"/>',
        f'  <text data-text-role="title" data-full-text="{normalize_attribute(title_raw)}" x="{x + 36}" y="{y + 38}" text-anchor="start" class="node-title" font-size="{title_size}">{title}</text>',
        f'  <text data-text-role="subtitle" data-full-text="{normalize_attribute(subtitle_raw)}" x="{x + 36}" y="{y + 58}" text-anchor="start" class="node-sub" font-size="{subtitle_size}">{subtitle}</text>',
    ]
    if role == "junction":
        lines.append(f'  <circle cx="{marker_x}" cy="{y + height / 2}" r="3" fill="{color}"/>')
    elif role == "dlq":
        lines.append(
            f'  <path data-graph-role="decoration" d="M {marker_x - 3} {y + height / 2 - 3} '
            f'L {marker_x + 3} {y + height / 2 + 3} M {marker_x + 3} {y + height / 2 - 3} '
            f'L {marker_x - 3} {y + height / 2 + 3}" stroke="{color}" stroke-width="1.7" stroke-linecap="round"/>'
        )
    elif role == "state_store":
        lines.append(
            f'  <rect data-graph-role="decoration" x="{marker_x - 3.5}" y="{y + height / 2 - 3.5}" '
            f'width="7" height="7" rx="1.2" fill="none" stroke="{color}" stroke-width="1.5"/>'
        )
    elif isinstance(node.get("station_order"), int):
        station_number = int(node["station_order"]) + 1
        lines.append(
            f'  <text data-graph-role="decoration" x="{marker_x}" y="{y + height / 2 + 2.5}" '
            f'text-anchor="middle" font-size="7" font-weight="800" fill="{color}">{station_number:02d}</text>'
        )
    if badge:
        badge_width = max(34.0, geometry.estimate_text_width(str(node.get("badge", node.get("partition_badge", ""))), 9) + 12)
        lines.extend(
            [
                f'  <rect x="{x + width - badge_width - 10}" y="{y + 6}" width="{badge_width}" height="15" rx="7.5" fill="{color}" opacity="0.13"/>',
                f'  <text x="{x + width - badge_width / 2 - 10}" y="{y + 16.5}" text-anchor="middle" font-size="8.2" font-weight="800" fill="{color}">{badge}</text>',
            ]
        )
    return "\n".join(lines)


def render_ops_service(node: Dict[str, object], style: Dict[str, object]) -> str:
    x = to_float(node["x"])
    y = to_float(node["y"])
    width = to_float(node.get("width", 180))
    height = to_float(node.get("height", 108))
    status = str(node.get("status", "ok"))
    status_colors = {"ok": "#22c55e", "warn": "#f59e0b", "critical": "#f43f5e", "unknown": "#64748b"}
    status_color = status_colors.get(status, status_colors["unknown"])
    title_raw = str(node.get("label", ""))
    status_raw = str(node.get("status_label", status.upper()))
    status_preferred = 8.5
    status_needed = geometry.estimate_text_width(status_raw, status_preferred, weight=1.08) + 4
    status_budget = min(width * 0.38, max(36.0, status_needed))
    title_budget = max(28.0, width - 46.0 - status_budget)
    title_text, title_size = fit_single_line_text(title_raw, title_budget, preferred=13.5, minimum=9.5)
    status_text, status_size = fit_single_line_text(status_raw, status_budget, preferred=status_preferred, minimum=6.8)
    title = normalize_text(title_text)
    status_label = normalize_text(status_text)
    lines = [
        f'  <rect x="{x}" y="{y}" width="{width}" height="{height}" rx="12" fill="{node.get("fill", style_value(style, "node_fill"))}" stroke="{node.get("stroke", style_value(style, "node_stroke"))}" stroke-width="1.4"/>',
        f'  <rect data-graph-role="decoration" x="{x}" y="{y + 12}" width="4" height="{height - 24}" rx="2" fill="{status_color}"/>',
        f'  <circle cx="{x + 16}" cy="{y + 19}" r="5" fill="{status_color}"/>',
        f'  <text data-text-role="title" data-full-text="{normalize_attribute(title_raw)}" '
        f'data-text-max-width="{title_budget}" x="{x + 28}" y="{y + 24}" text-anchor="start" '
        f'class="node-title" font-size="{title_size}">{title}</text>',
        f'  <text data-text-role="status" data-full-text="{normalize_attribute(status_raw)}" '
        f'data-text-max-width="{status_budget}" x="{x + width - 10}" y="{y + 21}" text-anchor="end" '
        f'font-size="{status_size}" font-weight="800" fill="{status_color}">{status_label}</text>',
    ]
    metrics = list(node.get("metric_badges", []))[:4]
    chip_gap = 6.0
    chip_width = (width - 24 - chip_gap) / 2
    chip_height = 27.0
    for index, metric in enumerate(metrics):
        column = index % 2
        row = index // 2
        chip_x = x + 10 + column * (chip_width + chip_gap)
        chip_y = y + 38 + row * (chip_height + 5)
        metric_status = status_colors.get(str(metric.get("status", "unknown")), status_colors["unknown"])
        metric_name = normalize_text(str(metric.get("name", ""))[:3].upper())
        metric_value_raw = f'{metric.get("value", "")}{metric.get("unit", "")}'
        metric_window_raw = f'@{metric.get("window", "")}'
        metric_value_budget = max(20.0, chip_width - 16.0)
        metric_window_budget = max(18.0, chip_width - 44.0)
        metric_value_text, metric_value_size = fit_single_line_text(
            metric_value_raw, metric_value_budget, preferred=9.5, minimum=7.0
        )
        metric_window_text, metric_window_size = fit_single_line_text(
            metric_window_raw, metric_window_budget, preferred=6.8, minimum=5.8
        )
        metric_value = normalize_text(metric_value_text)
        metric_window = normalize_text(metric_window_text)
        lines.extend(
            [
                f'  <rect x="{chip_x}" y="{chip_y}" width="{chip_width}" height="{chip_height}" rx="6" fill="#13263a" stroke="#29435d" stroke-width="0.8"/>',
                f'  <circle cx="{chip_x + 8}" cy="{chip_y + 9}" r="2.5" fill="{metric_status}"/>',
                f'  <text x="{chip_x + 14}" y="{chip_y + 11.5}" class="metric-label">{metric_name}</text>',
                f'  <text data-text-role="metric-window" data-full-text="{normalize_attribute(metric_window_raw)}" '
                f'data-text-max-width="{metric_window_budget}" x="{chip_x + chip_width - 6}" y="{chip_y + 11.5}" '
                f'text-anchor="end" font-size="{metric_window_size}" font-weight="700" '
                f'fill="{style_value(style, "text_muted")}">{metric_window}</text>',
                f'  <text data-text-role="metric-value" data-full-text="{normalize_attribute(metric_value_raw)}" '
                f'data-text-max-width="{metric_value_budget}" x="{chip_x + 8}" y="{chip_y + 23}" '
                f'class="metric-value" font-size="{metric_value_size}">{metric_value}</text>',
            ]
        )
    return "\n".join(lines)


def render_trace_span(node: Dict[str, object], style: Dict[str, object]) -> str:
    x = to_float(node["x"])
    y = to_float(node["y"])
    width = to_float(node.get("width", 300))
    height = to_float(node.get("height", 28))
    status = str(node.get("status", "ok"))
    color = {"ok": "#38bdf8", "warn": "#f59e0b", "critical": "#f43f5e"}.get(status, "#64748b")
    title_raw = str(node.get("label", node.get("span_id", "span")))
    duration_raw = f'{node.get("duration_ms", "")} ms'
    duration_needed = geometry.estimate_text_width(duration_raw, 10, weight=1.08) + 4
    duration_budget = min(width * 0.3, max(34.0, duration_needed))
    title_budget = max(30.0, width - 34.0 - duration_budget)
    title_text, title_size = fit_single_line_text(title_raw, title_budget, preferred=11.5, minimum=8.0)
    duration_text, duration_size = fit_single_line_text(duration_raw, duration_budget, preferred=10, minimum=7.0)
    title = normalize_text(title_text)
    duration = normalize_text(duration_text)
    return "\n".join(
        [
            f'  <rect x="{x}" y="{y}" width="{width}" height="{height}" rx="6" fill="{color}" fill-opacity="0.12" stroke="{color}" stroke-width="1.2"/>',
            f'  <rect x="{x}" y="{y}" width="5" height="{height}" rx="2.5" fill="{color}"/>',
            f'  <text data-text-role="title" data-full-text="{normalize_attribute(title_raw)}" '
            f'data-text-max-width="{title_budget}" x="{x + 14}" y="{y + 18.5}" text-anchor="start" '
            f'class="node-title" font-size="{title_size}">{title}</text>',
            f'  <text data-text-role="duration" data-full-text="{normalize_attribute(duration_raw)}" '
            f'data-text-max-width="{duration_budget}" x="{x + width - 10}" y="{y + 18.5}" text-anchor="end" '
            f'font-size="{duration_size}" font-weight="700" fill="{style_value(style, "text_secondary")}">{duration}</text>',
        ]
    )


def render_otel_collector(node: Dict[str, object], style: Dict[str, object]) -> str:
    x = to_float(node["x"])
    y = to_float(node["y"])
    width = to_float(node.get("width", 180))
    height = to_float(node.get("height", 60))
    title_raw = str(node.get("label", "OTel Collector"))
    title_budget = max(24.0, width - 24.0)
    title_text, title_size = fit_single_line_text(title_raw, title_budget, preferred=13, minimum=8.5)
    title = normalize_text(title_text)
    return "\n".join(
        [
            f'  <rect x="{x}" y="{y}" width="{width}" height="{height}" rx="10" fill="#10263a" stroke="#22d3ee" stroke-width="1.5"/>',
            f'  <text data-text-role="title" data-full-text="{normalize_attribute(title_raw)}" '
            f'data-text-max-width="{title_budget}" x="{x + 12}" y="{y + 24}" text-anchor="start" '
            f'class="node-title" font-size="{title_size}">{title}</text>',
            f'  <text x="{x + 12}" y="{y + 44}" text-anchor="start" font-size="8.8" font-weight="700" fill="#67e8f9">RECEIVE → PROCESS → EXPORT</text>',
        ]
    )


def render_special_node(node: Dict[str, object], style: Dict[str, object], kind: str) -> Optional[str]:
    if kind == "review_card":
        return render_review_node(node, style)
    if kind == "cloud_service":
        return render_cloud_node(node, style)
    if kind in {"transit_station", "transit_junction", "transit_terminal"}:
        return render_transit_node(node, style)
    if kind == "ops_service":
        return render_ops_service(node, style)
    if kind == "trace_span":
        return render_trace_span(node, style)
    if kind == "otel_collector":
        return render_otel_collector(node, style)
    return None


def render_rect_node(node: Dict[str, object], style: Dict[str, object], kind: str) -> str:
    special = render_special_node(node, style, kind)
    if special is not None:
        return special
    x = to_float(node["x"])
    y = to_float(node["y"])
    width = to_float(node.get("width", 180))
    height = to_float(node.get("height", 76))
    rx = to_float(node.get("rx", style_value(style, "node_radius")))
    fill = str(node.get("fill", style_value(style, "node_fill")))
    stroke = str(node.get("stroke", style_value(style, "node_stroke")))
    stroke_width = to_float(node.get("stroke_width", 2.0 if kind != "rect" else 1.8))
    filter_attr = ""
    node_shadow = node.get("filter")
    if node_shadow:
        filter_attr = f' filter="url(#{node_shadow})"'
    elif node.get("glow"):
        glow_name = str(node.get("glow"))
        glow_map = {
            "blue": "glowBlue",
            "purple": "glowPurple",
            "green": "glowGreen",
            "orange": "glowOrange",
        }
        if glow_name in glow_map:
            filter_attr = f' filter="url(#{glow_map[glow_name]})"'
    elif style_value(style, "node_shadow"):
        if not node.get("flat", False):
            filter_attr = f' filter="{style_value(style, "node_shadow")}"'
    title_text = str(node.get("label", ""))
    title = normalize_text(title_text)
    subtitle = normalize_text(node.get("sublabel", ""))
    type_label = normalize_text(node.get("type_label", ""))
    accent_fill = node.get("accent_fill")
    lines = []

    if kind == "double_rect":
        lines.append(
            f'  <rect x="{x}" y="{y}" width="{width}" height="{height}" rx="{rx}" fill="{fill}" stroke="{stroke}" stroke-width="{stroke_width}"{filter_attr}/>'
        )
        lines.append(
            f'  <rect x="{x + 6}" y="{y + 6}" width="{width - 12}" height="{height - 12}" rx="{max(rx - 3, 4)}" fill="none" stroke="{stroke}" stroke-width="1.2" opacity="0.65"/>'
        )
    elif kind == "terminal":
        lines.append(
            f'  <rect x="{x}" y="{y}" width="{width}" height="{height}" rx="{rx}" fill="{fill}" stroke="{stroke}" stroke-width="{stroke_width}"{filter_attr}/>'
        )
        lines.append(
            f'  <rect x="{x}" y="{y}" width="{width}" height="18" rx="{rx}" fill="{node.get("header_fill", "#1f2937")}" opacity="0.95"/>'
        )
        header_colors = node.get("header_dots", ["#ef4444", "#f59e0b", "#10b981"])
        for idx, color in enumerate(header_colors):
            lines.append(f'  <circle cx="{x + 16 + idx * 14}" cy="{y + 9}" r="4" fill="{color}"/>')
        lines.append(
            f'  <text x="{x + 18}" y="{y + 44}" font-size="28" font-weight="700" fill="{node.get("prompt_fill", "#10b981")}">$</text>'
        )
        lines.append(
            f'  <text x="{x + 38}" y="{y + 44}" font-size="22" font-weight="500" fill="{style_value(style, "text_secondary")}">_</text>'
        )
    elif kind == "document":
        fold = min(18, width * 0.18, height * 0.22)
        path = (
            f"M {x} {y} L {x + width - fold} {y} L {x + width} {y + fold} "
            f"L {x + width} {y + height} L {x} {y + height} Z"
        )
        lines.append(
            f'  <path d="{path}" fill="{fill}" stroke="{stroke}" stroke-width="{stroke_width}"{filter_attr}/>'
        )
        lines.append(
            f'  <path d="M {x + width - fold} {y} L {x + width - fold} {y + fold} L {x + width} {y + fold}" fill="none" stroke="{stroke}" stroke-width="{stroke_width}"/>'
        )
        for idx in range(4):
            line_y = y + 26 + idx * 14
            lines.append(
                f'  <line x1="{x + 18}" y1="{line_y}" x2="{x + width - 28}" y2="{line_y}" stroke="{node.get("line_stroke", "#c4b5fd")}" stroke-width="1.2"/>'
            )
    elif kind == "folder":
        tab_w = min(54, width * 0.34)
        tab_h = 18
        path = (
            f"M {x} {y + tab_h} L {x + tab_w * 0.4} {y + tab_h} L {x + tab_w * 0.58} {y} "
            f"L {x + tab_w} {y} L {x + width} {y} L {x + width} {y + height} L {x} {y + height} Z"
        )
        lines.append(
            f'  <path d="{path}" fill="{fill}" stroke="{stroke}" stroke-width="{stroke_width}"{filter_attr}/>'
        )
        for idx in range(3):
            line_y = y + 42 + idx * 14
            lines.append(
                f'  <line x1="{x + 22}" y1="{line_y}" x2="{x + width - 22}" y2="{line_y}" stroke="{node.get("line_stroke", stroke)}" stroke-opacity="0.35" stroke-width="1.2"/>'
            )
    elif kind == "hexagon":
        inset = 22
        path = (
            f"M {x + inset} {y} L {x + width - inset} {y} L {x + width} {y + height / 2} "
            f"L {x + width - inset} {y + height} L {x + inset} {y + height} L {x} {y + height / 2} Z"
        )
        lines.append(f'  <path d="{path}" fill="{fill}" stroke="{stroke}" stroke-width="{stroke_width}"{filter_attr}/>')
    elif kind == "speech":
        tail = 18
        path = (
            f"M {x + rx} {y} L {x + width - rx} {y} Q {x + width} {y} {x + width} {y + rx} "
            f"L {x + width} {y + height - rx} Q {x + width} {y + height} {x + width - rx} {y + height} "
            f"L {x + 26} {y + height} L {x + 12} {y + height + tail} L {x + 16} {y + height} "
            f"L {x + rx} {y + height} Q {x} {y + height} {x} {y + height - rx} "
            f"L {x} {y + rx} Q {x} {y} {x + rx} {y} Z"
        )
        lines.append(f'  <path d="{path}" fill="{fill}" stroke="{stroke}" stroke-width="{stroke_width}"{filter_attr}/>')
    else:
        lines.append(
            f'  <rect x="{x}" y="{y}" width="{width}" height="{height}" rx="{rx}" fill="{fill}" stroke="{stroke}" stroke-width="{stroke_width}"{filter_attr}/>'
        )

    if accent_fill and kind == "icon_box":
        lines.append(
            f'  <rect x="{x + 12}" y="{y + 12}" width="{width - 24}" height="{height - 24}" rx="{max(rx - 4, 4)}" fill="{accent_fill}" opacity="0.9"/>'
        )

    if kind == "user_avatar":
        circle_fill = node.get("icon_fill", "#dbeafe")
        icon_stroke = node.get("icon_stroke", stroke)
        cx = x + 26
        cy = y + height / 2
        lines.append(f'  <circle cx="{cx}" cy="{cy}" r="18" fill="{circle_fill}" stroke="{icon_stroke}" stroke-width="1.6"/>')
        lines.append(f'  <circle cx="{cx}" cy="{cy - 6}" r="5" fill="{icon_stroke}"/>')
        lines.append(f'  <path d="M {cx - 10} {cy + 11} Q {cx} {cy + 2} {cx + 10} {cy + 11}" fill="none" stroke="{icon_stroke}" stroke-width="2"/>')

    if kind == "bot":
        cx = x + width / 2
        cy = y + height / 2 + 2
        body_fill = node.get("body_fill", "#1e293b")
        accent = node.get("accent_fill", "#34d399")
        lines.append(f'  <rect x="{cx - 42}" y="{cy - 32}" width="84" height="84" rx="18" fill="{body_fill}" stroke="#334155" stroke-width="1.8"{filter_attr}/>')
        lines.append(f'  <rect x="{cx - 26}" y="{cy - 16}" width="52" height="22" rx="6" fill="#0f172a" stroke="#475569" stroke-width="1.2"/>')
        lines.append(f'  <circle cx="{cx - 12}" cy="{cy - 5}" r="5" fill="{accent}"/>')
        lines.append(f'  <circle cx="{cx + 12}" cy="{cy - 5}" r="5" fill="{accent}"/>')
        lines.append(f'  <rect x="{cx - 14}" y="{cy + 14}" width="28" height="6" rx="3" fill="#334155"/>')
        lines.append(f'  <line x1="{cx}" y1="{cy - 36}" x2="{cx}" y2="{cy - 50}" stroke="{accent}" stroke-width="3"/>')
        lines.append(f'  <circle cx="{cx}" cy="{cy - 54}" r="5" fill="{accent}"/>')

    if kind == "circle_cluster":
        r = min(width, height) / 4.0
        centers = [(x + width * 0.36, y + height * 0.56), (x + width * 0.58, y + height * 0.45), (x + width * 0.74, y + height * 0.58)]
        for cx, cy in centers:
            lines.append(f'  <circle cx="{cx}" cy="{cy}" r="{r}" fill="{fill}" stroke="{stroke}" stroke-width="{stroke_width}"/>')

    type_offset = y + 18 if kind not in {"terminal", "bot"} else y + 18
    title_y = y + height / 2 - (4 if type_label and kind not in {"terminal", "bot"} else 0)
    if kind in {"document", "folder"}:
        title_y = y + height + 26
    elif kind == "circle_cluster":
        title_y = y + height / 2 + 8
    elif kind == "bot":
        title_y = y + height + 22
    elif kind == "user_avatar":
        title_y = y + height / 2 + 6

    if type_label:
        lines.append(f'  <text x="{x + (54 if kind == "user_avatar" else width / 2)}" y="{type_offset}" text-anchor="middle" class="node-type">{type_label}</text>')
        title_y += 10 if kind not in {"document", "folder", "circle_cluster", "bot"} else 0

    title_x = x + width / 2
    text_anchor = "middle"
    if kind == "user_avatar":
        title_x = x + 64
        text_anchor = "start"
    if kind == "terminal":
        title_y = y + height - 14
    if kind == "bot":
        title_x = x + width / 2
        text_anchor = "middle"
    title_size = to_float(
        node.get("title_size"),
        fitted_text_size(title_text, width - (32 if kind == "double_rect" else 24)),
    )
    lines.append(
        f'  <text x="{title_x}" y="{title_y}" text-anchor="{text_anchor}" class="node-title" '
        f'font-size="{title_size}">{title}</text>'
    )

    if subtitle:
        sub_y = title_y + 22
        if kind == "document":
            sub_y = y + height + 44
            title_y = y + height + 24
        if kind == "folder":
            sub_y = y + height + 44
        if kind == "circle_cluster":
            sub_y = y + height / 2 + 28
        if kind == "bot":
            sub_y = y + height + 42
        if kind == "terminal":
            sub_y = y + height + 20
        if kind == "user_avatar":
            sub_y = title_y + 22
        lines.append(f'  <text x="{title_x}" y="{sub_y}" text-anchor="{text_anchor}" class="node-sub">{subtitle}</text>')

    tag_lines = []
    if node.get("tags"):
        tag_x = x + 18
        tag_y = y + height - 20
        if kind in {"document", "folder", "circle_cluster", "bot", "terminal"}:
            tag_y = y + height + 52
        tag_lines = render_tags(node, tag_x, tag_y, style)
    lines.extend(tag_lines)

    return "\n".join(lines)


def render_node(node: Dict[str, object], style: Dict[str, object]) -> str:
    kind = str(node.get("kind", node.get("shape", "rect")))
    if kind == "cylinder":
        x = to_float(node["x"])
        y = to_float(node["y"])
        width = to_float(node.get("width", 160))
        height = to_float(node.get("height", 120))
        rx = width / 2
        ry = min(18, height / 8)
        fill = str(node.get("fill", "#ecfdf5"))
        stroke = str(node.get("stroke", "#10b981"))
        stroke_width = to_float(node.get("stroke_width", 2.2))
        label_text = str(node.get("label", ""))
        label = normalize_text(label_text)
        subtitle = normalize_text(node.get("sublabel", ""))
        lines = [
            f'  <ellipse cx="{x + width / 2}" cy="{y + ry}" rx="{rx / 2}" ry="{ry}" fill="{fill}" stroke="{stroke}" stroke-width="{stroke_width}"/>',
            f'  <rect x="{x}" y="{y + ry}" width="{width}" height="{height - 2 * ry}" fill="{fill}" stroke="{stroke}" stroke-width="{stroke_width}"/>',
            f'  <ellipse cx="{x + width / 2}" cy="{y + height - ry}" rx="{rx / 2}" ry="{ry}" fill="{fill}" stroke="{stroke}" stroke-width="{stroke_width}"/>',
            f'  <ellipse cx="{x + width / 2}" cy="{y + height * 0.38}" rx="{rx / 2}" ry="{ry}" fill="none" stroke="{stroke}" stroke-opacity="0.45" stroke-width="1.2"/>',
            f'  <ellipse cx="{x + width / 2}" cy="{y + height * 0.6}" rx="{rx / 2}" ry="{ry}" fill="none" stroke="{stroke}" stroke-opacity="0.25" stroke-width="1.2"/>',
            f'  <text x="{x + width / 2}" y="{y + height / 2 - 6}" text-anchor="middle" class="node-title" '
            f'font-size="{to_float(node.get("title_size"), fitted_text_size(label_text, width - 24))}">{label}</text>',
        ]
        if subtitle:
            lines.append(f'  <text x="{x + width / 2}" y="{y + height / 2 + 18}" text-anchor="middle" class="node-sub">{subtitle}</text>')
        return "\n".join(lines)
    return render_rect_node(node, style, kind)


def inferred_port(node: Node, toward: Point) -> str:
    left, top, right, bottom = node.bounds
    dx = toward[0] - node.cx
    dy = toward[1] - node.cy
    width = right - left
    height = bottom - top
    if abs(dx) * height >= abs(dy) * width:
        return "right" if dx >= 0 else "left"
    return "bottom" if dy >= 0 else "top"


def prepare_arrows(arrows: Sequence[Dict[str, object]], node_map: Dict[str, Node]) -> List[Dict[str, object]]:
    prepared = [copy.deepcopy(arrow) for arrow in arrows]
    endpoint_groups: Dict[Tuple[str, str], List[Tuple[int, str, str]]] = {}

    for index, arrow in enumerate(prepared):
        edge_id = str(arrow.get("id") or f"edge-{index:03d}")
        arrow["_edge_id"] = edge_id
        arrow["_edge_dom_id"] = str(arrow.get("_dom_id") or safe_identifier(edge_id, f"edge-{index:03d}"))
        source = node_map.get(str(arrow.get("source"))) if arrow.get("source") else None
        target = node_map.get(str(arrow.get("target"))) if arrow.get("target") else None
        start_hint = (to_float(arrow.get("x1")), to_float(arrow.get("y1")))
        end_hint = (to_float(arrow.get("x2")), to_float(arrow.get("y2")))

        if source is not None:
            toward = end_hint if target is None else (target.cx, target.cy)
            source_port = str(arrow.get("source_port") or inferred_port(source, toward)).lower()
            arrow["_resolved_source_port"] = source_port
            endpoint_groups.setdefault((source.node_id, source_port), []).append((index, "source", edge_id))
        if target is not None:
            toward = start_hint if source is None else (source.cx, source.cy)
            target_port = str(arrow.get("target_port") or inferred_port(target, toward)).lower()
            arrow["_resolved_target_port"] = target_port
            endpoint_groups.setdefault((target.node_id, target_port), []).append((index, "target", edge_id))

    for (node_id, port), endpoints in sorted(endpoint_groups.items()):
        node = node_map[node_id]
        span = (node.bounds[3] - node.bounds[1]) if port in {"left", "right"} else (node.bounds[2] - node.bounds[0])
        ordered = sorted(endpoints, key=lambda item: item[2])
        count = len(ordered)
        usable_span = max(0.0, span - 24.0)
        if count > 1 and usable_span / (count - 1) < 6.0:
            raise ValueError(f"PORT_CAPACITY: {node_id}.{port} cannot fit {count} distinct endpoints")
        spacing = 0.0 if count <= 1 else min(18.0, usable_span / (count - 1))
        for position, (arrow_index, endpoint, _) in enumerate(ordered):
            offset = (position - (count - 1) / 2.0) * spacing
            prepared[arrow_index][f"_{endpoint}_port_offset"] = round(offset, 2)
    return prepared


def render_arrow(
    arrow: Dict[str, object],
    style: Dict[str, object],
    node_map: Dict[str, Node],
    route_obstacles: Sequence[Bounds],
    label_obstacles: Sequence[Bounds],
    *,
    existing_routes: Sequence[Sequence[Point]],
    canvas_bounds: Bounds,
) -> ArrowRender:
    edge_id = str(arrow.get("_edge_id") or "edge")
    edge_dom_id = str(arrow.get("_edge_dom_id") or safe_identifier(edge_id, "edge"))
    start_hint = (to_float(arrow.get("x1")), to_float(arrow.get("y1")))
    end_hint = (to_float(arrow.get("x2")), to_float(arrow.get("y2")))
    source_node = node_map.get(str(arrow.get("source"))) if arrow.get("source") else None
    target_node = node_map.get(str(arrow.get("target"))) if arrow.get("target") else None
    source_port = arrow.get("_resolved_source_port") or arrow.get("source_port")
    target_port = arrow.get("_resolved_target_port") or arrow.get("target_port")

    if source_node is not None:
        toward = end_hint if target_node is None else (target_node.cx, target_node.cy)
        start = anchor_point(
            source_node,
            toward,
            str(source_port) if source_port else None,
            to_float(arrow.get("_source_port_offset")),
        )
    else:
        start = start_hint

    if target_node is not None:
        toward = start_hint if source_node is None else (source_node.cx, source_node.cy)
        end = anchor_point(
            target_node,
            toward,
            str(target_port) if target_port else None,
            to_float(arrow.get("_target_port_offset")),
        )
    else:
        end = end_hint

    # Keep source and target bounds in the graph. Port endpoints lie on their
    # boundaries, so valid leads remain possible while routes cannot cut
    # through either node and re-enter from the wrong side.
    obstacles = list(route_obstacles)

    routing_data = dict(arrow)
    if source_port:
        routing_data["source_port"] = source_port
    if target_port:
        routing_data["target_port"] = target_port
    route = build_orthogonal_route(
        start,
        end,
        obstacles,
        routing_data,
        canvas_bounds=canvas_bounds,
        existing_routes=existing_routes,
    )
    interactions = geometry.route_interactions(route, existing_routes)
    if interactions.overlap_count:
        raise ValueError(f"edge {edge_id} has an unresolved collinear overlap")
    bridges = list(interactions.crossings)
    bends = geometry.bend_count(route)
    stretch = quality.route_stretch(route)
    path_d = geometry.path_with_bridges(route, bridges)
    color = color_for_flow(style, arrow)
    width = to_float(arrow.get("stroke_width", style_value(style, "arrow_width")))
    dash = arrow.get("stroke_dasharray")
    if dash is None and arrow.get("dashed"):
        dash = "6,4"
    marker = marker_for_color(style, color, arrow)
    source_id = normalize_attribute(arrow.get("source", ""))
    target_id = normalize_attribute(arrow.get("target", ""))
    bridge_attr = ";".join(f"{geometry.format_number(x)},{geometry.format_number(y)}" for x, y in bridges)
    edge_kind = str(arrow.get("edge_kind", arrow.get("transit_type", "flow")))
    topic_id = str(arrow.get("topic_id", ""))
    protocol = str(arrow.get("protocol", ""))
    via = str(arrow.get("via", ""))
    critical_path_id = str(arrow.get("critical_path_id", ""))
    critical = "true" if arrow.get("critical") else "false"
    flow = str(arrow.get("flow", ""))
    motion_role = str(arrow.get("motion_role", ""))
    motion_stage = str(arrow.get("motion_stage", ""))
    motion_order = str(arrow.get("motion_order", ""))
    critical_hop = str(arrow.get("critical_hop", ""))
    critical_hops = str(arrow.get("critical_hops", ""))
    shared_attributes = (
        f'data-edge-id="{normalize_attribute(edge_id)}" data-source="{source_id}" data-target="{target_id}" '
        f'data-edge-kind="{normalize_attribute(edge_kind)}" data-topic-id="{normalize_attribute(topic_id)}" '
        f'data-flow="{normalize_attribute(flow)}" '
        f'data-motion-role="{normalize_attribute(motion_role)}" '
        f'data-motion-stage="{normalize_attribute(motion_stage)}" '
        f'data-motion-order="{normalize_attribute(motion_order)}" '
        f'data-protocol="{normalize_attribute(protocol)}" data-via="{normalize_attribute(via)}" '
        f'data-critical-path-id="{normalize_attribute(critical_path_id)}" '
        f'data-critical-hop="{normalize_attribute(critical_hop)}" '
        f'data-critical-hops="{normalize_attribute(critical_hops)}" '
        f'data-critical="{critical}" data-bends="{bends}" data-route-stretch="{round(stretch, 3)}"'
    )
    rendered_paths: List[str] = []
    if bridges:
        background = str(style_value(style, "background"))
        rendered_paths.append(
            f'  <path id="{normalize_attribute(edge_dom_id)}-bridge-mask" data-graph-role="bridge-mask" '
            f'data-owner="{normalize_attribute(edge_id)}" d="{path_d}" fill="none" stroke="{background}" '
            f'stroke-width="{round(width + 4.5, 2)}" stroke-linecap="round" stroke-linejoin="round"/>'
        )
    style_index = int(style.get("_style_index", 0))
    if style_index == 9:
        # A second, deliberately imperfect pencil stroke is decorative only;
        # the routable edge remains the single semantic connector below.
        rough_offset = deterministic_jitter(arrow.get("_rough_seed", 0), edge_id, 7, amplitude=1.2)
        rendered_paths.append(
            f'  <path id="{normalize_attribute(edge_dom_id)}-review-stroke" data-graph-role="decoration" '
            f'data-owner="{normalize_attribute(edge_id)}" d="{path_d}" fill="none" stroke="{color}" '
            f'stroke-width="0.9" stroke-dasharray="2.5,2" opacity="0.30" '
            f'stroke-dashoffset="{round(rough_offset, 2)}"/>'
        )
    elif style_index == 11 and str(arrow.get("transit_type", "")) == "rail":
        rendered_paths.append(
            f'  <path id="{normalize_attribute(edge_dom_id)}-rail-casing" data-graph-role="decoration" '
            f'data-owner="{normalize_attribute(edge_id)}" d="{path_d}" fill="none" '
            f'stroke="{style_value(style, "rail_casing")}" stroke-width="{round(width + 3.2, 2)}" '
            f'stroke-linecap="round" stroke-linejoin="round" opacity="0.28"/>'
        )
    elif style_index == 12 and arrow.get("critical"):
        rendered_paths.append(
            f'  <path id="{normalize_attribute(edge_dom_id)}-critical-glow" data-graph-role="decoration" '
            f'data-owner="{normalize_attribute(edge_id)}" d="{path_d}" fill="none" stroke="{color}" '
            f'stroke-width="{round(width + 5, 2)}" stroke-linecap="round" stroke-linejoin="round" '
            f'opacity="0.22" filter="url(#pulseGlow)"/>'
        )
    path = (
        f'  <path id="{normalize_attribute(edge_dom_id)}" data-graph-role="edge" {shared_attributes} '
        f'data-bridges="{bridge_attr}" d="{path_d}" fill="none" stroke="{color}" '
        f'stroke-width="{width}" stroke-linecap="round" stroke-linejoin="round" marker-end="{marker}"'
    )
    if dash:
        path += f' stroke-dasharray="{dash}"'
    if arrow.get("opacity") is not None:
        path += f' opacity="{arrow["opacity"]}"'
    path += "/>"
    rendered_paths.append(path)
    if style_index == 11 and str(arrow.get("transit_type", "")) == "rail" and len(route) >= 2:
        direction_x = (route[0][0] + route[-1][0]) / 2
        direction_y = (route[0][1] + route[-1][1]) / 2
        rendered_paths.append(
            f'  <path id="{normalize_attribute(edge_dom_id)}-direction" data-graph-role="decoration" '
            f'data-owner="{normalize_attribute(edge_id)}" d="M {direction_x - 3.5} {direction_y - 3.5} '
            f'L {direction_x + 1.5} {direction_y} L {direction_x - 3.5} {direction_y + 3.5}" '
            f'fill="none" stroke="{style_value(style, "background")}" stroke-width="1.4" '
            f'stroke-linecap="round" stroke-linejoin="round"/>'
        )
    if style_index == 12 and arrow.get("critical") and len(route) >= 2:
        hop_x = (route[0][0] + route[-1][0]) / 2
        hop_y = (route[0][1] + route[-1][1]) / 2
        hop = int(arrow.get("critical_hop", 1))
        total_hops = int(arrow.get("critical_hops", 1))
        rendered_paths.extend(
            [
                f'  <circle id="{normalize_attribute(edge_dom_id)}-hop" data-graph-role="decoration" '
                f'data-owner="{normalize_attribute(edge_id)}" cx="{hop_x}" cy="{hop_y}" r="9" '
                f'fill="{style_value(style, "background")}" stroke="{color}" stroke-width="1.2"/>',
                f'  <text data-graph-role="decoration" data-owner="{normalize_attribute(edge_id)}" '
                f'x="{hop_x}" y="{hop_y + 2.7}" text-anchor="middle" font-size="7" font-weight="800" '
                f'fill="{color}">{hop}/{total_hops}</text>',
            ]
        )
    label_svg = ""
    label_bounds = None

    label = str(arrow.get("label", "")).strip()
    secondary_label = protocol.strip() if style_index == 9 else ""
    if style_index == 10 and not label and via.strip():
        label = via.strip()
    if label:
        label_proxy = label
        if secondary_label and geometry.estimate_text_width(secondary_label, 12) > geometry.estimate_text_width(label, 12):
            label_proxy = secondary_label
        label_x, label_y = choose_label_position_avoiding(
            route,
            label_proxy,
            label_obstacles,
            routes=existing_routes,
            canvas_bounds=canvas_bounds,
            dx=to_float(arrow.get("label_dx", 0)),
            dy=to_float(arrow.get("label_dy", -4)),
        )
        if secondary_label:
            label_content = render_dual_label_badge(label_x, label_y, label, secondary_label, style)
            label_bounds = estimate_dual_label_bounds(label_x, label_y, label, secondary_label)
        else:
            label_content = render_label_badge(label_x, label_y, label, style, label_style=str(arrow.get("label_style", "badge")))
            label_bounds = estimate_label_bounds(label_x, label_y, label)
        label_svg = (
            f'  <g id="{normalize_attribute(edge_dom_id)}-label" data-graph-role="label" '
            f'data-owner="{normalize_attribute(edge_id)}" data-graph-bounds="'
            f'{geometry.format_number(label_bounds[0])},{geometry.format_number(label_bounds[1])},'
            f'{geometry.format_number(label_bounds[2])},{geometry.format_number(label_bounds[3])}">\n'
            f'{label_content}\n  </g>'
        )

    report: Dict[str, object] = {
        "id": edge_id,
        "source": str(arrow.get("source", "")),
        "target": str(arrow.get("target", "")),
        "source_port": [round(start[0], 2), round(start[1], 2)],
        "target_port": [round(end[0], 2), round(end[1], 2)],
        "waypoints": [[to_float(point[0]), to_float(point[1])] for point in (arrow.get("route_points") or [])],
        "route": [[round(x, 2), round(y, 2)] for x, y in route],
        "length": round(geometry.route_length(route), 2),
        "bends": bends,
        "route_stretch": round(stretch, 3),
        "crossings": [[round(x, 2), round(y, 2)] for x, y in interactions.crossings],
        "bridges": [[round(x, 2), round(y, 2)] for x, y in bridges],
    }
    return ArrowRender(edge_id, "\n".join(rendered_paths), label_svg, label_bounds, route, report)


def render_legend(
    legend: Sequence[Dict[str, object]],
    style: Dict[str, object],
    width: float,
    height: float,
    data: Dict[str, object],
) -> str:
    layout = legend_layout(data, legend, width, height)
    if not layout:
        return ""
    legend_x, legend_y, bounds = layout
    lines = [
        '  <g id="legend" data-graph-role="legend">',
        f'    <rect id="legend-zone" data-graph-role="reserved" data-reserved-kind="legend" '
        f'x="{geometry.format_number(bounds[0])}" y="{geometry.format_number(bounds[1])}" '
        f'width="{geometry.format_number(bounds[2] - bounds[0])}" height="{geometry.format_number(bounds[3] - bounds[1])}" '
        f'rx="10" fill="none" stroke="none"/>',
    ]
    orientation = str(data.get("legend_orientation", "vertical")).strip().lower()
    cursor_x = legend_x
    for idx, item in enumerate(legend):
        y = legend_y if orientation == "horizontal" else legend_y + idx * 22
        color = item.get("color")
        if not color:
            color = style_value(style, "arrow_colors")[FLOW_ALIASES.get(str(item.get("flow", "control")).lower(), "control")]
        marker = marker_for_color(style, str(color), {"flow": item.get("flow", "control")})
        item_x = cursor_x if orientation == "horizontal" else legend_x
        lines.append(f'    <line data-graph-role="decoration" x1="{item_x}" y1="{y}" x2="{item_x + 30}" y2="{y}" stroke="{color}" stroke-width="{style_value(style, "arrow_width")}" marker-end="{marker}"/>')
        lines.append(f'    <text data-graph-role="decoration" x="{item_x + 40}" y="{y + 4}" class="legend">{normalize_text(item.get("label", ""))}</text>')
        if orientation == "horizontal":
            cursor_x += 40 + geometry.estimate_text_width(str(item.get("label", "")), 12) + 28
    if data.get("legend_box"):
        bg = data.get("legend_box_fill", style_value(style, "arrow_label_bg"))
        opacity = data.get("legend_box_opacity", 0.88)
        lines.insert(
            2,
            f'    <rect data-graph-role="decoration" x="{geometry.format_number(bounds[0])}" '
            f'y="{geometry.format_number(bounds[1])}" width="{geometry.format_number(bounds[2] - bounds[0])}" '
            f'height="{geometry.format_number(bounds[3] - bounds[1])}" rx="10" fill="{bg}" opacity="{opacity}"/>',
        )
    lines.append("  </g>")
    return "\n".join(lines)


def render_footer(data: Dict[str, object], style: Dict[str, object], width: float, height: float) -> str:
    layout = footer_layout(data, width, height)
    if not layout:
        return ""
    x, y, _ = layout
    text = str(data.get("footer", "")).strip()
    return f'  <text x="{x}" y="{y}" class="footnote">{normalize_text(text)}</text>'


def bounds_metadata(bounds: Bounds) -> str:
    return ",".join(geometry.format_number(value) for value in bounds)


def build_svg_with_report(template_type: str, data: Dict[str, object]) -> Tuple[str, Dict[str, object]]:
    diagram = normalize_diagram(data, template_type)
    source_data = diagram.as_dict()
    mode = diagram.mode

    # ``normalize_diagram`` resolves both the legacy ``style`` selector and
    # the v1 ``visual_theme`` selector.  Reuse that single decision here so a
    # conflicting or misspelled selector can never silently fall back to a
    # different renderer.
    style_index, style = parse_style(diagram.style_index)
    style["_style_index"] = style_index
    composition_contract = quality.resolve_contract(
        source_data.get("composition", source_data.get("quality_profile", "standard"))
    )
    if source_data.get("style_overrides"):
        style.update(source_data["style_overrides"])
    width, height = parse_template_viewbox(template_type)
    width = to_float(source_data.get("width", width))
    height = to_float(source_data.get("height", height))
    if source_data.get("viewBox"):
        match = re.match(r"0 0 ([0-9.]+) ([0-9.]+)", str(source_data["viewBox"]))
        if match:
            width = float(match.group(1))
            height = float(match.group(2))
    if not math.isfinite(width) or not math.isfinite(height) or width <= 0 or height <= 0:
        raise ValueError("canvas width and height must be finite positive numbers")

    containers = list(source_data.get("containers", []))
    nodes_data = list(source_data.get("nodes", []))
    arrows_data = list(source_data.get("arrows", []))
    legend = list(source_data.get("legend", []))

    if style_index == 9:
        rough_seed = source_data.get("rough_seed", 0)
        for node_data in nodes_data:
            node_data.setdefault("_rough_seed", rough_seed)
        for container in containers:
            container.setdefault("_rough_seed", rough_seed)
        for arrow_data in arrows_data:
            arrow_data.setdefault("_rough_seed", rough_seed)

    defs = render_defs(style_index, style)
    canvas = render_canvas(style_index, style, width, height)
    title_block, content_start_y = render_title_block(style, source_data, width)
    window_controls = render_window_controls(source_data, style_index, width)
    header_meta = render_header_meta(source_data, style, width)
    style_signature = render_style_signature(style_index, source_data, width)

    # Assign auto_place y before building node maps so arrows route correctly
    for node_data in nodes_data:
        if "y" not in node_data and node_data.get("auto_place"):
            node_data["y"] = content_start_y + to_float(node_data.get("offset_y", 0))

    normalized_nodes = [normalize_node(node, f"node-{idx}") for idx, node in enumerate(nodes_data)]
    if len({node.node_id for node in normalized_nodes}) != len(normalized_nodes):
        raise ValueError("node ids must be unique")
    node_map = {node.node_id: node for node in normalized_nodes}

    # Raw semantic IDs stay intact in data attributes and reports. SVG DOM IDs
    # are allocated separately so normalization collisions (for example
    # ``a b`` vs ``a-b`` or multiple CJK-only IDs) cannot create duplicate IDs.
    used_dom_ids = set(_RESERVED_DOM_IDS)
    for index, container in enumerate(containers):
        raw_id = str(container.get("id") or f"container-{index:03d}")
        container["_dom_id"] = allocate_dom_identifier(
            safe_identifier(raw_id, f"container-{index:03d}"), used_dom_ids, ("-header",)
        )
    for index, arrow in enumerate(arrows_data):
        raw_id = str(arrow.get("id") or f"edge-{index:03d}")
        arrow["_dom_id"] = allocate_dom_identifier(
            safe_identifier(raw_id, f"edge-{index:03d}"), used_dom_ids, _EDGE_DOM_SUFFIXES
        )
    for node, node_data in zip(normalized_nodes, nodes_data):
        node_data["_dom_id"] = allocate_dom_identifier(
            f"node-{safe_identifier(node.node_id, 'node')}", used_dom_ids
        )

    section_obstacles = [bounds for container in containers if (bounds := container_header_bounds(container, style)) is not None]
    footer_reserved = footer_layout(source_data, width, height)
    blueprint_block_svg, blueprint_block_bounds = render_blueprint_title_block(source_data, style, style_index, width, height)
    node_obstacles = [node.bounds for node in normalized_nodes]
    placement_obstacles = list(node_obstacles) + list(section_obstacles)
    if footer_reserved:
        placement_obstacles.append(footer_reserved[2])
    if blueprint_block_bounds:
        placement_obstacles.append(blueprint_block_bounds)
    legend_placement = resolve_legend_layout(
        source_data,
        legend,
        width,
        height,
        placement_obstacles,
        arrows_data,
        node_map,
    )
    legend_reserved = legend_layout(source_data, legend, width, height)

    reserved_bounds = list(section_obstacles)
    if legend_reserved:
        reserved_bounds.append(legend_reserved[2])
    if footer_reserved:
        reserved_bounds.append(footer_reserved[2])
    if blueprint_block_bounds:
        reserved_bounds.append(blueprint_block_bounds)

    route_obstacles = node_obstacles + reserved_bounds
    label_obstacles = node_obstacles + reserved_bounds
    prepared_arrows = prepare_arrows(arrows_data, node_map)
    rendered_by_index: Dict[int, ArrowRender] = {}
    existing_routes: List[Sequence[Point]] = []
    routing_order = sorted(
        range(len(prepared_arrows)),
        key=lambda index: (0 if prepared_arrows[index].get("route_points") else 1, index),
    )
    issues: List[Dict[str, object]] = []
    for index in routing_order:
        rendered = render_arrow(
            prepared_arrows[index],
            style,
            node_map,
            route_obstacles,
            label_obstacles,
            existing_routes=existing_routes,
            canvas_bounds=(0.0, 0.0, width, height),
        )
        rendered_by_index[index] = rendered
        existing_routes.append(rendered.route)
        if rendered.label_bounds:
            label_obstacles.append(rendered.label_bounds)
            route_obstacles.append(rendered.label_bounds)
        if rendered.report["bridges"]:
            issues.append(
                {
                    "severity": "info",
                    "code": "EDGE_CROSSING_BRIDGED",
                    "element": rendered.edge_id,
                    "coordinates": rendered.report["bridges"],
                }
            )

    contract_data = composition_contract.as_dict()
    visual_theme = STYLE_NAMES[style_index]
    semantic_profile = str(diagram.semantic_report.get("profile", "generic"))
    diagram_type = str(source_data.get("diagram_type", mode))
    motion_scene = str(source_data.get("motion_scene", ""))
    lines = [
        f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {int(width)} {int(height)}" '
        f'width="{int(width)}" height="{int(height)}" data-generator="fireworks-tech-graph" '
        f'data-schema-version="1" data-text-metrics="heuristic-v1" '
        f'data-style-id="{style_index}" data-visual-theme="{normalize_attribute(visual_theme)}" '
        f'data-diagram-type="{normalize_attribute(diagram_type)}" '
        f'data-motion-scene="{normalize_attribute(motion_scene)}" '
        f'data-semantic-profile="{normalize_attribute(semantic_profile)}" data-semantic-valid="true" '
        f'data-quality-profile="{normalize_attribute(composition_contract.profile)}" '
        f'data-max-bends-per-edge="{contract_data["max_bends_per_edge"]}" '
        f'data-max-total-bends="{contract_data["max_total_bends"]}" '
        f'data-max-route-stretch="{contract_data["max_route_stretch"]}" '
        f'data-max-bridged-crossings="{contract_data["max_bridged_crossings"]}" '
        f'data-min-node-gap="{contract_data["min_node_gap"]}" '
        f'data-min-container-gutter="{contract_data["min_container_gutter"]}" '
        f'data-min-label-clearance="{contract_data["min_label_clearance"]}" '
        f'data-min-segment-length="{contract_data["min_segment_length"]}">'
    ]
    lines.append(defs)
    lines.append(canvas)
    if window_controls:
        lines.append(window_controls)
    if header_meta:
        lines.append(header_meta)
    lines.append(title_block)
    if style_signature:
        lines.append(style_signature)

    for index, container in enumerate(containers):
        container_id = str(container.get("_dom_id") or safe_identifier(container.get("id"), f"container-{index:03d}"))
        container_bounds = rectangle_bounds(
            to_float(container["x"]),
            to_float(container["y"]),
            to_float(container["width"]),
            to_float(container["height"]),
        )
        lines.append(
            f'  <g id="{normalize_attribute(container_id)}" data-graph-role="container" '
            f'data-container-id="{normalize_attribute(container.get("id", ""))}" '
            f'data-semantic-role="{normalize_attribute(container.get("deployment_kind", container.get("c4_type", "boundary")))}" '
            f'data-graph-bounds="{bounds_metadata(container_bounds)}">'
        )
        lines.append(render_section(container, style))
        header_bounds = container_header_bounds(container, style)
        if header_bounds:
            lines.append(
                f'    <rect id="{normalize_attribute(container_id)}-header" data-graph-role="reserved" '
                f'data-reserved-kind="container-header" x="{geometry.format_number(header_bounds[0])}" '
                f'y="{geometry.format_number(header_bounds[1])}" width="{geometry.format_number(header_bounds[2] - header_bounds[0])}" '
                f'height="{geometry.format_number(header_bounds[3] - header_bounds[1])}" fill="none" stroke="none"/>'
            )
        lines.append("  </g>")

    # A bridge is assigned to the edge routed after an existing edge. Preserve
    # that order in the SVG paint stack so the bridge mask erases the lower
    # edge before the bridge owner is painted on top.
    lines.extend(rendered_by_index[index].path_svg for index in routing_order)

    for node, node_data in zip(normalized_nodes, nodes_data):
        node_id = str(node_data.get("_dom_id") or f"node-{safe_identifier(node.node_id, 'node')}")
        semantic_role = node_data.get(
            "c4_type",
            node_data.get("deployment_kind", node_data.get("transit_role", node_data.get("ops_role", node_data.get("kind", "node")))),
        )
        lines.append(
            f'  <g id="{normalize_attribute(node_id)}" data-graph-role="node" '
            f'data-node-id="{normalize_attribute(node.node_id)}" '
            f'data-semantic-role="{normalize_attribute(semantic_role)}" '
            f'data-motion-role="{normalize_attribute(node_data.get("motion_role", ""))}" '
            f'data-motion-stage="{normalize_attribute(node_data.get("motion_stage", ""))}" '
            f'data-motion-order="{normalize_attribute(node_data.get("motion_order", ""))}" '
            f'data-parent="{normalize_attribute(node_data.get("parent", ""))}" '
            f'data-deployment-id="{normalize_attribute(node_data.get("deployment_id", ""))}" '
            f'data-topic-id="{normalize_attribute(node_data.get("topic_id", ""))}" '
            f'data-span-id="{normalize_attribute(node_data.get("span_id", ""))}" '
            f'data-station-order="{normalize_attribute(node_data.get("station_order", ""))}" '
            f'data-status="{normalize_attribute(node_data.get("status", ""))}" '
            f'data-start-ms="{normalize_attribute(node_data.get("start_ms", ""))}" '
            f'data-duration-ms="{normalize_attribute(node_data.get("duration_ms", ""))}" '
            f'data-parent-span="{normalize_attribute(node_data.get("parent_span", ""))}" '
            f'data-graph-bounds="{bounds_metadata(node.bounds)}">'
        )
        lines.append(render_node(node_data, style))
        lines.append("  </g>")

    lines.extend(
        rendered_by_index[index].label_svg
        for index in range(len(prepared_arrows))
        if rendered_by_index[index].label_svg
    )

    legend_svg = render_legend(legend, style, width, height, source_data)
    if legend_svg:
        lines.append(legend_svg)

    if blueprint_block_svg:
        if blueprint_block_bounds:
            lines.append(
                f'  <g id="blueprint-title-block" data-graph-role="reserved" '
                f'data-graph-bounds="{bounds_metadata(blueprint_block_bounds)}">'
            )
            lines.append(blueprint_block_svg)
            lines.append("  </g>")
        else:
            lines.append(blueprint_block_svg)

    footer_svg = render_footer(source_data, style, width, height)
    if footer_svg:
        if footer_reserved:
            lines.append(
                f'  <g id="footer" data-graph-role="reserved" data-graph-bounds="{bounds_metadata(footer_reserved[2])}">'
            )
            lines.append(footer_svg)
            lines.append("  </g>")
        else:
            lines.append(footer_svg)

    lines.append("</svg>")
    composition = quality.assess_composition(
        nodes=[(node.node_id, node.bounds) for node in normalized_nodes],
        containers=[
            (
                str(container.get("id") or f"container-{index:03d}"),
                rectangle_bounds(
                    to_float(container["x"]),
                    to_float(container["y"]),
                    to_float(container["width"]),
                    to_float(container["height"]),
                ),
            )
            for index, container in enumerate(containers)
        ],
        edges=[rendered_by_index[index].report for index in range(len(prepared_arrows))],
        contract=composition_contract,
    )
    if not composition["ok"]:
        summary = "; ".join(
            f'{item["code"]}:{item["element"]}={item["actual"]}>{item["limit"]}'
            for item in composition["violations"]
        )
        raise ValueError(f"COMPOSITION_QUALITY: {summary}")

    report: Dict[str, object] = {
        "schema_version": 1,
        "input_schema": diagram.input_schema,
        "mode": mode,
        "style": {"id": style_index, "name": visual_theme},
        "semantics": dict(diagram.semantic_report),
        "ok": True,
        "canvas": {"width": round(width, 2), "height": round(height, 2)},
        "text_metrics": "heuristic-v1",
        "placements": {
            "legend": {
                key: ([round(item, 2) for item in value] if key == "bounds" else value)
                for key, value in legend_placement.items()
            }
            if legend_placement
            else None
        },
        "edges": [rendered_by_index[index].report for index in range(len(prepared_arrows))],
        "composition": composition,
        "issues": issues,
        "summary": {
            "nodes": len(nodes_data),
            "edges": len(prepared_arrows),
            "bridged_crossings": sum(len(rendered.report["bridges"]) for rendered in rendered_by_index.values()),
        },
    }
    return "\n".join(line for line in lines if line), report


def build_svg(template_type: str, data: Dict[str, object]) -> str:
    return build_svg_with_report(template_type, data)[0]


def parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("template_type")
    parser.add_argument("output_path")
    parser.add_argument("data_json", nargs="?")
    parser.add_argument("--layout-report", "--report", dest="layout_report")
    return parser.parse_args(argv)


def main(argv: Optional[Sequence[str]] = None) -> None:
    args = parse_args(argv)
    try:
        if args.data_json is not None:
            data = json.loads(args.data_json)
        else:
            data = json.load(sys.stdin)
        svg_content, report = build_svg_with_report(args.template_type, data)
        os.makedirs(os.path.dirname(os.path.abspath(args.output_path)), exist_ok=True)
        with open(args.output_path, "w", encoding="utf-8") as handle:
            handle.write(svg_content)
        if args.layout_report:
            os.makedirs(os.path.dirname(os.path.abspath(args.layout_report)), exist_ok=True)
            with open(args.layout_report, "w", encoding="utf-8") as handle:
                json.dump(report, handle, ensure_ascii=False, indent=2, sort_keys=True)
                handle.write("\n")
        print(f"✓ SVG generated: {args.output_path}")
        if args.layout_report:
            print(f"✓ Layout report: {args.layout_report}")
    except FileNotFoundError as exc:
        print(f"Error: {exc}")
        sys.exit(1)
    except json.JSONDecodeError as exc:
        print(f"Error: Invalid JSON: {exc}")
        sys.exit(1)
    except ValueError as exc:
        if args.layout_report:
            os.makedirs(os.path.dirname(os.path.abspath(args.layout_report)), exist_ok=True)
            failure_report = {
                "schema_version": 1,
                "ok": False,
                "issues": [{"severity": "error", "code": "LAYOUT_ERROR", "message": str(exc)}],
            }
            with open(args.layout_report, "w", encoding="utf-8") as handle:
                json.dump(failure_report, handle, ensure_ascii=False, indent=2, sort_keys=True)
                handle.write("\n")
        print(f"Error: {exc}")
        sys.exit(1)
    except Exception as exc:  # pragma: no cover
        print(f"Unexpected error: {exc}")
        sys.exit(1)


if __name__ == "__main__":
    main()
skills/fireworks-tech-graph/scripts/README.md
# Fireworks Tech Graph - Scripts

辅助脚本集合,用于提高 SVG 图表生成的稳定性和效率。

## 脚本列表

### 1. validate-svg.sh

SVG 验证脚本,检查 SVG 语法并报告详细错误。

**用法:**
```bash
./validate-svg.sh <svg-file>
```

**检查项目:**
- XML 结构、属性语法和实体转义(使用 XML parser,避免把 `top_k=5` 之类的文本误判为属性)
- `marker-start` / `marker-mid` / `marker-end` 引用完整性
- 箭头与组件碰撞(支持绝对/相对 `M/L/H/V/Q/C/S/T` 路径,曲线路径采样检测)
- 渲染验证(cairosvg 优先,rsvg-convert 兜底)

**示例:**
```bash
./validate-svg.sh /path/to/diagram.svg
```

### 2. generate-diagram.sh

SVG 图表生成脚本,提供自动验证和 PNG 导出。

**用法:**
```bash
./generate-diagram.sh [OPTIONS]
```

**选项:**
- `-t, --type TYPE` - 图表类型(见脚本帮助)
- `-s, --style STYLE` - 风格编号(1-12,默认:1)
- `-o, --output PATH` - 输出路径(默认:当前目录)
- `-w, --width WIDTH` - PNG 宽度(像素,默认:1920)
- `--no-validate` - 跳过验证
- `-h, --help` - 显示帮助

**示例:**
```bash
# 生成架构图(Style 1)
./generate-diagram.sh -t architecture -s 1 -o ./output/arch.svg

# 生成流程图(Style 2,2400px 宽)
./generate-diagram.sh -t flowchart -s 2 -w 2400
```

**注意:** SVG 内容需要先准备好;这个脚本只负责验证与导出。

### 3. generate-from-template.py

基于风格配置和 JSON 数据生成 SVG。当前版本不再只是简单塞入 `nodes/arrows`,
而是会执行 style guide 中的部分可计算规则,例如:

- `style` - 风格编号(1-12)或规范风格名
- `semantic_profile` - 可选语义契约;Style 9-12 默认分别启用 C4、云部署、事件流和可观测性契约
- `containers` - 泳道 / 分组容器
- `containers[].header_prefix` / `containers[].header_text` - 工程编号式分区标题
- `containers[].side_label` - 左侧 layer label
- `nodes[].kind` - 语义组件类型,例如 `double_rect`、`cylinder`、`document`、`terminal`、`circle_cluster`
- `arrows[].flow` - 语义箭头类型,例如 `control`、`write`、`read`、`data`
- `source_port` / `target_port` - 指定端口锚点
- `route_points` / `corridor_x` / `corridor_y` - 控制复杂图的走线质量
- `style_overrides` - 对现有 style 做局部覆盖
- `window_controls` / `meta_*` - 顶部终端 chrome
- `blueprint_title_block` - 工程蓝图右下角 title block

**用法:**
```bash
python3 ./generate-from-template.py architecture ./output/arch.svg '{"style":1,"title":"My Diagram","containers":[],"nodes":[],"arrows":[]}'
```

**示例:**
```bash
python3 ./generate-from-template.py memory ./output/mem0.svg '{
  "style": 1,
  "title": "Mem0 Memory Architecture",
  "containers": [
    {"x":30,"y":90,"width":900,"height":90,"label":"Input Layer","header_prefix":"01"}
  ],
  "nodes": [
    {"id":"manager","kind":"double_rect","x":360,"y":220,"width":300,"height":72,"label":"Memory Manager"},
    {"id":"vector","kind":"cylinder","x":90,"y":360,"width":140,"height":110,"label":"Vector Store"}
  ],
  "arrows": [
    {"source":"manager","target":"vector","flow":"write","dashed":true}
  ]
}'
```

### 4. test-all-styles.sh

批量测试脚本覆盖 12 种风格。Style 1-7 与 9-12 从 JSON fixture 生成,AI 手绘的 Style 8 使用静态 SVG fixture;任何风格缺少回归 fixture 都会使批测失败。

**用法:**
```bash
./test-all-styles.sh
```

**功能:**
- 检查所有风格的参考文件
- 渲染 `fixtures/*.json` 回归样例
- 验证生成出的 SVG 文件
- 导出 PNG 文件到 `test-output/` 目录
- 生成测试报告

**输出:**
- 测试摘要(通过/失败统计)
- PNG 文件(带时间戳)
- 详细的验证错误信息

**示例:**
```bash
./test-all-styles.sh
```

## 依赖

所有脚本需要至少一个 PNG 渲染器(推荐 cairosvg):

- **cairosvg**(推荐)- SVG 转 PNG,CSS 支持最好
  ```bash
  python3 -m pip install cairosvg
  ```

- **rsvg-convert**(备选)- 系统包;复杂 SVG 可能丢失 CSS / `<foreignObject>`
  ```bash
  brew install librsvg                # macOS
  sudo apt install librsvg2-bin       # Ubuntu/Debian
  ```

`generate-diagram.sh` 会优先调用 cairosvg,缺失时自动回退到 rsvg-convert。完整对比见 [PNG 导出参考](../references/png-export.md)。

聚焦后的语义动效由 `fireworks.py animate`、`motion.py` 和 `svg2gif.js` 协作完成,只接收带有 12 套已验收 role/stage/order 契约之一的生成器语义 SVG。精确源文件字节不锁定,但任意同风格拓扑不会自动套用动效。媒体输出只允许经过验证的 GIF,默认还会生成同名 `.motion.json` 报告。运行时需要 FFmpeg/FFprobe、Chrome/Chromium,以及 Skill 安装位置可解析到的 `puppeteer` 或 `puppeteer-core`;当前工作目录中的同名模块不会被隐式执行。依赖安装与最简命令:

```bash
for SKILL_ROOT in \
  "$HOME/.agents/skills/fireworks-tech-graph" \
  "$HOME/.claude/skills/fireworks-tech-graph"
do
  [ -d "$SKILL_ROOT" ] || continue
  npm install --prefix "$SKILL_ROOT" --ignore-scripts --no-save --package-lock=false puppeteer-core@25.3.0
done
SKILL_ROOT="${CLAUDE_SKILL_DIR:-$HOME/.agents/skills/fireworks-tech-graph}"
python3 "$SKILL_ROOT/scripts/fireworks.py" animate diagram.svg diagram.gif
```

默认自动识别风格,输出 5.75 秒、20fps、960px 宽、115 帧无限循环 GIF。第 1–36 帧保持既有 draw-on,第 36–38 帧淡入运行流,第 38–109 帧为完整稳定数据流,第 110–114 帧按 `[1,.7575,.515,.2725,.03]` reset。Style 1–12 的 signature、速度、路径、几何和构建合同均为 `user-approved`,包括 `persistent-data-flow-head`、`terminal-evidence-stream`、`blueprint-registration-bead`、14×10 `notion-memory-card`、`glass-task-capsule`、`policy-seal`、`token-train`、`gem-tracer`、`review-cursor`、region chevrons、event train 与 ops scanner;共享 `+2s-settled-flow` 时间修订也已于 2026-07-17 验收,默认新包的 `review_status` 为 `user-approved`。显式 3.75 秒/75 帧和 2.75 秒/55 帧继续支持。75 帧及以下要求全部 raster 唯一;更长时间线允许非相邻重复出现在 full-opacity 区间,frame 110 是 reset opacity 为 1.00 的唯一例外并分类为 `intentional_reset_boundary_repeat`,frame 111–114 必须全局不同;至少保留 75 个唯一 raster 且相邻重复数为零。75-vs-115 gate 分开统计 binary / decoded-RGBA / guarded-antialias 三类;guarded 等价要求 AE ≤ 128、normalized RMSE ≤ 0.001、component 宽或高不超过 2px 且只落在 edge/node border,DOM 和 signature geometry 仍 strict-exact。完整约束见 [动效参考](../references/motion-effects.md)。

- **grep, sed, awk** - 文本处理(macOS 自带)

## 目录结构

```
fireworks-tech-graph/
├── SKILL.md                    # Skill 主文档
├── references/                 # 风格参考文件
│   ├── style-1-flat-icon.md
│   ├── style-2-dark-terminal.md
│   └── ...
├── fixtures/                   # 回归测试样例(JSON)
│   ├── mem0-style1.json
│   ├── tool-call-style2.json
│   └── ...
├── scripts/                    # 辅助脚本(本目录)
│   ├── README.md              # 本文档
│   ├── validate-svg.sh        # SVG 验证
│   ├── generate-diagram.sh    # SVG 验证与 PNG 导出
│   ├── generate-from-template.py # 模板化生成 SVG
│   ├── motion.py              # SVG 转 GIF 校验、编码与原子报告
│   ├── svg2gif.js             # Chromium 手动时间轴逐帧渲染
│   └── test-all-styles.sh     # 批量测试
└── test-output/               # 测试输出目录(自动创建)
```

## 使用场景

### 场景 1:验证现有 SVG

```bash
SKILL_ROOT=~/.agents/skills/fireworks-tech-graph # Codex
# SKILL_ROOT=~/.claude/skills/fireworks-tech-graph # Claude Code
"$SKILL_ROOT/scripts/validate-svg.sh" /path/to/your-diagram.svg
```

### 场景 2:生成并验证图表

1. 使用 Codex 或 Claude Code 生成 SVG 内容
2. 运行验证和导出:
   ```bash
   SKILL_ROOT=~/.agents/skills/fireworks-tech-graph # Codex
   # SKILL_ROOT=~/.claude/skills/fireworks-tech-graph # Claude Code
   "$SKILL_ROOT/scripts/generate-diagram.sh" -t architecture -s 1 -o ./output/arch.svg
   ```

### 场景 3:批量测试所有风格

```bash
SKILL_ROOT=~/.agents/skills/fireworks-tech-graph # Codex
# SKILL_ROOT=~/.claude/skills/fireworks-tech-graph # Claude Code
"$SKILL_ROOT/scripts/test-all-styles.sh"
```

测试脚本会自动:
1. 读取 `../fixtures/*.json`
2. 按 `template_type + style` 调用 `generate-from-template.py`
3. 运行 `validate-svg.sh`
4. 导出 PNG 到 `../test-output/`

### 场景 4:validator 单元测试

```bash
python3 -m unittest discover -s tests -p 'test_validate_svg.py' -v
```

覆盖 marker 双端引用、文本等号、`H/V` 路径、曲线路径、虚线组件和容器排除等正反例。

对启发式难以区分的形状,可显式添加 `data-graph-role="node|container|legend|decoration|label|background"`。`node` 会强制纳入障碍物检测,其余角色会从组件障碍物中排除;`legend` 组内的示例箭头也不会被当作业务流。

查看测试输出:
```bash
ls -lh ../test-output/
```

## 故障排除

### 问题:找不到 PNG 渲染器

**解决方案(任选其一,推荐 cairosvg):**
```bash
python3 -m pip install cairosvg        # 推荐
brew install librsvg                   # macOS 系统包
sudo apt install librsvg2-bin          # Ubuntu/Debian
```

### 问题:rsvg-convert 渲染缺框/缺文字

**原因:** rsvg-convert 对 `<foreignObject>`、CSS `filter`、复杂 `<style>` 块支持有限。

**解决方案:** 切换到 cairosvg:
```bash
python3 -m pip install cairosvg
```
脚本会自动优先使用 cairosvg。如果仍需要像素级还原(例如浏览器生成的 SVG),按 [PNG 导出参考](../references/png-export.md) 使用 `svg2png.js`。

### 问题:权限被拒绝

**解决方案:**
```bash
chmod +x *.sh
```

### 问题:SVG 验证失败

**解决方案:**
1. 查看详细错误信息
2. 使用 Edit 工具修复语法错误
3. 重新运行验证

## 开发说明

### 添加新的验证规则

编辑 `validate-svg.sh`,在现有检查项后添加新的检查逻辑:

```bash
# Check N: Your new check
echo -n "Checking something... "
# Your validation logic here
if [ condition ]; then
    echo -e "${GREEN}✓ Pass${NC}"
else
    echo -e "${RED}✗ Fail${NC}"
fi
```

### 扩展支持的图表类型

编辑 `generate-diagram.sh`,在 `--type` 参数处理中添加新类型。

## 版本历史

- **v1.2.0** (2026-07-17) - 语义动效与动态展示
  - 12 种已验收的 SVG→GIF 场景动效与 5.75 秒 settled-flow 默认时间线
  - README 全动态图集、GIF manifest、媒体回读与安装副本全风格门禁
  - 动效源 SVG 保持语义契约约束,不绑定标题和内容字节
- **v1.1.0** (2026-07-15) - 几何与分发升级
  - Schema v1 与类型化 Diagram IR
  - 正交路由、端口分流、图例/标签避让、跨线桥与确定性布局报告
  - `fireworks.py` 统一 CLI 与离线交互 HTML 导出
  - 完整 npx Skill 镜像、CI、Release archive parity 与安装 canary
- **v1.0.0** (2026-04-11) - 初始版本
  - SVG 验证脚本
  - 图表生成脚本
  - 批量测试脚本

## 许可证

MIT License - 与 fireworks-tech-graph skill 相同
skills/fireworks-tech-graph/schemas/diagram-v1.schema.json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://yizhiyanhua-ai.github.io/fireworks-tech-graph/schemas/diagram-v1.schema.json",
  "title": "Fireworks Tech Graph Diagram v1",
  "type": "object",
  "required": ["schema_version", "mode", "nodes", "arrows"],
  "properties": {
    "schema_version": { "const": 1 },
    "mode": { "type": "string", "minLength": 1 },
    "style": {
      "oneOf": [
        { "type": "integer", "minimum": 1, "maximum": 12 },
        { "type": "string", "minLength": 1 }
      ]
    },
    "visual_theme": {
      "oneOf": [
        { "type": "integer", "minimum": 1, "maximum": 12 },
        { "type": "string", "minLength": 1 }
      ]
    },
    "diagram_type": { "type": "string", "minLength": 1 },
    "semantic_profile": {
      "enum": ["generic", "c4-review", "cloud-fabric", "event-transit", "ops-pulse"]
    },
    "c4_level": { "enum": ["context", "container", "component"] },
    "review_state": { "type": "string", "minLength": 1 },
    "rough_seed": { "type": "integer" },
    "scope": { "type": "string", "minLength": 1 },
    "platform_profile": { "enum": ["provider-neutral", "aws", "azure", "gcp", "kubernetes"] },
    "deployment_mode": { "type": "string", "minLength": 1 },
    "icon_manifest_version": { "type": "string", "minLength": 1 },
    "line_code": { "type": "string", "minLength": 1 },
    "observation_window": { "type": "string", "minLength": 1 },
    "critical_path": {
      "type": "array",
      "minItems": 1,
      "uniqueItems": true,
      "items": { "type": "string", "minLength": 1 }
    },
    "topics": {
      "type": "array",
      "minItems": 1,
      "maxItems": 4,
      "items": {
        "type": "object",
        "required": ["id", "color"],
        "properties": {
          "id": { "type": "string", "minLength": 1 },
          "color": { "type": "string", "minLength": 1 }
        },
        "additionalProperties": true
      }
    },
    "quality_profile": { "enum": ["standard", "showcase"] },
    "composition": {
      "type": "object",
      "properties": {
        "profile": { "enum": ["standard", "showcase"] },
        "max_bends_per_edge": { "type": "integer", "minimum": 0 },
        "max_total_bends": { "type": "integer", "minimum": 0 },
        "max_route_stretch": { "type": "number", "minimum": 0 },
        "max_bridged_crossings": { "type": "integer", "minimum": 0 },
        "min_node_gap": { "type": "number", "minimum": 0 },
        "min_container_gutter": { "type": "number", "minimum": 0 },
        "min_label_clearance": { "type": "number", "minimum": 0 },
        "min_segment_length": { "type": "number", "minimum": 0 }
      },
      "additionalProperties": false
    },
    "legend_orientation": { "enum": ["vertical", "horizontal"] },
    "width": { "type": "number", "exclusiveMinimum": 0 },
    "height": { "type": "number", "exclusiveMinimum": 0 },
    "containers": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["id", "x", "y", "width", "height"],
        "properties": {
          "id": { "type": "string", "minLength": 1 },
          "parent": { "type": "string", "minLength": 1 },
          "deployment_kind": { "type": "string", "minLength": 1 },
          "x": { "type": "number" },
          "y": { "type": "number" },
          "width": { "type": "number", "exclusiveMinimum": 0 },
          "height": { "type": "number", "exclusiveMinimum": 0 }
        },
        "additionalProperties": true
      }
    },
    "nodes": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["id"],
        "properties": {
          "id": { "type": "string", "minLength": 1 },
          "parent": { "type": "string", "minLength": 1 },
          "c4_type": { "enum": ["person", "software_system", "external_system", "container", "component"] },
          "description": { "type": "string" },
          "technology": { "type": "string" },
          "deployment_id": { "type": "string", "minLength": 1 },
          "deployment_kind": { "type": "string", "minLength": 1 },
          "icon_id": { "type": "string", "minLength": 1 },
          "transit_role": { "enum": ["producer", "station", "junction", "consumer", "dlq", "state_store"] },
          "topic_id": { "type": "string", "minLength": 1 },
          "station_order": { "type": "integer", "minimum": 0 },
          "consumer_group": { "type": "string", "minLength": 1 },
          "ops_role": { "enum": ["service", "collector", "trace_span"] },
          "span_id": { "type": "string", "minLength": 1 },
          "parent_span": { "type": "string", "minLength": 1 },
          "start_ms": { "type": "number", "minimum": 0 },
          "duration_ms": { "type": "number", "exclusiveMinimum": 0 },
          "signals": {
            "type": "object",
            "required": ["latency", "traffic", "errors", "saturation"],
            "properties": {
              "latency": { "$ref": "#/$defs/goldenSignal" },
              "traffic": { "$ref": "#/$defs/goldenSignal" },
              "errors": { "$ref": "#/$defs/goldenSignal" },
              "saturation": { "$ref": "#/$defs/goldenSignal" }
            },
            "additionalProperties": false
          },
          "x": { "type": "number" },
          "y": { "type": "number" },
          "width": { "type": "number", "exclusiveMinimum": 0 },
          "height": { "type": "number", "exclusiveMinimum": 0 }
        },
        "additionalProperties": true
      }
    },
    "arrows": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "minLength": 1 },
          "source": { "type": "string" },
          "target": { "type": "string" },
          "edge_kind": { "enum": ["business", "telemetry"] },
          "transit_type": { "enum": ["rail", "publish", "branch", "consume", "retry", "state", "dead_letter"] },
          "topic_id": { "type": "string", "minLength": 1 },
          "protocol": { "type": "string", "minLength": 1 },
          "via": { "type": "string", "minLength": 1 },
          "route_points": {
            "type": "array",
            "items": {
              "type": "array",
              "prefixItems": [{ "type": "number" }, { "type": "number" }],
              "items": false,
              "minItems": 2,
              "maxItems": 2
            }
          }
        },
        "additionalProperties": true
      }
    }
  },
  "$defs": {
    "goldenSignal": {
      "type": "object",
      "required": ["value", "unit", "window", "status"],
      "properties": {
        "value": { "type": ["string", "number"] },
        "unit": { "type": "string", "minLength": 1 },
        "window": { "type": "string", "minLength": 1 },
        "status": { "enum": ["ok", "warn", "critical", "unknown"] }
      },
      "additionalProperties": false
    }
  },
  "additionalProperties": true
}
skills/fireworks-tech-graph/scripts/export-interactive-html.py
#!/usr/bin/env python3
"""Compatibility entrypoint for the interactive HTML exporter."""

from interactive_html import main


if __name__ == "__main__":
    raise SystemExit(main())
skills/fireworks-tech-graph/scripts/svg2png.js
#!/usr/bin/env node

const fs = require("fs");
const path = require("path");
const puppeteer = require("puppeteer");

async function main() {
  const directory = path.resolve(process.argv[2] || ".");
  const svgFiles = fs.readdirSync(directory).filter((file) => file.endsWith(".svg"));
  const browser = await puppeteer.launch({
    headless: "new",
    args: ["--no-sandbox", "--disable-setuid-sandbox"],
  });

  try {
    for (const file of svgFiles) {
      const svgPath = path.join(directory, file);
      const pngPath = svgPath.replace(/\.svg$/, ".png");
      const svgContent = fs.readFileSync(svgPath, "utf8");
      const widthMatch = svgContent.match(/width="(\d+)/);
      const heightMatch = svgContent.match(/height="(\d+)/);
      const viewBoxMatch = svgContent.match(/viewBox="[^"]*\s(\d+)\s(\d+)"/);
      const width = widthMatch ? Number(widthMatch[1]) : viewBoxMatch ? Number(viewBoxMatch[1]) : 1200;
      const height = heightMatch ? Number(heightMatch[1]) : viewBoxMatch ? Number(viewBoxMatch[2]) : 800;
      const page = await browser.newPage();

      await page.setViewport({ width, height, deviceScaleFactor: 2 });
      await page.setContent(
        `<html><body style="margin:0;background:transparent"><img src="data:image/svg+xml;base64,${Buffer.from(svgContent).toString("base64")}" width="${width}" height="${height}"></body></html>`,
        { waitUntil: "networkidle0" },
      );
      await page.screenshot({ path: pngPath, type: "png", omitBackground: true });
      await page.close();
      console.log(`Done: ${file} -> ${path.basename(pngPath)} (${width}x${height} @2x)`);
    }
  } finally {
    await browser.close();
  }
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});
skills/fireworks-tech-graph/scripts/svg2gif.js
#!/usr/bin/env node

"use strict";

const fs = require("fs");
const path = require("path");

const MINIMUM_FRAME_COUNT = 55;
const EMPTY_OPENING_FRAME = 0;
const RESET_OPACITY_SAMPLES = Object.freeze([1.00, 0.7575, 0.515, 0.2725, 0.03]);
const STYLE_1_DRAW_SCHEDULE = Object.freeze([
  Object.freeze({ role: "ingress", order: 0, start: 1, end: 8 }),
  Object.freeze({ role: "reason", order: 0, start: 5, end: 12 }),
  Object.freeze({ role: "extract", order: 0, start: 9, end: 16 }),
  Object.freeze({ role: "transform", order: 0, start: 13, end: 20 }),
  Object.freeze({ role: "resolve", order: 1, start: 17, end: 24 }),
  Object.freeze({ role: "memory-write", order: 0, start: 21, end: 28 }),
  Object.freeze({ role: "memory-read", order: 0, start: 25, end: 32 }),
  Object.freeze({ role: "response-context", order: 0, start: 29, end: 36 }),
]);
const STYLE_2_DRAW_SCHEDULE = Object.freeze([
  Object.freeze({ role: "ingress", stage: 1, order: 0, start: 1, end: 8 }),
  Object.freeze({ role: "delegate", stage: 2, order: 0, start: 5, end: 12 }),
  Object.freeze({ role: "tool-call", stage: 3, order: 0, start: 9, end: 16 }),
  Object.freeze({ role: "inspect", stage: 4, order: 0, start: 13, end: 20 }),
  Object.freeze({ role: "index", stage: 5, order: 0, start: 17, end: 24 }),
  Object.freeze({ role: "grounding", stage: 6, order: 0, start: 21, end: 28 }),
  Object.freeze({ role: "grounding", stage: 6, order: 1, start: 25, end: 32 }),
  Object.freeze({ role: "answer", stage: 7, order: 0, start: 29, end: 36 }),
]);
const STYLE_3_DRAW_SCHEDULE = Object.freeze([
  Object.freeze({ role: "ingress", stage: 1, order: 0, start: 1, end: 6 }),
  Object.freeze({ role: "policy", stage: 2, order: 0, start: 4, end: 9 }),
  Object.freeze({ role: "fanout", stage: 3, order: 0, start: 8, end: 13 }),
  Object.freeze({ role: "fanout", stage: 3, order: 1, start: 11, end: 16 }),
  Object.freeze({ role: "fanout", stage: 3, order: 2, start: 14, end: 19 }),
  Object.freeze({ role: "data-write", stage: 4, order: 0, start: 18, end: 23 }),
  Object.freeze({ role: "data-write", stage: 4, order: 1, start: 21, end: 26 }),
  Object.freeze({ role: "data-write", stage: 4, order: 2, start: 24, end: 29 }),
  Object.freeze({ role: "event", stage: 5, order: 0, start: 28, end: 33 }),
  Object.freeze({ role: "telemetry", stage: 6, order: 0, start: 31, end: 36 }),
]);
const STYLE_4_DRAW_SCHEDULE = Object.freeze([
  Object.freeze({ role: "sample", stage: 1, order: 0, start: 1, end: 4 }),
  Object.freeze({ role: "attend", stage: 2, order: 0, start: 5, end: 8 }),
  Object.freeze({ role: "invoke", stage: 3, order: 0, start: 9, end: 12 }),
  Object.freeze({ role: "remember", stage: 4, order: 0, start: 13, end: 22 }),
  Object.freeze({ role: "consolidate", stage: 5, order: 0, start: 23, end: 26 }),
  Object.freeze({ role: "recall", stage: 6, order: 0, start: 27, end: 36 }),
]);
const STYLE_5_DRAW_SCHEDULE = Object.freeze([
  Object.freeze({ role: "ingress", stage: 1, order: 0, start: 1, end: 6 }),
  Object.freeze({ role: "delegate", stage: 2, order: 0, start: 5, end: 12 }),
  Object.freeze({ role: "delegate", stage: 2, order: 1, start: 8, end: 15 }),
  Object.freeze({ role: "delegate", stage: 2, order: 2, start: 11, end: 18 }),
  Object.freeze({ role: "evidence", stage: 3, order: 0, start: 17, end: 24 }),
  Object.freeze({ role: "artifact", stage: 3, order: 1, start: 20, end: 27 }),
  Object.freeze({ role: "context", stage: 4, order: 0, start: 25, end: 30 }),
  Object.freeze({ role: "deliver", stage: 5, order: 0, start: 29, end: 36 }),
  Object.freeze({ role: "approval", stage: 5, order: 1, start: 29, end: 36 }),
]);
const STYLE_6_DRAW_SCHEDULE = Object.freeze([
  Object.freeze({ role: "ingress", stage: 1, order: 0, start: 1, end: 6 }),
  Object.freeze({ role: "dispatch", stage: 2, order: 0, start: 5, end: 12 }),
  Object.freeze({ role: "runtime-branch", stage: 3, order: 0, start: 10, end: 17 }),
  Object.freeze({ role: "runtime-branch", stage: 3, order: 1, start: 13, end: 20 }),
  Object.freeze({ role: "runtime-branch", stage: 3, order: 2, start: 16, end: 23 }),
  Object.freeze({ role: "foundation", stage: 4, order: 0, start: 21, end: 28 }),
  Object.freeze({ role: "foundation", stage: 4, order: 1, start: 24, end: 31 }),
  Object.freeze({ role: "foundation", stage: 4, order: 2, start: 27, end: 34 }),
  Object.freeze({ role: "promote", stage: 5, order: 0, start: 31, end: 36 }),
]);
const STYLE_7_DRAW_SCHEDULE = Object.freeze([
  Object.freeze({ role: "connect", stage: 1, order: 0, start: 1, end: 6 }),
  Object.freeze({ role: "prepare", stage: 2, order: 0, start: 5, end: 12 }),
  Object.freeze({ role: "invoke", stage: 3, order: 0, start: 10, end: 17 }),
  Object.freeze({ role: "tool-call", stage: 4, order: 0, start: 15, end: 22 }),
  Object.freeze({ role: "token-stream", stage: 4, order: 1, start: 18, end: 27 }),
  Object.freeze({ role: "govern", stage: 5, order: 0, start: 25, end: 32 }),
  Object.freeze({ role: "measure", stage: 5, order: 1, start: 25, end: 32 }),
  Object.freeze({ role: "promote", stage: 6, order: 0, start: 31, end: 36 }),
]);
const STYLE_8_DRAW_SCHEDULE = Object.freeze([
  Object.freeze({ role: "primary", stage: 1, order: 0, start: 1, end: 6 }),
  Object.freeze({ role: "primary", stage: 2, order: 0, start: 5, end: 10 }),
  Object.freeze({ role: "memory-read", stage: 3, order: 0, start: 9, end: 18 }),
  Object.freeze({ role: "tool-call", stage: 3, order: 1, start: 12, end: 21 }),
  Object.freeze({ role: "data", stage: 4, order: 0, start: 20, end: 25 }),
  Object.freeze({ role: "trace", stage: 5, order: 0, start: 24, end: 29 }),
  Object.freeze({ role: "feedback", stage: 6, order: 0, start: 28, end: 36 }),
]);
const STYLE_9_DRAW_SCHEDULE = Object.freeze([
  Object.freeze({ role: "review-entry", stage: 1, order: 0, start: 1, end: 7 }),
  Object.freeze({ role: "review-request", stage: 2, order: 0, start: 7, end: 13 }),
  Object.freeze({ role: "review-async", stage: 3, order: 0, start: 13, end: 22 }),
  Object.freeze({ role: "review-state", stage: 4, order: 0, start: 22, end: 30 }),
  Object.freeze({ role: "review-external", stage: 4, order: 1, start: 28, end: 36 }),
]);
const STYLE_10_DRAW_SCHEDULE = Object.freeze([
  Object.freeze({ role: "global-route", stage: 1, order: 0, start: 1, end: 12 }),
  Object.freeze({ role: "global-route", stage: 1, order: 1, start: 1, end: 12 }),
  Object.freeze({ role: "regional-write", stage: 2, order: 0, start: 13, end: 22 }),
  Object.freeze({ role: "regional-write", stage: 2, order: 1, start: 13, end: 22 }),
  Object.freeze({ role: "cross-region", stage: 3, order: 0, start: 23, end: 36 }),
]);
const STYLE_11_DRAW_SCHEDULE = Object.freeze([
  Object.freeze({ role: "topic-rail", stage: 1, order: 0, start: 1, end: 6 }),
  Object.freeze({ role: "topic-rail", stage: 2, order: 0, start: 7, end: 12 }),
  Object.freeze({ role: "topic-rail", stage: 3, order: 0, start: 13, end: 18 }),
  Object.freeze({ role: "topic-rail", stage: 4, order: 0, start: 19, end: 24 }),
  Object.freeze({ role: "dead-letter", stage: 5, order: 0, start: 25, end: 32 }),
  Object.freeze({ role: "state-project", stage: 5, order: 1, start: 29, end: 36 }),
]);
const STYLE_12_DRAW_SCHEDULE = Object.freeze([
  Object.freeze({ role: "critical-request", stage: 1, order: 0, start: 1, end: 6 }),
  Object.freeze({ role: "critical-request", stage: 2, order: 0, start: 7, end: 12 }),
  Object.freeze({ role: "critical-request", stage: 3, order: 0, start: 13, end: 18 }),
  Object.freeze({ role: "telemetry-export", stage: 4, order: 0, start: 19, end: 26 }),
]);
const STYLE_1_STREAM_CONTRACT = Object.freeze({
  start: 36,
  fadeInFactors: Object.freeze([0.30, 0.65, 1.00]),
  bodyDashPattern: Object.freeze([16, 25]),
  headDashPattern: Object.freeze([6, 35]),
  dashPeriod: 41,
  dashOffsetPerFrame: -6.0,
  headLeadOffset: -10,
  bodyOpacity: 0.90,
  headOpacity: 0.98,
  headStrokeWidth: 2.20,
  bodyPrimitive: "persistent-data-flow-stream",
  headPrimitive: "persistent-data-flow-head",
  bodyColor: "#06b6d4",
  headColor: "#e0f2fe",
  bodyWidthMaximum: 4.0,
  bodyWidthMinimum: 3.0,
  bodyWidthMultiplier: 1.60,
  bodyWidthDescription: "min(4.0, max(3.0, source_stroke * 1.60))",
  sourceStrokeWidth: 2.4,
  resolvedStrokeWidth: 3.84,
  phaseStageMultiplier: 7,
  phasePolicy: "(motionStage * 7 + motionOrder * 3) mod 41",
  expectedStreamPhases: Object.freeze([7, 14, 21, 28, 31, 35, 1, 8]),
  directionSentinels: Object.freeze([
    Object.freeze({ role: "ingress", order: 0, expected: Object.freeze(["right"]) }),
    Object.freeze({ role: "resolve", order: 1, expected: Object.freeze(["left"]) }),
    Object.freeze({ role: "memory-write", order: 0, expected: Object.freeze(["down", "left", "down"]) }),
  ]),
  resetBehavior: "all eight body/head pairs keep advancing while topology, labels, and both flow layers fade together",
});
const STYLE_2_STREAM_CONTRACT = Object.freeze({
  start: 36,
  fadeInFactors: Object.freeze([0.30, 0.65, 1.00]),
  bodyDashPattern: Object.freeze([15, 26]),
  headDashPattern: Object.freeze([5, 36]),
  dashPeriod: 41,
  dashOffsetPerFrame: -6.0,
  headLeadOffset: -10,
  bodyOpacity: 0.94,
  headOpacity: 1.00,
  headStrokeWidth: 2.00,
  bodyPrimitive: "terminal-evidence-stream",
  headPrimitive: "terminal-command-head",
  bodyColor: "inherit-source-stroke",
  headColor: "#f8fafc",
  bodyWidthMaximum: 3.8,
  bodyWidthMinimum: 3.0,
  bodyWidthMultiplier: 1.50,
  bodyWidthPrecision: 2,
  bodyWidthDescription: "min(3.8, max(3.0, source_stroke * 1.50))",
  sourceStrokeWidth: 2.3,
  resolvedStrokeWidth: 3.45,
  phaseStageMultiplier: 6,
  phasePolicy: "(motionStage * 6 + motionOrder * 3) mod 41",
  expectedStreamPhases: Object.freeze([6, 12, 18, 24, 30, 36, 39, 1]),
  expectedSourceColors: Object.freeze([
    "#a855f7", "#a855f7", "#38bdf8", "#38bdf8",
    "#22c55e", "#fb7185", "#fb7185", "#f97316",
  ]),
  directionSentinels: Object.freeze([
    Object.freeze({ role: "ingress", order: 0, expected: Object.freeze(["right"]) }),
    Object.freeze({ role: "delegate", order: 0, expected: Object.freeze(["down", "left", "down"]) }),
    Object.freeze({ role: "tool-call", order: 0, expected: Object.freeze(["right"]) }),
    Object.freeze({ role: "inspect", order: 0, expected: Object.freeze(["down"]) }),
    Object.freeze({ role: "index", order: 0, expected: Object.freeze(["right", "up"]) }),
    Object.freeze({ role: "grounding", order: 0, expected: Object.freeze(["up", "left", "up"]) }),
    Object.freeze({ role: "grounding", order: 1, expected: Object.freeze(["right"]) }),
    Object.freeze({ role: "answer", order: 0, expected: Object.freeze(["right"]) }),
  ]),
  resetBehavior: "all eight body/head pairs keep advancing while topology, labels, cursor, and both flow layers fade together",
});
const STYLE_3_STREAM_CONTRACT = Object.freeze({
  start: 36,
  fadeInFactors: Object.freeze([0.30, 0.65, 1.00]),
  bodyDashPattern: Object.freeze([12, 31]),
  dashPeriod: 43,
  dashOffsetPerFrame: -6.0,
  beadAdvancePerFrame: 6.0,
  bodyOpacity: 0.92,
  beadOpacity: 0.98,
  beadRadius: 3.0,
  beadFill: "#e0f2fe",
  beadStrokeWidth: 1.2,
  bodyPrimitive: "blueprint-distribution-wave",
  beadPrimitive: "blueprint-registration-bead",
  bodyColor: "inherit-source-stroke",
  bodyWidthMaximum: 3.4,
  bodyWidthMinimum: 2.8,
  bodyWidthMultiplier: 1.40,
  bodyWidthPrecision: 2,
  bodyWidthDescription: "min(3.4, max(2.8, source_stroke * 1.40))",
  sourceStrokeWidth: 2.1,
  resolvedStrokeWidth: 2.94,
  phaseStageMultiplier: 7,
  phaseOrderMultiplier: 0,
  phasePolicy: "(motionStage * 7 + motionOrder * 0) mod 43",
  expectedStreamPhases: Object.freeze([7, 14, 21, 21, 21, 28, 28, 28, 35, 42]),
  expectedSourceColors: Object.freeze([
    "#38bdf8", "#67e8f9", "#38bdf8", "#38bdf8", "#38bdf8",
    "#fde047", "#fde047", "#fde047", "#fb7185", "#fb7185",
  ]),
  directionSentinels: Object.freeze([
    Object.freeze({ role: "ingress", order: 0, expected: Object.freeze(["right"]) }),
    Object.freeze({ role: "policy", order: 0, expected: Object.freeze(["right"]) }),
    Object.freeze({ role: "fanout", order: 0, expected: Object.freeze(["down", "left", "down"]) }),
    Object.freeze({ role: "fanout", order: 1, expected: Object.freeze(["down"]) }),
    Object.freeze({ role: "fanout", order: 2, expected: Object.freeze(["down", "right", "down"]) }),
    Object.freeze({ role: "data-write", order: 0, expected: Object.freeze(["down"]) }),
    Object.freeze({ role: "data-write", order: 1, expected: Object.freeze(["down"]) }),
    Object.freeze({ role: "data-write", order: 2, expected: Object.freeze(["down"]) }),
    Object.freeze({ role: "event", order: 0, expected: Object.freeze(["right"]) }),
    Object.freeze({ role: "telemetry", order: 0, expected: Object.freeze(["down"]) }),
  ]),
  resetBehavior: "all ten bodies and registration beads keep advancing while topology, labels, and both Blueprint flow layers fade together",
});
const STYLE_4_STREAM_CONTRACT = Object.freeze({
  start: 36,
  fadeInFactors: Object.freeze([0.30, 0.65, 1.00]),
  bodyDashPattern: Object.freeze([12, 35]),
  dashPeriod: 47,
  dashOffsetPerFrame: -6.0,
  cardAdvancePerFrame: 6.0,
  bodyOpacity: 0.88,
  cardOpacity: 0.98,
  bodyPrimitive: "notion-memory-rail",
  cardPrimitive: "notion-memory-card",
  bodyColor: "semantic-memory-destination",
  bodyWidthMaximum: 3.0,
  bodyWidthMinimum: 2.4,
  bodyWidthMultiplier: 1.50,
  bodyWidthPrecision: 2,
  bodyWidthDescription: "min(3.0, max(2.4, source_stroke * 1.50))",
  sourceStrokeWidth: 1.8,
  resolvedStrokeWidth: 2.70,
  phaseStageMultiplier: 7,
  phaseOrderMultiplier: 0,
  phasePolicy: "(motionStage * 7 + motionOrder * 0) mod 47",
  expectedStreamPhases: Object.freeze([7, 14, 21, 28, 35, 42]),
  expectedSourceColors: Object.freeze([
    "#3b82f6", "#3b82f6", "#3b82f6", "#3b82f6", "#3b82f6", "#3b82f6",
  ]),
  semanticColors: Object.freeze([
    "#3b82f6", "#3b82f6", "#7c3aed", "#059669", "#ea580c", "#ea580c",
  ]),
  initialNormalizedProgress: Object.freeze([0.08, 0.22, 0.36, 0.50, 0.64, 0.78]),
  endpointClearance: 8,
  outerRect: Object.freeze({ x: -7, y: -5, width: 14, height: 10, rx: 2 }),
  cardFill: "#ffffff",
  cardStrokeWidth: 1.4,
  inkStrokeWidth: 2.0,
  inkLinecap: "butt",
  inkShapeRendering: "crispEdges",
  inkLines: Object.freeze([
    Object.freeze({ x1: -4.5, y1: -2, x2: 4, y2: -2 }),
    Object.freeze({ x1: -4.5, y1: 2, x2: 0.5, y2: 2 }),
  ]),
  directionSentinels: Object.freeze([
    Object.freeze({ role: "sample", order: 0, expected: Object.freeze(["right"]), tangentRotation: 0 }),
    Object.freeze({ role: "attend", order: 0, expected: Object.freeze(["right"]), tangentRotation: 0 }),
    Object.freeze({ role: "invoke", order: 0, expected: Object.freeze(["right"]), tangentRotation: 0 }),
    Object.freeze({ role: "remember", order: 0, expected: Object.freeze(["down"]), tangentRotation: 90 }),
    Object.freeze({ role: "consolidate", order: 0, expected: Object.freeze(["right"]), tangentRotation: 0 }),
    Object.freeze({ role: "recall", order: 0, expected: Object.freeze(["up"]), tangentRotation: -90 }),
  ]),
  resetBehavior: "all six rails and six memory cards keep advancing while topology, labels, rails, and cards fade together",
});
const SPECIALIZED_STREAM_CONTRACTS = Object.freeze({
  5: Object.freeze({
    start: 36, fadeInFactors: Object.freeze([0.30, 0.65, 1.00]),
    bodyDashPattern: Object.freeze([13, 30]), dashPeriod: 43, dashOffsetPerFrame: -6,
    advancePerFrame: 6, bodyOpacity: 0.88, bodyPrimitive: "glass-handoff-rail",
    signaturePrimitive: "glass-task-capsule", signatureKind: "glass-task-capsule",
    bodyWidthMaximum: 2.2, bodyWidthDescription: "min(2.2, source_stroke)", endpointClearance: 8,
    phaseStageMultiplier: 7, phaseOrderMultiplier: 3,
    phasePolicy: "(motionStage * 7 + motionOrder * 3) mod 43",
    expectedStreamPhases: Object.freeze([7, 14, 17, 20, 21, 24, 28, 35, 38]),
    directionSentinels: Object.freeze([
      Object.freeze({ role: "ingress", order: 0, expected: Object.freeze(["right"]) }),
      Object.freeze({ role: "delegate", order: 0, expected: Object.freeze(["down", "left", "down"]) }),
      Object.freeze({ role: "delegate", order: 2, expected: Object.freeze(["down", "right", "down"]) }),
      Object.freeze({ role: "evidence", order: 0, expected: Object.freeze(["down"]) }),
      Object.freeze({ role: "context", order: 0, expected: Object.freeze(["right"]) }),
    ]),
    auxiliary: Object.freeze({ kind: "node-halo", primitive: "coordinator-halo", nodeId: "coordinator", periodFrames: 16, minimumOpacity: 0.12, maximumOpacity: 0.32 }),
  }),
  6: Object.freeze({
    start: 36, fadeInFactors: Object.freeze([0.30, 0.65, 1.00]),
    bodyDashPattern: Object.freeze([11, 36]), dashPeriod: 47, dashOffsetPerFrame: -6,
    advancePerFrame: 6, bodyOpacity: 0.82, bodyPrimitive: "governance-thread",
    signaturePrimitive: "policy-seal", signatureKind: "policy-seal",
    bodyWidthMaximum: 2.8, bodyWidthDescription: "min(2.8, source_stroke)", endpointClearance: 8,
    phaseStageMultiplier: 7, phaseOrderMultiplier: 3,
    phasePolicy: "(motionStage * 7 + motionOrder * 3) mod 47",
    expectedStreamPhases: Object.freeze([7, 14, 21, 24, 27, 28, 31, 34, 35]),
    directionSentinels: Object.freeze([
      Object.freeze({ role: "ingress", order: 0, expected: Object.freeze(["right"]) }),
      Object.freeze({ role: "dispatch", order: 0, expected: Object.freeze(["down"]) }),
      Object.freeze({ role: "runtime-branch", order: 0, expected: Object.freeze(["left"]) }),
      Object.freeze({ role: "runtime-branch", order: 1, expected: Object.freeze(["right"]) }),
      Object.freeze({ role: "foundation", order: 0, expected: Object.freeze(["down"]) }),
      Object.freeze({ role: "promote", order: 0, expected: Object.freeze(["right"]) }),
    ]),
  }),
  7: Object.freeze({
    start: 36, fadeInFactors: Object.freeze([0.30, 0.65, 1.00]),
    bodyDashPattern: Object.freeze([10, 33]), dashPeriod: 43, dashOffsetPerFrame: -6,
    advancePerFrame: 6, bodyOpacity: 0.86, bodyPrimitive: "api-token-rail",
    signaturePrimitive: "token-train", signatureKind: "token-train",
    bodyWidthMaximum: 2.5, bodyWidthDescription: "min(2.5, source_stroke)", endpointClearance: 10,
    phaseStageMultiplier: 7, phaseOrderMultiplier: 3,
    phasePolicy: "(motionStage * 7 + motionOrder * 3) mod 43",
    expectedStreamPhases: Object.freeze([7, 14, 21, 28, 31, 35, 38, 42]),
    directionSentinels: Object.freeze([
      Object.freeze({ role: "connect", order: 0, expected: Object.freeze(["right"]) }),
      Object.freeze({ role: "prepare", order: 0, expected: Object.freeze(["down", "left", "down"]) }),
      Object.freeze({ role: "tool-call", order: 0, expected: Object.freeze(["right"]) }),
      Object.freeze({ role: "token-stream", order: 1, expected: Object.freeze(["down", "left", "down"]) }),
      Object.freeze({ role: "govern", order: 0, expected: Object.freeze(["down"]) }),
      Object.freeze({ role: "promote", order: 0, expected: Object.freeze(["right"]) }),
    ]),
  }),
  8: Object.freeze({
    start: 36, fadeInFactors: Object.freeze([0.30, 0.65, 1.00]),
    bodyDashPattern: Object.freeze([14, 33]), dashPeriod: 47, dashOffsetPerFrame: -6,
    advancePerFrame: 6, bodyOpacity: 0.86, bodyPrimitive: "luxury-circuit-rail",
    signaturePrimitive: "gem-tracer", signatureKind: "gem-tracer",
    bodyWidthMaximum: 2.8, bodyWidthDescription: "min(2.8, source_stroke)", endpointClearance: 8,
    phaseStageMultiplier: 7, phaseOrderMultiplier: 3,
    phasePolicy: "(motionStage * 7 + motionOrder * 3) mod 47",
    expectedStreamPhases: Object.freeze([7, 14, 21, 24, 28, 35, 42]),
    directionSentinels: Object.freeze([
      Object.freeze({ role: "primary", stage: 1, order: 0, expected: Object.freeze(["right"]) }),
      Object.freeze({ role: "primary", stage: 2, order: 0, expected: Object.freeze(["right"]) }),
      Object.freeze({ role: "memory-read", stage: 3, order: 0, expected: Object.freeze(["down", "left", "down"]) }),
      Object.freeze({ role: "feedback", stage: 6, order: 0, expected: Object.freeze(["up", "left", "up"]) }),
    ]),
  }),
  9: Object.freeze({
    start: 36, fadeInFactors: Object.freeze([0.30, 0.65, 1.00]),
    bodyDashPattern: Object.freeze([8, 33]), dashPeriod: 41, dashOffsetPerFrame: -5,
    advancePerFrame: 5, bodyOpacity: 0.82, bodyPrimitive: "review-trace-rail",
    signaturePrimitive: "review-cursor", signatureKind: "review-cursor",
    bodyWidthMaximum: 2.6, bodyWidthDescription: "min(2.6, source_stroke)", endpointClearance: 9,
    phaseStageMultiplier: 7, phaseOrderMultiplier: 3,
    phasePolicy: "(motionStage * 7 + motionOrder * 3) mod 41",
    expectedStreamPhases: Object.freeze([7, 14, 21, 28, 31]),
    directionSentinels: Object.freeze([
      Object.freeze({ role: "review-entry", order: 0, expected: Object.freeze(["right"]) }),
      Object.freeze({ role: "review-request", order: 0, expected: Object.freeze(["right"]) }),
      Object.freeze({ role: "review-async", order: 0, expected: Object.freeze(["down"]) }),
      Object.freeze({ role: "review-state", order: 0, expected: Object.freeze(["left"]) }),
      Object.freeze({ role: "review-external", order: 1, expected: Object.freeze(["right"]) }),
    ]),
  }),
  10: Object.freeze({
    start: 36, fadeInFactors: Object.freeze([0.30, 0.65, 1.00]),
    bodyDashPattern: Object.freeze([12, 31]), dashPeriod: 43, dashOffsetPerFrame: -6,
    advancePerFrame: 6, bodyOpacity: 0.82, bodyPrimitive: "cloud-flow-rail",
    signaturePrimitive: "region-chevron-pair-or-replication-capsule", signatureKind: "cloud-flow",
    bodyWidthMaximum: 2.7, bodyWidthDescription: "min(2.7, source_stroke)", endpointClearance: 8,
    phaseStageMultiplier: 7, phaseOrderMultiplier: 0,
    phasePolicy: "motionStage * 7 mod 43; A/B orders are phase-locked",
    expectedStreamPhases: Object.freeze([7, 7, 14, 14, 21]),
    directionSentinels: Object.freeze([
      Object.freeze({ role: "global-route", order: 0, expected: Object.freeze(["down"]) }),
      Object.freeze({ role: "global-route", order: 1, expected: Object.freeze(["down"]) }),
      Object.freeze({ role: "regional-write", order: 0, expected: Object.freeze(["down"]) }),
      Object.freeze({ role: "regional-write", order: 1, expected: Object.freeze(["down"]) }),
      Object.freeze({ role: "cross-region", order: 0, expected: Object.freeze(["right"]) }),
    ]),
    auxiliary: Object.freeze({ kind: "container-pair-pulse", primitive: "availability-pulse", containerIds: Object.freeze(["region-a", "region-b"]), periodFrames: 16, minimumOpacity: 0.10, maximumOpacity: 0.26 }),
  }),
  11: Object.freeze({
    start: 36, fadeInFactors: Object.freeze([0.30, 0.65, 1.00]),
    bodyDashPattern: Object.freeze([8, 33]), dashPeriod: 41, dashOffsetPerFrame: -5,
    advancePerFrame: 5, bodyOpacity: 0.78, bodyPrimitive: "event-transit-rail",
    signaturePrimitive: "event-train-or-branch-car", signatureKind: "event-transit",
    bodyWidthMaximum: 2.2, bodyWidthDescription: "min(2.2, source_stroke)", endpointClearance: 7,
    phaseStageMultiplier: 5, phaseOrderMultiplier: 3,
    phasePolicy: "(motionStage * 5 + motionOrder * 3) mod 41",
    expectedStreamPhases: Object.freeze([5, 10, 15, 20, 25, 28]),
    directionSentinels: Object.freeze([
      Object.freeze({ role: "topic-rail", stage: 1, order: 0, expected: Object.freeze(["right"]) }),
      Object.freeze({ role: "topic-rail", stage: 2, order: 0, expected: Object.freeze(["right"]) }),
      Object.freeze({ role: "topic-rail", stage: 3, order: 0, expected: Object.freeze(["right"]) }),
      Object.freeze({ role: "topic-rail", stage: 4, order: 0, expected: Object.freeze(["right"]) }),
      Object.freeze({ role: "dead-letter", stage: 5, order: 0, expected: Object.freeze(["down"]) }),
      Object.freeze({ role: "state-project", stage: 5, order: 1, expected: Object.freeze(["down"]) }),
    ]),
    auxiliary: Object.freeze({ kind: "station-dwell-rings", primitive: "station-dwell-ring", periodFrames: 10 }),
  }),
  12: Object.freeze({
    start: 36, fadeInFactors: Object.freeze([0.30, 0.65, 1.00]),
    bodyDashPattern: Object.freeze([12, 31]), dashPeriod: 43, dashOffsetPerFrame: -5,
    advancePerFrame: 5, bodyOpacity: 0.84, bodyPrimitive: "incident-pulse-rail-or-telemetry-export-rail",
    signaturePrimitive: "ecg-head-or-telemetry-export-packet", signatureKind: "ops-pulse",
    bodyWidthMaximum: 2.2, bodyWidthDescription: "min(2.2, source_stroke)", endpointClearance: 8,
    phaseStageMultiplier: 5, phaseOrderMultiplier: 0,
    phasePolicy: "motionStage * 5 mod 43",
    expectedStreamPhases: Object.freeze([5, 10, 15, 20]),
    directionSentinels: Object.freeze([
      Object.freeze({ role: "critical-request", stage: 1, order: 0, expected: Object.freeze(["right"]) }),
      Object.freeze({ role: "critical-request", stage: 2, order: 0, expected: Object.freeze(["right"]) }),
      Object.freeze({ role: "critical-request", stage: 3, order: 0, expected: Object.freeze(["right"]) }),
      Object.freeze({ role: "telemetry-export", stage: 4, order: 0, expected: Object.freeze(["down"]) }),
    ]),
    auxiliary: Object.freeze({ kind: "ops-pulse", primitive: "checkout-degraded-halo", nodeId: "checkout-service", periodFrames: 18, minimumOpacity: 0.10, maximumOpacity: 0.28 }),
  }),
});
const TERMINAL_CURSOR_CONTRACT = Object.freeze({
  primitive: "terminal-prompt-cursor",
  nodeId: "terminal",
  sourceText: "_",
  start: 16,
  periodFrames: 16,
  brightFrames: 8,
  absentFrames: 8,
  brightOpacity: 0.95,
  height: 2.2,
  fill: "#a7f3d0",
});
const SCENE_CONTRACTS = Object.freeze({
  "memory-weave": Object.freeze({
    styleId: 1,
    name: "Memory Weave",
    drawSchedule: STYLE_1_DRAW_SCHEDULE,
    stream: STYLE_1_STREAM_CONTRACT,
    signature: null,
    legacyStyleOneReport: true,
  }),
  "tool-grounding": Object.freeze({
    styleId: 2,
    name: "Dark Terminal",
    drawSchedule: STYLE_2_DRAW_SCHEDULE,
    stream: STYLE_2_STREAM_CONTRACT,
    signature: TERMINAL_CURSOR_CONTRACT,
    legacyStyleOneReport: false,
  }),
  "service-blueprint": Object.freeze({
    styleId: 3,
    name: "Blueprint",
    drawSchedule: STYLE_3_DRAW_SCHEDULE,
    stream: STYLE_3_STREAM_CONTRACT,
    signature: null,
    legacyStyleOneReport: false,
    streamMode: "blueprint-registration-bead",
    expectedRouteLabelCount: 7,
    requiresLabelPerEdge: false,
    requiredMaximumConcurrentDraws: 2,
  }),
  "memory-lifecycle": Object.freeze({
    styleId: 4,
    name: "Notion Clean",
    drawSchedule: STYLE_4_DRAW_SCHEDULE,
    stream: STYLE_4_STREAM_CONTRACT,
    signature: null,
    legacyStyleOneReport: false,
    streamMode: "notion-memory-card-handoff",
    expectedRouteLabelCount: 2,
    requiresLabelPerEdge: false,
    requiredMaximumConcurrentDraws: 1,
  }),
  "agent-orchestration": Object.freeze({
    styleId: 5, name: "Glassmorphism", drawSchedule: STYLE_5_DRAW_SCHEDULE,
    stream: SPECIALIZED_STREAM_CONTRACTS[5], signature: null, legacyStyleOneReport: false,
    streamMode: "specialized-live-signature", expectedRouteLabelCount: 9,
    requiresLabelPerEdge: true, requiredMaximumConcurrentDraws: 2,
  }),
  "governed-runtime": Object.freeze({
    styleId: 6, name: "Claude Official", drawSchedule: STYLE_6_DRAW_SCHEDULE,
    stream: SPECIALIZED_STREAM_CONTRACTS[6], signature: null, legacyStyleOneReport: false,
    streamMode: "specialized-live-signature", expectedRouteLabelCount: 9,
    requiresLabelPerEdge: true, requiredMaximumConcurrentDraws: 2,
  }),
  "token-stream": Object.freeze({
    styleId: 7, name: "OpenAI Official", drawSchedule: STYLE_7_DRAW_SCHEDULE,
    stream: SPECIALIZED_STREAM_CONTRACTS[7], signature: null, legacyStyleOneReport: false,
    streamMode: "specialized-live-signature", expectedRouteLabelCount: 8,
    requiresLabelPerEdge: true, requiredMaximumConcurrentDraws: 3,
  }),
  "golden-circuit": Object.freeze({
    styleId: 8, name: "Dark Luxury", drawSchedule: STYLE_8_DRAW_SCHEDULE,
    stream: SPECIALIZED_STREAM_CONTRACTS[8], signature: null, legacyStyleOneReport: false,
    streamMode: "specialized-live-signature", expectedRouteLabelCount: 7,
    requiresLabelPerEdge: true, requiredMaximumConcurrentDraws: 2, stageAwareRouteKey: true,
  }),
  "review-trace": Object.freeze({
    styleId: 9, name: "C4 Review Canvas", drawSchedule: STYLE_9_DRAW_SCHEDULE,
    stream: SPECIALIZED_STREAM_CONTRACTS[9], signature: null, legacyStyleOneReport: false,
    streamMode: "specialized-live-signature", expectedRouteLabelCount: 5,
    requiresLabelPerEdge: true, requiredMaximumConcurrentDraws: 2,
  }),
  "cloud-flow": Object.freeze({
    styleId: 10, name: "Cloud Fabric", drawSchedule: STYLE_10_DRAW_SCHEDULE,
    stream: SPECIALIZED_STREAM_CONTRACTS[10], signature: null, legacyStyleOneReport: false,
    streamMode: "specialized-live-signature", expectedRouteLabelCount: 3,
    requiresLabelPerEdge: false, requiredMaximumConcurrentDraws: 2,
  }),
  "event-transit": Object.freeze({
    styleId: 11, name: "Event Transit", drawSchedule: STYLE_11_DRAW_SCHEDULE,
    stream: SPECIALIZED_STREAM_CONTRACTS[11], signature: null, legacyStyleOneReport: false,
    streamMode: "specialized-live-signature", expectedRouteLabelCount: 0,
    requiresLabelPerEdge: false, requiredMaximumConcurrentDraws: 2, stageAwareRouteKey: true,
  }),
  "ops-pulse": Object.freeze({
    styleId: 12, name: "Ops Pulse", drawSchedule: STYLE_12_DRAW_SCHEDULE,
    stream: SPECIALIZED_STREAM_CONTRACTS[12], signature: null, legacyStyleOneReport: false,
    streamMode: "specialized-live-signature", expectedRouteLabelCount: 0,
    requiresLabelPerEdge: false, requiredMaximumConcurrentDraws: 1, stageAwareRouteKey: true,
  }),
});
const PRESETS = new Set(Object.keys(SCENE_CONTRACTS));

function parseArguments(argv) {
  const values = {};
  for (let index = 0; index < argv.length; index += 1) {
    const argument = argv[index];
    if (argument === "--probe") {
      values.probe = true;
      continue;
    }
    if (!argument.startsWith("--") || index + 1 >= argv.length) {
      throw new Error(`Invalid argument: ${argument}`);
    }
    values[argument.slice(2)] = argv[index + 1];
    index += 1;
  }
  return values;
}

function loadRenderer() {
  const attempts = [];
  const loaders = [
    {
      label: "puppeteer",
      load: () => require("puppeteer"),
      resolve: () => require.resolve("puppeteer"),
      version: () => require("puppeteer/package.json").version,
    },
    {
      label: "puppeteer-core",
      load: () => require("puppeteer-core"),
      resolve: () => require.resolve("puppeteer-core"),
      version: () => require("puppeteer-core/package.json").version,
    },
  ];
  if (process.env.FIREWORKS_PUPPETEER_PATH) {
    const explicitPath = path.resolve(process.env.FIREWORKS_PUPPETEER_PATH);
    loaders.unshift({
      label: "FIREWORKS_PUPPETEER_PATH",
      load: () => require(explicitPath),
      resolve: () => require.resolve(explicitPath),
      version: () => {
        const packagePath = fs.statSync(explicitPath).isDirectory()
          ? path.join(explicitPath, "package.json")
          : path.join(path.dirname(explicitPath), "package.json");
        return JSON.parse(fs.readFileSync(packagePath, "utf8")).version;
      },
    });
  }
  for (const candidate of loaders) {
    try {
      return {
        api: candidate.load(),
        module: candidate.label,
        resolvedModule: candidate.resolve(),
        moduleVersion: candidate.version(),
      };
    } catch (error) {
      attempts.push(`${candidate.label}:${error.code || error.message}`);
    }
  }
  throw new Error(
    `Puppeteer is unavailable. Install puppeteer-core or set FIREWORKS_PUPPETEER_PATH. ${attempts.join("; ")}`,
  );
}

function chromeExecutable(renderer) {
  const candidates = [
    process.env.FIREWORKS_CHROME_PATH,
    "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
    "/Applications/Chromium.app/Contents/MacOS/Chromium",
    "/usr/bin/google-chrome",
    "/usr/bin/google-chrome-stable",
    "/usr/bin/chromium",
    "/usr/bin/chromium-browser",
    "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
    "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
  ].filter(Boolean);
  for (const candidate of candidates) {
    if (fs.existsSync(candidate)) {
      return candidate;
    }
  }
  if (typeof renderer.executablePath === "function") {
    try {
      const bundled = renderer.executablePath();
      if (bundled && fs.existsSync(bundled)) {
        return bundled;
      }
    } catch (error) {
      // puppeteer-core intentionally has no bundled browser.
    }
  }
  return null;
}

function probe() {
  const loaded = loadRenderer();
  const executable = chromeExecutable(loaded.api);
  if (!executable) {
    throw new Error("No compatible Chrome or Chromium executable was found");
  }
  return {
    ok: true,
    module: loaded.module,
    resolved_module: loaded.resolvedModule,
    module_version: loaded.moduleVersion,
    chrome: executable,
    deterministic_timeline: true,
    sandbox_default: "enabled",
    input: "semantic-svg",
    motion_model: "connector-draw-on-with-persistent-data-flow",
    presets: [...PRESETS],
  };
}

async function installMotionRuntime(page, preset, fps, frameCount) {
  const sceneContract = SCENE_CONTRACTS[preset];
  if (!sceneContract) {
    throw new Error(`Preset ${preset} is awaiting style review`);
  }
  return page.evaluate(
    ({
      selectedPreset,
      selectedFps,
      selectedFrameCount,
      selectedSceneContract,
      minimumFrameCount,
      emptyOpeningFrame,
      resetOpacitySamples,
    }) => {
      const SVG_NS = "http://www.w3.org/2000/svg";
      const root = document.querySelector("svg");
      if (!root) {
        throw new Error("SVG root is unavailable");
      }
      const drawSchedule = selectedSceneContract.drawSchedule;
      const persistentStreamContract = selectedSceneContract.stream;
      const signatureContract = selectedSceneContract.signature;
      const expectedStreamPhases = persistentStreamContract.expectedStreamPhases;
      if (Number(root.dataset.styleId) !== selectedSceneContract.styleId) {
        throw new Error(
          `Preset ${selectedPreset} belongs to Style ${selectedSceneContract.styleId}, input is Style ${root.dataset.styleId || "unknown"}`,
        );
      }
      if (selectedFrameCount < minimumFrameCount) {
        throw new Error(`${selectedSceneContract.name} requires at least ${minimumFrameCount} rendered frames`);
      }
      const renderedFrameMax = selectedFrameCount - 1;
      const resetRange = [selectedFrameCount - resetOpacitySamples.length, renderedFrameMax];
      const fullOpacityEnd = resetRange[0] - 1;

      const attributeSignature = (element) => Array.from(element.attributes)
        .map((attribute) => `${attribute.name}=${attribute.value}`)
        .sort()
        .join("\u001f");
      const directTextSignature = (element) => Array.from(element.childNodes)
        .filter((node) => node.nodeType === Node.TEXT_NODE)
        .map((node) => node.data)
        .join("\u001f");
      const staticDomSnapshot = [root, ...root.querySelectorAll("*")].map((element) => ({
        element,
        parent: element.parentNode,
        attributes: attributeSignature(element),
        directText: directTextSignature(element),
      }));
      const assertStaticDomUnchanged = () => {
        for (const entry of staticDomSnapshot) {
          if (
            !entry.element.isConnected ||
            entry.element.parentNode !== entry.parent ||
            attributeSignature(entry.element) !== entry.attributes ||
            directTextSignature(entry.element) !== entry.directText
          ) {
            const identity = entry.element.id
              || entry.element.dataset?.edgeId
              || entry.element.dataset?.nodeId
              || entry.element.tagName;
            throw new Error(`Motion mutated source SVG element: ${identity}`);
          }
        }
      };

      const edges = Array.from(root.querySelectorAll('[data-graph-role="edge"]'));
      const nodes = Array.from(root.querySelectorAll('[data-graph-role="node"]'));
      const routeLabels = Array.from(root.querySelectorAll('[data-graph-role="label"][data-owner]'));
      const routeOwnerDecorations = Array.from(
        root.querySelectorAll('[data-graph-role="decoration"][data-owner]'),
      );
      if (!edges.length || !nodes.length) {
        throw new Error("Semantic edges and nodes are required");
      }
      const routeKey = (role, order, stage) => selectedSceneContract.stageAwareRouteKey
        ? `${role}/${stage}/${order}`
        : `${role}/${order}`;
      const edgesByRouteKey = new Map();
      edges.forEach((edge) => {
        const role = edge.dataset.motionRole || "";
        const order = Number(edge.dataset.motionOrder);
        const stage = Number(edge.dataset.motionStage);
        if (!role || !Number.isInteger(order) || !Number.isInteger(stage)) {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} has invalid motion metadata`);
        }
        const key = routeKey(role, order, stage);
        const matches = edgesByRouteKey.get(key) || [];
        matches.push(edge);
        edgesByRouteKey.set(key, matches);
      });
      const expectedRouteKeys = drawSchedule.map((entry) => routeKey(entry.role, entry.order, entry.stage));
      const missingRouteKeys = expectedRouteKeys.filter((key) => !edgesByRouteKey.has(key));
      const unexpectedRouteKeys = [...edgesByRouteKey.keys()].filter((key) => !expectedRouteKeys.includes(key));
      const duplicateRouteKeys = [...edgesByRouteKey.entries()]
        .filter(([, matches]) => matches.length !== 1)
        .map(([key]) => key);
      if (
        edges.length !== drawSchedule.length
        || missingRouteKeys.length
        || unexpectedRouteKeys.length
        || duplicateRouteKeys.length
      ) {
        throw new Error(
          `${selectedSceneContract.name} requires exact (motion role, motion order) coverage; `
          + `missing=${missingRouteKeys.join(",")}; unexpected=${unexpectedRouteKeys.join(",")}; duplicate=${duplicateRouteKeys.join(",")}`,
        );
      }
      const edgeForKey = (role, order, stage) => edgesByRouteKey.get(routeKey(role, order, stage))[0];
      const edgeForEntry = (entry) => edgeForKey(entry.role, entry.order, entry.stage);
      drawSchedule.forEach((entry) => {
        if (entry.stage !== undefined && Number(edgeForEntry(entry).dataset.motionStage) !== entry.stage) {
          throw new Error(`${selectedSceneContract.name} route ${routeKey(entry.role, entry.order, entry.stage)} has an invalid stage`);
        }
      });
      if (persistentStreamContract.expectedSourceColors) {
        const sourceColors = drawSchedule.map((entry) => edgeForEntry(entry).getAttribute("stroke"));
        if (sourceColors.some((color, index) => color !== persistentStreamContract.expectedSourceColors[index])) {
          throw new Error(
            `${selectedSceneContract.name} semantic route colors changed: expected `
            + `${persistentStreamContract.expectedSourceColors.join(",")}, got ${sourceColors.join(",")}`,
          );
        }
      }
      const labelCounts = new Map();
      routeLabels.forEach((label) => {
        const owner = label.dataset.owner || "";
        labelCounts.set(owner, (labelCounts.get(owner) || 0) + 1);
      });
      const edgeIds = new Set(edges.map((edge) => edge.dataset.edgeId || ""));
      const duplicateLabelOwners = [...labelCounts.entries()]
        .filter(([, count]) => count !== 1)
        .map(([owner]) => owner);
      const unknownLabelOwners = [...labelCounts.keys()].filter((owner) => !edgeIds.has(owner));
      const missingLabelOwners = edges
        .map((edge) => edge.dataset.edgeId || "")
        .filter((edgeId) => !labelCounts.has(edgeId));
      if (
        duplicateLabelOwners.length
        || unknownLabelOwners.length
        || (selectedSceneContract.requiresLabelPerEdge !== false && missingLabelOwners.length)
        || (
          selectedSceneContract.expectedRouteLabelCount !== undefined
          && routeLabels.length !== selectedSceneContract.expectedRouteLabelCount
        )
      ) {
        throw new Error(
          `${selectedSceneContract.name} route-owned label contract changed; `
          + `missing=${missingLabelOwners.join(",")}; unknown=${unknownLabelOwners.join(",")}; duplicate=${duplicateLabelOwners.join(",")}`,
        );
      }

      const concurrency = [];
      for (let frame = 0; frame <= renderedFrameMax; frame += 1) {
        concurrency.push(drawSchedule.filter((entry) => (
          selectedSceneContract.styleId >= 5
            ? frame > entry.start && frame < entry.end
            : frame >= entry.start && frame <= entry.end
        )).length);
      }
      const maximumConcurrentDraws = Math.max(...concurrency);
      if (maximumConcurrentDraws > selectedSceneContract.requiredMaximumConcurrentDraws) {
        throw new Error(
          `Draw-on schedule exceeds the ${selectedSceneContract.requiredMaximumConcurrentDraws}-connector concurrency limit`,
        );
      }
      if (
        selectedSceneContract.requiredMaximumConcurrentDraws !== undefined
        && maximumConcurrentDraws !== selectedSceneContract.requiredMaximumConcurrentDraws
      ) {
        throw new Error(`${selectedSceneContract.name} draw-on concurrency changed`);
      }

      const motionLayer = document.createElementNS(SVG_NS, "g");
      motionLayer.setAttribute("data-graph-role", "decoration");
      motionLayer.setAttribute("data-motion-layer", "connector-draw-on-with-persistent-data-flow");
      motionLayer.setAttribute("aria-hidden", "true");
      motionLayer.setAttribute("pointer-events", "none");
      const edgeHidingStyle = document.createElementNS(SVG_NS, "style");
      edgeHidingStyle.setAttribute("data-motion-source-edge-hider", "true");
      const sourceHidingSelectors = [
        '[data-graph-role="edge"]',
        '[data-graph-role="label"][data-owner]',
      ];
      if (selectedSceneContract.styleId === 11 || selectedSceneContract.styleId === 12) {
        sourceHidingSelectors.push(
          '[data-graph-role="decoration"][data-owner]:not([data-motion-primitive])',
        );
      }
      edgeHidingStyle.textContent = `${sourceHidingSelectors.join(",")}{visibility:hidden!important}`;
      motionLayer.append(edgeHidingStyle);
      const labelLayer = document.createElementNS(SVG_NS, "g");
      labelLayer.setAttribute("data-graph-role", "decoration");
      labelLayer.setAttribute("data-motion-layer", "route-label-arrivals");
      labelLayer.setAttribute("aria-hidden", "true");
      labelLayer.setAttribute("pointer-events", "none");
      const firstNode = nodes[0].parentNode === root ? nodes[0] : null;
      root.insertBefore(motionLayer, firstNode);

      const effects = [];
      const drawReports = [];
      const persistentStreamReports = [];
      const persistentPacketHeadReports = [];
      const registrationBeadReports = [];
      const notionMemoryCardReports = [];
      const specializedSignatureReports = [];
      const auxiliaryReports = [];
      const traceSpanRevealReports = [];
      const settledOwnerDecorationReports = [];
      const settledOwnerDecorationClones = [];
      const transientTraceSpanSources = [];
      let settledOwnerDirectionLayer = null;
      let waterfallScannerReport = null;
      let terminalPromptCursorReport = null;
      const clamped = (value, minimum = 0, maximum = 1) => Math.min(maximum, Math.max(minimum, value));
      const renderedFrameAtTime = (time) => clamped(
        time * selectedFps - 0.5,
        0,
        renderedFrameMax,
      );
      const inclusiveProgress = (frame, start, end) => clamped((frame - start + 1) / (end - start + 1));
      const resetOpacity = (frame) => {
        if (frame <= resetRange[0]) {
          return resetOpacitySamples[0];
        }
        const position = clamped(frame - resetRange[0], 0, resetOpacitySamples.length - 1);
        const lowerIndex = Math.floor(position);
        const upperIndex = Math.min(resetOpacitySamples.length - 1, Math.ceil(position));
        const fraction = position - lowerIndex;
        return resetOpacitySamples[lowerIndex]
          + (resetOpacitySamples[upperIndex] - resetOpacitySamples[lowerIndex]) * fraction;
      };
      const streamFadeIn = (frame) => {
        const position = clamped(
          frame - persistentStreamContract.start,
          0,
          persistentStreamContract.fadeInFactors.length - 1,
        );
        const lowerIndex = Math.floor(position);
        const upperIndex = Math.min(persistentStreamContract.fadeInFactors.length - 1, Math.ceil(position));
        const fraction = position - lowerIndex;
        return persistentStreamContract.fadeInFactors[lowerIndex]
          + (
            persistentStreamContract.fadeInFactors[upperIndex]
            - persistentStreamContract.fadeInFactors[lowerIndex]
          ) * fraction;
      };
      const removeCloneIdentity = (clone) => {
        for (const attribute of Array.from(clone.attributes)) {
          if (attribute.name === "id" || attribute.name.startsWith("data-")) {
            clone.removeAttribute(attribute.name);
          }
        }
      };
      const prepareDecoration = (edge, primitive, preserveMarkers) => {
        const clone = edge.cloneNode(false);
        removeCloneIdentity(clone);
        if (!preserveMarkers) {
          clone.removeAttribute("marker-start");
          clone.removeAttribute("marker-mid");
          clone.removeAttribute("marker-end");
        }
        clone.removeAttribute("filter");
        clone.setAttribute("data-graph-role", "decoration");
        clone.setAttribute("data-motion-primitive", primitive);
        clone.setAttribute("data-owner", edge.dataset.edgeId || "");
        clone.setAttribute("aria-hidden", "true");
        clone.setAttribute("pointer-events", "none");
        clone.setAttribute("display", "none");
        clone.setAttribute("opacity", "0");
        return clone;
      };

      function addDrawOnRoute(edge, entry) {
        if (typeof edge.getTotalLength !== "function") {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} does not support path drawing`);
        }
        const length = edge.getTotalLength();
        if (!Number.isFinite(length) || length <= 0) {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} has invalid path length`);
        }
        const drawPath = prepareDecoration(edge, "connector-draw-on", false);
        drawPath.setAttribute("stroke-dasharray", `${length} ${length}`);
        drawPath.setAttribute("stroke-dashoffset", String(length));
        const settledPath = prepareDecoration(edge, "settled-connector", true);
        settledPath.removeAttribute("stroke-dasharray");
        settledPath.removeAttribute("stroke-dashoffset");
        motionLayer.append(drawPath, settledPath);

        effects.push((time) => {
          const frame = renderedFrameAtTime(time);
          const opacity = resetOpacity(frame);
          if (frame < entry.start) {
            drawPath.setAttribute("display", "none");
            drawPath.setAttribute("opacity", "0");
            settledPath.setAttribute("display", "none");
            settledPath.setAttribute("opacity", "0");
            return;
          }
          if (frame < entry.end) {
            const startsIdleBatch = frame === entry.start && !drawSchedule.some((candidate) => (
              candidate !== entry && frame > candidate.start && frame < candidate.end
            ));
            const raw = selectedSceneContract.styleId >= 5
              ? clamped(
                (frame - entry.start + (startsIdleBatch ? 1 : 0))
                  / (entry.end - entry.start + (startsIdleBatch ? 1 : 0)),
              )
              : inclusiveProgress(frame, entry.start, entry.end);
            const progress = raw;
            drawPath.setAttribute("display", "inline");
            drawPath.setAttribute("opacity", String(opacity));
            drawPath.setAttribute("stroke-dashoffset", String(length * (1 - progress)));
            settledPath.setAttribute("display", "none");
            settledPath.setAttribute("opacity", "0");
            return;
          }
          drawPath.setAttribute("display", "none");
          drawPath.setAttribute("opacity", "0");
          settledPath.setAttribute("display", "inline");
          settledPath.setAttribute("opacity", String(opacity));
        });
        const drawReport = {
          edge_id: edge.dataset.edgeId || "",
          role: entry.role,
          rendered_frames: [entry.start, entry.end],
          easing: "linear",
          marker_during_draw: false,
          marker_after_arrival: Boolean(edge.getAttribute("marker-end")),
        };
        if (!selectedSceneContract.legacyStyleOneReport) {
          drawReport.stage = Number(edge.dataset.motionStage);
          drawReport.order = Number(edge.dataset.motionOrder);
          drawReport.schedule_key = routeKey(entry.role, entry.order, entry.stage);
        }
        drawReports.push(drawReport);
      }

      function addPersistentDataFlow(edge, entry) {
        const sourceWidth = Number.parseFloat(edge.getAttribute("stroke-width") || "1.5");
        const rawBodyStrokeWidth = Math.min(
          persistentStreamContract.bodyWidthMaximum,
          Math.max(
            persistentStreamContract.bodyWidthMinimum,
            sourceWidth * persistentStreamContract.bodyWidthMultiplier,
          ),
        );
        const bodyStrokeWidth = persistentStreamContract.bodyWidthPrecision === undefined
          ? rawBodyStrokeWidth
          : Number(rawBodyStrokeWidth.toFixed(persistentStreamContract.bodyWidthPrecision));
        const motionStage = Number(edge.dataset.motionStage);
        const motionOrder = Number(edge.dataset.motionOrder);
        if (!Number.isFinite(motionStage) || !Number.isFinite(motionOrder)) {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} has invalid stream phase metadata`);
        }
        const initialPhase = (
          (
            motionStage * persistentStreamContract.phaseStageMultiplier
            + motionOrder * 3
          ) % persistentStreamContract.dashPeriod
          + persistentStreamContract.dashPeriod
        ) % persistentStreamContract.dashPeriod;
        const streamColor = persistentStreamContract.bodyColor === "inherit-source-stroke"
          ? edge.getAttribute("stroke")
          : persistentStreamContract.bodyColor;
        const packetHeadColor = persistentStreamContract.headColor;
        const stream = prepareDecoration(edge, persistentStreamContract.bodyPrimitive, false);
        stream.setAttribute("fill", "none");
        stream.setAttribute("stroke", streamColor);
        stream.setAttribute("stroke-width", String(bodyStrokeWidth));
        stream.setAttribute("stroke-linecap", "round");
        stream.setAttribute("stroke-linejoin", "round");
        stream.setAttribute("stroke-dasharray", persistentStreamContract.bodyDashPattern.join(" "));

        const packetHead = prepareDecoration(edge, persistentStreamContract.headPrimitive, false);
        packetHead.setAttribute("fill", "none");
        packetHead.setAttribute("stroke", packetHeadColor);
        packetHead.setAttribute("stroke-width", String(persistentStreamContract.headStrokeWidth));
        packetHead.setAttribute("stroke-linecap", "round");
        packetHead.setAttribute("stroke-linejoin", "round");
        packetHead.setAttribute("stroke-dasharray", persistentStreamContract.headDashPattern.join(" "));
        motionLayer.append(stream, packetHead);

        const streamDecorations = [stream, packetHead];
        const markerFree = streamDecorations.every((decoration) => (
          !decoration.hasAttribute("marker-start")
          && !decoration.hasAttribute("marker-mid")
          && !decoration.hasAttribute("marker-end")
        ));
        const filterFree = streamDecorations.every((decoration) => !decoration.hasAttribute("filter"));
        if (!markerFree || !filterFree) {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} stream pair must be marker-free and filter-free`);
        }

        effects.push((time) => {
          const frame = renderedFrameAtTime(time);
          if (frame < persistentStreamContract.start || frame > renderedFrameMax) {
            stream.setAttribute("display", "none");
            stream.setAttribute("opacity", "0");
            packetHead.setAttribute("display", "none");
            packetHead.setAttribute("opacity", "0");
            return;
          }
          const dashOffset = initialPhase
            + (frame - persistentStreamContract.start) * persistentStreamContract.dashOffsetPerFrame;
          const fade = streamFadeIn(frame) * resetOpacity(frame);
          stream.setAttribute("display", "inline");
          stream.setAttribute("opacity", String(persistentStreamContract.bodyOpacity * fade));
          stream.setAttribute("stroke-dashoffset", String(dashOffset));
          packetHead.setAttribute("display", "inline");
          packetHead.setAttribute("opacity", String(persistentStreamContract.headOpacity * fade));
          packetHead.setAttribute(
            "stroke-dashoffset",
            String(dashOffset + persistentStreamContract.headLeadOffset),
          );
        });
        const streamReport = {
          edge_id: edge.dataset.edgeId || "",
          role: entry.role,
          primitive: persistentStreamContract.bodyPrimitive,
          rendered_frames: [persistentStreamContract.start, renderedFrameMax],
          fade_in_frames: [persistentStreamContract.start, persistentStreamContract.start + 2],
          full_opacity_frames: [persistentStreamContract.start + 2, fullOpacityEnd],
          stroke_width: bodyStrokeWidth,
          color: streamColor,
          dash_pattern: persistentStreamContract.bodyDashPattern,
          dash_period: persistentStreamContract.dashPeriod,
          dash_offset_per_rendered_frame: persistentStreamContract.dashOffsetPerFrame,
          initial_phase: initialPhase,
          phase_policy: persistentStreamContract.phasePolicy,
          motion_stage: motionStage,
          motion_order: motionOrder,
          direction: "source-to-target",
          opacity: persistentStreamContract.bodyOpacity,
          travel_easing: "linear",
          marker_free: markerFree,
          filter_free: filterFree,
        };
        const packetHeadReport = {
          edge_id: edge.dataset.edgeId || "",
          role: entry.role,
          primitive: persistentStreamContract.headPrimitive,
          rendered_frames: [persistentStreamContract.start, renderedFrameMax],
          fade_in_frames: [persistentStreamContract.start, persistentStreamContract.start + 2],
          full_opacity_frames: [persistentStreamContract.start + 2, fullOpacityEnd],
          stroke_width: persistentStreamContract.headStrokeWidth,
          color: packetHeadColor,
          dash_pattern: persistentStreamContract.headDashPattern,
          dash_period: persistentStreamContract.dashPeriod,
          dash_offset_per_rendered_frame: persistentStreamContract.dashOffsetPerFrame,
          body_initial_phase: initialPhase,
          dash_offset_from_body: persistentStreamContract.headLeadOffset,
          initial_dash_offset: initialPhase + persistentStreamContract.headLeadOffset,
          direction: "source-to-target",
          opacity: persistentStreamContract.headOpacity,
          travel_easing: "linear",
          marker_free: markerFree,
          filter_free: filterFree,
        };
        if (!selectedSceneContract.legacyStyleOneReport) {
          streamReport.schedule_key = routeKey(entry.role, entry.order, entry.stage);
          streamReport.source_stroke = edge.getAttribute("stroke");
          packetHeadReport.schedule_key = routeKey(entry.role, entry.order, entry.stage);
          packetHeadReport.motion_stage = motionStage;
          packetHeadReport.motion_order = motionOrder;
        }
        persistentStreamReports.push(streamReport);
        persistentPacketHeadReports.push(packetHeadReport);
      }

      function addBlueprintDistributionWave(edge, entry) {
        if (typeof edge.getTotalLength !== "function" || typeof edge.getPointAtLength !== "function") {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} does not support registration-bead travel`);
        }
        const pathLength = edge.getTotalLength();
        if (!Number.isFinite(pathLength) || pathLength <= 0) {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} has invalid registration-bead path length`);
        }
        const sourceWidth = Number.parseFloat(edge.getAttribute("stroke-width") || "1.5");
        if (sourceWidth !== persistentStreamContract.sourceStrokeWidth) {
          throw new Error(
            `Edge ${edge.dataset.edgeId || "unknown"} source stroke width changed: `
            + `expected ${persistentStreamContract.sourceStrokeWidth}, got ${sourceWidth}`,
          );
        }
        const rawBodyStrokeWidth = Math.min(
          persistentStreamContract.bodyWidthMaximum,
          Math.max(
            persistentStreamContract.bodyWidthMinimum,
            sourceWidth * persistentStreamContract.bodyWidthMultiplier,
          ),
        );
        const bodyStrokeWidth = Number(rawBodyStrokeWidth.toFixed(persistentStreamContract.bodyWidthPrecision));
        if (bodyStrokeWidth !== persistentStreamContract.resolvedStrokeWidth) {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} Blueprint wave width changed`);
        }
        const motionStage = Number(edge.dataset.motionStage);
        const motionOrder = Number(edge.dataset.motionOrder);
        if (!Number.isFinite(motionStage) || !Number.isFinite(motionOrder)) {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} has invalid Blueprint phase metadata`);
        }
        const initialPhase = (
          (
            motionStage * persistentStreamContract.phaseStageMultiplier
            + motionOrder * persistentStreamContract.phaseOrderMultiplier
          ) % persistentStreamContract.dashPeriod
          + persistentStreamContract.dashPeriod
        ) % persistentStreamContract.dashPeriod;
        const sourceColor = edge.getAttribute("stroke");
        const stream = prepareDecoration(edge, persistentStreamContract.bodyPrimitive, false);
        stream.setAttribute("fill", "none");
        stream.setAttribute("stroke", sourceColor);
        stream.setAttribute("stroke-width", String(bodyStrokeWidth));
        stream.setAttribute("stroke-linecap", "round");
        stream.setAttribute("stroke-linejoin", "round");
        stream.setAttribute("stroke-dasharray", persistentStreamContract.bodyDashPattern.join(" "));

        const bead = document.createElementNS(SVG_NS, "circle");
        const initialPoint = edge.getPointAtLength(initialPhase % pathLength);
        bead.setAttribute("data-graph-role", "decoration");
        bead.setAttribute("data-motion-primitive", persistentStreamContract.beadPrimitive);
        bead.setAttribute("data-owner", edge.dataset.edgeId || "");
        bead.setAttribute("cx", String(initialPoint.x));
        bead.setAttribute("cy", String(initialPoint.y));
        bead.setAttribute("r", String(persistentStreamContract.beadRadius));
        bead.setAttribute("fill", persistentStreamContract.beadFill);
        bead.setAttribute("stroke", sourceColor);
        bead.setAttribute("stroke-width", String(persistentStreamContract.beadStrokeWidth));
        bead.setAttribute("opacity", "0");
        bead.setAttribute("aria-hidden", "true");
        bead.setAttribute("pointer-events", "none");
        motionLayer.append(stream, bead);

        const decorations = [stream, bead];
        const markerFree = decorations.every((decoration) => (
          !decoration.hasAttribute("marker-start")
          && !decoration.hasAttribute("marker-mid")
          && !decoration.hasAttribute("marker-end")
        ));
        const filterFree = decorations.every((decoration) => !decoration.hasAttribute("filter"));
        if (!markerFree || !filterFree) {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} Blueprint wave must be marker-free and filter-free`);
        }

        effects.push((time) => {
          const frame = renderedFrameAtTime(time);
          if (frame < persistentStreamContract.start || frame > renderedFrameMax) {
            stream.setAttribute("display", "none");
            stream.setAttribute("opacity", "0");
            bead.setAttribute("opacity", "0");
            return;
          }
          const liveFrame = frame - persistentStreamContract.start;
          const dashOffset = initialPhase + liveFrame * persistentStreamContract.dashOffsetPerFrame;
          const beadDistance = (
            (
              initialPhase + liveFrame * persistentStreamContract.beadAdvancePerFrame
            ) % pathLength
            + pathLength
          ) % pathLength;
          const point = edge.getPointAtLength(beadDistance);
          const fade = streamFadeIn(frame) * resetOpacity(frame);
          stream.setAttribute("display", "inline");
          stream.setAttribute("opacity", String(persistentStreamContract.bodyOpacity * fade));
          stream.setAttribute("stroke-dashoffset", String(dashOffset));
          bead.setAttribute("cx", String(point.x));
          bead.setAttribute("cy", String(point.y));
          bead.setAttribute("opacity", String(persistentStreamContract.beadOpacity * fade));
        });
        persistentStreamReports.push({
          edge_id: edge.dataset.edgeId || "",
          role: entry.role,
          schedule_key: routeKey(entry.role, entry.order, entry.stage),
          primitive: persistentStreamContract.bodyPrimitive,
          rendered_frames: [persistentStreamContract.start, renderedFrameMax],
          fade_in_frames: [persistentStreamContract.start, persistentStreamContract.start + 2],
          full_opacity_frames: [persistentStreamContract.start + 2, fullOpacityEnd],
          stroke_width: bodyStrokeWidth,
          color: sourceColor,
          source_stroke: sourceColor,
          dash_pattern: persistentStreamContract.bodyDashPattern,
          dash_period: persistentStreamContract.dashPeriod,
          dash_offset_per_rendered_frame: persistentStreamContract.dashOffsetPerFrame,
          initial_phase: initialPhase,
          phase_policy: persistentStreamContract.phasePolicy,
          motion_stage: motionStage,
          motion_order: motionOrder,
          direction: "source-to-target",
          opacity: persistentStreamContract.bodyOpacity,
          travel_easing: "linear",
          marker_free: markerFree,
          filter_free: filterFree,
        });
        registrationBeadReports.push({
          edge_id: edge.dataset.edgeId || "",
          role: entry.role,
          schedule_key: routeKey(entry.role, entry.order, entry.stage),
          primitive: persistentStreamContract.beadPrimitive,
          shape: "circle",
          rendered_frames: [persistentStreamContract.start, renderedFrameMax],
          fade_in_frames: [persistentStreamContract.start, persistentStreamContract.start + 2],
          full_opacity_frames: [persistentStreamContract.start + 2, fullOpacityEnd],
          radius: persistentStreamContract.beadRadius,
          fill: persistentStreamContract.beadFill,
          stroke: sourceColor,
          stroke_width: persistentStreamContract.beadStrokeWidth,
          opacity: persistentStreamContract.beadOpacity,
          initial_path_distance: initialPhase,
          initial_point: { x: initialPoint.x, y: initialPoint.y },
          path_length: pathLength,
          path_advance_per_rendered_frame: persistentStreamContract.beadAdvancePerFrame,
          direction: "source-to-target",
          wrap: "target-end-to-source-start",
          animated_attributes: ["cx", "cy", "opacity"],
          motion_stage: motionStage,
          motion_order: motionOrder,
          marker_free: markerFree,
          filter_free: filterFree,
        });
      }

      function addNotionMemoryCardHandoff(edge, entry) {
        if (typeof edge.getTotalLength !== "function" || typeof edge.getPointAtLength !== "function") {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} does not support Notion memory-card travel`);
        }
        const pathLength = edge.getTotalLength();
        const endpointClearance = persistentStreamContract.endpointClearance;
        if (!Number.isFinite(pathLength) || pathLength <= endpointClearance * 2) {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} has invalid Notion memory-card path length`);
        }
        const scheduleIndex = drawSchedule.findIndex((candidate) => (
          candidate.role === entry.role && candidate.order === entry.order
        ));
        if (scheduleIndex < 0) {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} is absent from the Notion schedule`);
        }
        const sourceWidth = Number.parseFloat(edge.getAttribute("stroke-width") || "1.5");
        if (sourceWidth !== persistentStreamContract.sourceStrokeWidth) {
          throw new Error(
            `Edge ${edge.dataset.edgeId || "unknown"} source stroke width changed: `
            + `expected ${persistentStreamContract.sourceStrokeWidth}, got ${sourceWidth}`,
          );
        }
        const rawBodyStrokeWidth = Math.min(
          persistentStreamContract.bodyWidthMaximum,
          Math.max(
            persistentStreamContract.bodyWidthMinimum,
            sourceWidth * persistentStreamContract.bodyWidthMultiplier,
          ),
        );
        const bodyStrokeWidth = Number(rawBodyStrokeWidth.toFixed(persistentStreamContract.bodyWidthPrecision));
        if (bodyStrokeWidth !== persistentStreamContract.resolvedStrokeWidth) {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} Notion rail width changed`);
        }
        const motionStage = Number(edge.dataset.motionStage);
        const motionOrder = Number(edge.dataset.motionOrder);
        if (!Number.isFinite(motionStage) || !Number.isFinite(motionOrder)) {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} has invalid Notion phase metadata`);
        }
        const initialPhase = (
          (
            motionStage * persistentStreamContract.phaseStageMultiplier
            + motionOrder * persistentStreamContract.phaseOrderMultiplier
          ) % persistentStreamContract.dashPeriod
          + persistentStreamContract.dashPeriod
        ) % persistentStreamContract.dashPeriod;
        const semanticColor = persistentStreamContract.semanticColors[scheduleIndex];
        const initialNormalizedProgress = persistentStreamContract.initialNormalizedProgress[scheduleIndex];
        const availableTravel = pathLength - endpointClearance * 2;
        const initialPathDistance = endpointClearance + initialNormalizedProgress * availableTravel;

        const normalizeRotation = (rotation) => {
          const normalized = ((rotation + 180) % 360 + 360) % 360 - 180;
          return Math.abs(normalized) < 1e-9 ? 0 : normalized;
        };
        const tangentRotationAtDistance = (distance) => {
          const delta = Math.min(0.25, availableTravel / 4);
          const before = edge.getPointAtLength(Math.max(0, distance - delta));
          const after = edge.getPointAtLength(Math.min(pathLength, distance + delta));
          return normalizeRotation(Math.atan2(after.y - before.y, after.x - before.x) * 180 / Math.PI);
        };
        const initialPoint = edge.getPointAtLength(initialPathDistance);
        const initialTangentRotation = tangentRotationAtDistance(initialPathDistance);
        const sentinel = persistentStreamContract.directionSentinels[scheduleIndex];
        if (
          !sentinel
          || sentinel.role !== entry.role
          || sentinel.order !== entry.order
          || Math.abs(initialTangentRotation - sentinel.tangentRotation) > 0.001
        ) {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} Notion tangent rotation changed`);
        }

        const stream = prepareDecoration(edge, persistentStreamContract.bodyPrimitive, false);
        stream.setAttribute("fill", "none");
        stream.setAttribute("stroke", semanticColor);
        stream.setAttribute("stroke-width", String(bodyStrokeWidth));
        stream.setAttribute("stroke-linecap", "round");
        stream.setAttribute("stroke-linejoin", "round");
        stream.setAttribute("stroke-dasharray", persistentStreamContract.bodyDashPattern.join(" "));

        const card = document.createElementNS(SVG_NS, "g");
        card.setAttribute("data-graph-role", "decoration");
        card.setAttribute("data-motion-primitive", persistentStreamContract.cardPrimitive);
        card.setAttribute("data-owner", edge.dataset.edgeId || "");
        card.setAttribute(
          "transform",
          `translate(${initialPoint.x} ${initialPoint.y}) rotate(${initialTangentRotation})`,
        );
        card.setAttribute("opacity", "0");
        card.setAttribute("aria-hidden", "true");
        card.setAttribute("pointer-events", "none");

        const outer = document.createElementNS(SVG_NS, "rect");
        outer.setAttribute("x", String(persistentStreamContract.outerRect.x));
        outer.setAttribute("y", String(persistentStreamContract.outerRect.y));
        outer.setAttribute("width", String(persistentStreamContract.outerRect.width));
        outer.setAttribute("height", String(persistentStreamContract.outerRect.height));
        outer.setAttribute("rx", String(persistentStreamContract.outerRect.rx));
        outer.setAttribute("fill", persistentStreamContract.cardFill);
        outer.setAttribute("stroke", semanticColor);
        outer.setAttribute("stroke-width", String(persistentStreamContract.cardStrokeWidth));

        const inkLines = persistentStreamContract.inkLines.map((geometry) => {
          const line = document.createElementNS(SVG_NS, "line");
          Object.entries(geometry).forEach(([name, value]) => line.setAttribute(name, String(value)));
          line.setAttribute("stroke", semanticColor);
          line.setAttribute("stroke-width", String(persistentStreamContract.inkStrokeWidth));
          line.setAttribute("stroke-linecap", persistentStreamContract.inkLinecap);
          line.setAttribute("shape-rendering", persistentStreamContract.inkShapeRendering);
          return line;
        });
        card.append(outer, ...inkLines);
        motionLayer.append(stream, card);

        const decorations = [stream, card, outer, ...inkLines];
        const markerFree = decorations.every((decoration) => (
          !decoration.hasAttribute("marker-start")
          && !decoration.hasAttribute("marker-mid")
          && !decoration.hasAttribute("marker-end")
        ));
        const filterFree = decorations.every((decoration) => !decoration.hasAttribute("filter"));
        const shadowFree = decorations.every((decoration) => (
          !decoration.hasAttribute("filter") && !decoration.hasAttribute("style")
        ));
        if (!markerFree || !filterFree || !shadowFree) {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} Notion handoff must be marker/filter/shadow-free`);
        }

        effects.push((time) => {
          const frame = renderedFrameAtTime(time);
          if (frame < persistentStreamContract.start || frame > renderedFrameMax) {
            stream.setAttribute("display", "none");
            stream.setAttribute("opacity", "0");
            card.setAttribute("opacity", "0");
            return;
          }
          const liveFrame = frame - persistentStreamContract.start;
          const dashOffset = initialPhase + liveFrame * persistentStreamContract.dashOffsetPerFrame;
          const cardDistance = endpointClearance + (
            (
              initialPathDistance - endpointClearance
              + liveFrame * persistentStreamContract.cardAdvancePerFrame
            ) % availableTravel
            + availableTravel
          ) % availableTravel;
          const point = edge.getPointAtLength(cardDistance);
          const tangentRotation = tangentRotationAtDistance(cardDistance);
          if (Math.abs(tangentRotation - sentinel.tangentRotation) > 0.001) {
            throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} Notion card left its declared tangent`);
          }
          const fade = streamFadeIn(frame) * resetOpacity(frame);
          stream.setAttribute("display", "inline");
          stream.setAttribute("opacity", String(persistentStreamContract.bodyOpacity * fade));
          stream.setAttribute("stroke-dashoffset", String(dashOffset));
          card.setAttribute("transform", `translate(${point.x} ${point.y}) rotate(${tangentRotation})`);
          card.setAttribute("opacity", String(persistentStreamContract.cardOpacity * fade));
        });

        persistentStreamReports.push({
          edge_id: edge.dataset.edgeId || "",
          role: entry.role,
          schedule_key: routeKey(entry.role, entry.order, entry.stage),
          primitive: persistentStreamContract.bodyPrimitive,
          rendered_frames: [persistentStreamContract.start, renderedFrameMax],
          fade_in_frames: [persistentStreamContract.start, persistentStreamContract.start + 2],
          full_opacity_frames: [persistentStreamContract.start + 2, fullOpacityEnd],
          stroke_width: bodyStrokeWidth,
          color: semanticColor,
          source_stroke: edge.getAttribute("stroke"),
          dash_pattern: persistentStreamContract.bodyDashPattern,
          dash_period: persistentStreamContract.dashPeriod,
          dash_offset_per_rendered_frame: persistentStreamContract.dashOffsetPerFrame,
          initial_phase: initialPhase,
          phase_policy: persistentStreamContract.phasePolicy,
          motion_stage: motionStage,
          motion_order: motionOrder,
          direction: "source-to-target",
          opacity: persistentStreamContract.bodyOpacity,
          travel_easing: "linear",
          marker_free: markerFree,
          filter_free: filterFree,
        });
        notionMemoryCardReports.push({
          edge_id: edge.dataset.edgeId || "",
          role: entry.role,
          schedule_key: routeKey(entry.role, entry.order, entry.stage),
          primitive: persistentStreamContract.cardPrimitive,
          shape: "group",
          rendered_frames: [persistentStreamContract.start, renderedFrameMax],
          fade_in_frames: [persistentStreamContract.start, persistentStreamContract.start + 2],
          full_opacity_frames: [persistentStreamContract.start + 2, fullOpacityEnd],
          outer_rect: {
            ...persistentStreamContract.outerRect,
            fill: persistentStreamContract.cardFill,
            stroke: semanticColor,
            stroke_width: persistentStreamContract.cardStrokeWidth,
          },
          ink_lines: persistentStreamContract.inkLines,
          ink_stroke: semanticColor,
          ink_stroke_width: persistentStreamContract.inkStrokeWidth,
          ink_linecap: persistentStreamContract.inkLinecap,
          ink_shape_rendering: persistentStreamContract.inkShapeRendering,
          opacity: persistentStreamContract.cardOpacity,
          semantic_color: semanticColor,
          path_length: pathLength,
          endpoint_clearance: endpointClearance,
          initial_normalized_progress: initialNormalizedProgress,
          initial_path_distance: initialPathDistance,
          initial_point: { x: initialPoint.x, y: initialPoint.y },
          tangent_rotation: initialTangentRotation,
          path_advance_per_rendered_frame: persistentStreamContract.cardAdvancePerFrame,
          direction: "source-to-target",
          wrap: "target-clearance-to-source-clearance",
          animated_attributes: ["transform", "opacity"],
          motion_stage: motionStage,
          motion_order: motionOrder,
          marker_free: markerFree,
          filter_free: filterFree,
          shadow_free: shadowFree,
        });
      }

      const parseGraphBounds = (element) => {
        const values = (element.dataset.graphBounds || "").split(",").map(Number);
        if (values.length !== 4 || values.some((value) => !Number.isFinite(value))) {
          throw new Error(`Element ${element.id || element.dataset.nodeId || element.dataset.containerId || "unknown"} has invalid graph bounds`);
        }
        return { x: values[0], y: values[1], width: values[2] - values[0], height: values[3] - values[1] };
      };
      const appendMotionAttributes = (element, primitive, owner) => {
        element.setAttribute("data-graph-role", "decoration");
        element.setAttribute("data-motion-primitive", primitive);
        element.setAttribute("data-owner", owner);
        element.setAttribute("aria-hidden", "true");
        element.setAttribute("pointer-events", "none");
      };
      const tangentRotationAt = (edge, distance, pathLength) => {
        const delta = Math.min(0.75, Math.max(0.2, pathLength / 200));
        const before = edge.getPointAtLength(Math.max(0, distance - delta));
        const after = edge.getPointAtLength(Math.min(pathLength, distance + delta));
        return Math.atan2(after.y - before.y, after.x - before.x) * 180 / Math.PI;
      };
      const ensureGemFilter = () => {
        const filterId = "fireworks-motion-gem-halo";
        if (!motionLayer.querySelector(`#${filterId}`)) {
          const defs = document.createElementNS(SVG_NS, "defs");
          const filter = document.createElementNS(SVG_NS, "filter");
          filter.setAttribute("id", filterId);
          filter.setAttribute("x", "-80%");
          filter.setAttribute("y", "-80%");
          filter.setAttribute("width", "260%");
          filter.setAttribute("height", "260%");
          const blur = document.createElementNS(SVG_NS, "feGaussianBlur");
          blur.setAttribute("stdDeviation", "1.4");
          filter.append(blur);
          defs.append(filter);
          motionLayer.prepend(defs);
        }
        return filterId;
      };
      const buildSpecializedSignature = (edge, entry, color) => {
        const owner = edge.dataset.edgeId || "";
        const group = document.createElementNS(SVG_NS, "g");
        let primitive = persistentStreamContract.signaturePrimitive;
        let geometry = {};
        let rearExtent = 0;
        let forwardExtent = 0;
        let filteredElementCount = 0;

        if (persistentStreamContract.signatureKind === "glass-task-capsule") {
          primitive = "glass-task-capsule";
          const plate = document.createElementNS(SVG_NS, "rect");
          plate.setAttribute("x", "-7"); plate.setAttribute("y", "-4.5");
          plate.setAttribute("width", "14"); plate.setAttribute("height", "9"); plate.setAttribute("rx", "3");
          plate.setAttribute("fill", color); plate.setAttribute("fill-opacity", "0.30");
          plate.setAttribute("stroke", "#ffffff"); plate.setAttribute("stroke-opacity", "0.78"); plate.setAttribute("stroke-width", "1");
          plate.setAttribute("data-motion-component", "translucent-plate");
          const highlight = document.createElementNS(SVG_NS, "line");
          highlight.setAttribute("x1", "-4"); highlight.setAttribute("y1", "-2.7");
          highlight.setAttribute("x2", "4"); highlight.setAttribute("y2", "-2.7");
          highlight.setAttribute("stroke", "#ffffff"); highlight.setAttribute("stroke-width", "1"); highlight.setAttribute("stroke-opacity", "0.85");
          highlight.setAttribute("data-motion-component", "white-highlight");
          const dots = [-2.5, 2.5].map((x) => {
            const dot = document.createElementNS(SVG_NS, "circle");
            dot.setAttribute("cx", String(x)); dot.setAttribute("cy", "1.2"); dot.setAttribute("r", "2");
            dot.setAttribute("fill", color); dot.setAttribute("stroke", "#ffffff"); dot.setAttribute("stroke-width", "0.6");
            dot.setAttribute("data-motion-component", "work-item-dot");
            return dot;
          });
          group.append(plate, highlight, ...dots);
          geometry = { shape: "rounded-translucent-plate", width: 14, height: 9, rx: 3, highlight_stroke_width: 1, work_item_dot_radius: 2, work_item_dot_count: 2 };
          rearExtent = 7; forwardExtent = 7;
        } else if (persistentStreamContract.signatureKind === "policy-seal") {
          primitive = "policy-seal";
          const hexagon = document.createElementNS(SVG_NS, "polygon");
          hexagon.setAttribute("points", "0,-6 5.2,-3 5.2,3 0,6 -5.2,3 -5.2,-3");
          hexagon.setAttribute("fill", "#fffaf0"); hexagon.setAttribute("fill-opacity", "0.30");
          hexagon.setAttribute("stroke", "#fffaf0"); hexagon.setAttribute("stroke-width", "1.2");
          hexagon.setAttribute("data-motion-component", "seal-hexagon");
          const dot = document.createElementNS(SVG_NS, "circle");
          dot.setAttribute("cx", "0"); dot.setAttribute("cy", "-1.2"); dot.setAttribute("r", "1.5"); dot.setAttribute("fill", color);
          dot.setAttribute("data-motion-component", "seal-center-dot");
          const bar = document.createElementNS(SVG_NS, "line");
          bar.setAttribute("x1", "-2"); bar.setAttribute("y1", "3"); bar.setAttribute("x2", "2"); bar.setAttribute("y2", "3");
          bar.setAttribute("stroke", color); bar.setAttribute("stroke-width", "1.4"); bar.setAttribute("stroke-linecap", "round");
          bar.setAttribute("data-motion-component", "approval-bar");
          group.append(hexagon, dot, bar);
          geometry = { shape: "warm-white-hexagonal-outline", width: 12, height: 12, center_dot_diameter: 3, approval_bar_width: 4, shadow: false, glow: false };
          rearExtent = 6; forwardExtent = 6;
        } else if (persistentStreamContract.signatureKind === "token-train") {
          primitive = "token-train";
          const opacities = [1, 0.72, 0.44];
          [-8, -2, 4].forEach((x, index) => {
            const cell = document.createElementNS(SVG_NS, "rect");
            cell.setAttribute("x", String(x)); cell.setAttribute("y", "-2"); cell.setAttribute("width", "4"); cell.setAttribute("height", "4"); cell.setAttribute("rx", "1");
            cell.setAttribute("fill", color); cell.setAttribute("fill-opacity", String(opacities[index]));
            cell.setAttribute("data-motion-component", `token-cell-${index + 1}`);
            group.append(cell);
          });
          geometry = { shape: "three-cell-token-train", group_width: 18, group_height: 8, cell_width: 4, cell_height: 4, cell_gap: 2, cell_opacities: opacities };
          rearExtent = 8; forwardExtent = 8;
        } else if (persistentStreamContract.signatureKind === "gem-tracer") {
          primitive = "gem-tracer";
          const tail = document.createElementNS(SVG_NS, "polygon");
          tail.setAttribute("points", "-4,0 -16,-2 -16,2"); tail.setAttribute("fill", color); tail.setAttribute("fill-opacity", "0.55");
          tail.setAttribute("data-motion-component", "tapered-tail");
          const halo = document.createElementNS(SVG_NS, "rect");
          halo.setAttribute("x", "-3.5"); halo.setAttribute("y", "-3.5"); halo.setAttribute("width", "7"); halo.setAttribute("height", "7");
          halo.setAttribute("transform", "rotate(45)"); halo.setAttribute("fill", color); halo.setAttribute("fill-opacity", "0.34");
          halo.setAttribute("filter", `url(#${ensureGemFilter()})`); halo.setAttribute("data-motion-component", "diamond-halo");
          const diamond = document.createElementNS(SVG_NS, "rect");
          diamond.setAttribute("x", "-3.5"); diamond.setAttribute("y", "-3.5"); diamond.setAttribute("width", "7"); diamond.setAttribute("height", "7");
          diamond.setAttribute("transform", "rotate(45)"); diamond.setAttribute("fill", color); diamond.setAttribute("stroke", "#fde68a"); diamond.setAttribute("stroke-width", "0.8");
          diamond.setAttribute("data-motion-component", "gem-diamond");
          const specular = document.createElementNS(SVG_NS, "circle");
          specular.setAttribute("cx", "1"); specular.setAttribute("cy", "-1"); specular.setAttribute("r", "1"); specular.setAttribute("fill", "#ffffff");
          specular.setAttribute("data-motion-component", "specular-point");
          group.append(tail, halo, diamond, specular);
          geometry = { shape: "diamond-with-tapered-tail", diamond_width: 7, diamond_height: 7, diamond_rotation: 45, specular_diameter: 2, tail_length: 12 };
          rearExtent = 16; forwardExtent = 5; filteredElementCount = 1;
        } else if (persistentStreamContract.signatureKind === "review-cursor") {
          primitive = "review-cursor";
          const circle = document.createElementNS(SVG_NS, "circle");
          circle.setAttribute("cx", "0"); circle.setAttribute("cy", "0"); circle.setAttribute("r", "5.5"); circle.setAttribute("fill", "none");
          circle.setAttribute("stroke", color); circle.setAttribute("stroke-width", "1.4"); circle.setAttribute("data-motion-component", "cursor-circle");
          const handle = document.createElementNS(SVG_NS, "line");
          handle.setAttribute("x1", "3.9"); handle.setAttribute("y1", "3.9"); handle.setAttribute("x2", "7.44"); handle.setAttribute("y2", "7.44");
          handle.setAttribute("stroke", color); handle.setAttribute("stroke-width", "1.4"); handle.setAttribute("stroke-linecap", "round"); handle.setAttribute("data-motion-component", "cursor-handle");
          const check = document.createElementNS(SVG_NS, "polyline");
          check.setAttribute("points", "-2,0 -0.5,1.5 2,-1.5"); check.setAttribute("fill", "none"); check.setAttribute("stroke", color); check.setAttribute("stroke-width", "1.2");
          check.setAttribute("stroke-linecap", "round"); check.setAttribute("stroke-linejoin", "round"); check.setAttribute("data-motion-component", "cursor-check");
          group.append(circle, handle, check);
          geometry = { shape: "review-mark", outline_circle_diameter: 11, diagonal_handle_length: 5, internal_check_extent: 3 };
          rearExtent = 6; forwardExtent = 8;
        } else if (persistentStreamContract.signatureKind === "cloud-flow") {
          if (entry.role === "cross-region") {
            primitive = "replication-capsule";
            const capsule = document.createElementNS(SVG_NS, "rect");
            capsule.setAttribute("x", "-7"); capsule.setAttribute("y", "-3.5"); capsule.setAttribute("width", "14"); capsule.setAttribute("height", "7"); capsule.setAttribute("rx", "3.5");
            capsule.setAttribute("fill", "#ffffff"); capsule.setAttribute("fill-opacity", "0.92"); capsule.setAttribute("stroke", color); capsule.setAttribute("stroke-width", "1.2");
            capsule.setAttribute("data-motion-component", "replication-outline");
            [-2.5, 2.5].forEach((x) => {
              const cell = document.createElementNS(SVG_NS, "circle");
              cell.setAttribute("cx", String(x)); cell.setAttribute("cy", "0"); cell.setAttribute("r", "1.5"); cell.setAttribute("fill", "#7c3aed");
              cell.setAttribute("data-motion-component", "replication-data-cell"); group.append(cell);
            });
            group.prepend(capsule);
            geometry = { shape: "replication-capsule", width: 14, height: 7, data_cell_count: 2, direction: "left-to-right" };
            rearExtent = 7; forwardExtent = 7;
          } else {
            primitive = "region-chevron-pair";
            [-2.5, 2.5].forEach((center) => {
              const chevron = document.createElementNS(SVG_NS, "path");
              chevron.setAttribute("d", `M ${center - 3},-2.5 L ${center},0 L ${center - 3},2.5`);
              chevron.setAttribute("fill", "none"); chevron.setAttribute("stroke", color); chevron.setAttribute("stroke-width", "1.6");
              chevron.setAttribute("stroke-linecap", "round"); chevron.setAttribute("stroke-linejoin", "round"); chevron.setAttribute("data-motion-component", "region-chevron");
              group.append(chevron);
            });
            geometry = { shape: "region-chevron-pair", chevron_width: 6, chevron_height: 5, separation: 5 };
            rearExtent = 6; forwardExtent = 3;
          }
        } else if (persistentStreamContract.signatureKind === "event-transit") {
          if (entry.role === "topic-rail") {
            primitive = "event-train";
            [-8, 0, 8].forEach((x) => {
              const car = document.createElementNS(SVG_NS, "circle");
              car.setAttribute("cx", String(x)); car.setAttribute("cy", "0"); car.setAttribute("r", "2.5"); car.setAttribute("fill", color);
              car.setAttribute("stroke", "#ffffff"); car.setAttribute("stroke-width", "0.6"); car.setAttribute("data-motion-component", "event-car"); group.append(car);
            });
            geometry = { shape: "three-car-event-train", car_diameter: 5, car_gap: 3, car_count: 3 };
            rearExtent = 10.5; forwardExtent = 10.5;
          } else if (entry.role === "dead-letter") {
            primitive = "exception-car";
            const exception = document.createElementNS(SVG_NS, "circle");
            exception.setAttribute("cx", "0"); exception.setAttribute("cy", "0"); exception.setAttribute("r", "4"); exception.setAttribute("fill", "#fff7f7");
            exception.setAttribute("stroke", "#c62828"); exception.setAttribute("stroke-width", "1.4"); exception.setAttribute("data-motion-component", "exception-outline");
            const mark = document.createElementNS(SVG_NS, "path");
            mark.setAttribute("d", "M 0,-2 L 0,1 M 0,2.5 L 0,2.6"); mark.setAttribute("stroke", "#c62828"); mark.setAttribute("stroke-width", "1.2"); mark.setAttribute("stroke-linecap", "round");
            mark.setAttribute("data-motion-component", "exception-mark"); group.append(exception, mark);
            geometry = { shape: "red-outlined-exception-car", diameter: 8 };
            rearExtent = 4; forwardExtent = 4;
          } else {
            primitive = "projection-car";
            [-6.5, 1.5].forEach((x) => {
              const cell = document.createElementNS(SVG_NS, "rect");
              cell.setAttribute("x", String(x)); cell.setAttribute("y", "-2.5"); cell.setAttribute("width", "5"); cell.setAttribute("height", "5"); cell.setAttribute("rx", "1.2");
              cell.setAttribute("fill", "#00897b"); cell.setAttribute("stroke", "#ffffff"); cell.setAttribute("stroke-width", "0.6"); cell.setAttribute("data-motion-component", "projection-cell"); group.append(cell);
            });
            geometry = { shape: "teal-two-cell-projection-car", cell_count: 2, cell_width: 5, cell_gap: 3 };
            rearExtent = 6.5; forwardExtent = 6.5;
          }
        } else if (persistentStreamContract.signatureKind === "ops-pulse") {
          if (entry.role === "critical-request") {
            primitive = "ecg-head";
            const ecg = document.createElementNS(SVG_NS, "polyline");
            ecg.setAttribute("points", "-10,0 -6,0 -4,-3.5 -1,3.5 2,-4.5 5,0 10,0"); ecg.setAttribute("fill", "none");
            ecg.setAttribute("stroke", "#fde68a"); ecg.setAttribute("stroke-width", "1.6"); ecg.setAttribute("stroke-linecap", "round"); ecg.setAttribute("stroke-linejoin", "round");
            ecg.setAttribute("data-motion-component", "ecg-waveform"); group.append(ecg);
            geometry = { shape: "compact-ecg-head", width: 20, stroke_width: 1.6 };
            rearExtent = 10; forwardExtent = 10;
          } else {
            primitive = "telemetry-export-packet";
            [-6, 0, 6].forEach((x, index) => {
              const dot = document.createElementNS(SVG_NS, "circle");
              dot.setAttribute("cx", String(x)); dot.setAttribute("cy", "0"); dot.setAttribute("r", "2"); dot.setAttribute("fill", "#22d3ee");
              dot.setAttribute("fill-opacity", String([0.48, 0.72, 1][index])); dot.setAttribute("data-motion-component", "telemetry-dot"); group.append(dot);
            });
            geometry = { shape: "cyan-three-dot-export-packet", dot_count: 3, dot_diameter: 4 };
            rearExtent = 8; forwardExtent = 8;
          }
        } else {
          throw new Error(`${selectedSceneContract.name} has no specialized signature builder`);
        }

        appendMotionAttributes(group, primitive, owner);
        group.setAttribute("opacity", "0");
        return { group, primitive, geometry, rearExtent, forwardExtent, filteredElementCount };
      };

      function addSpecializedLiveSignature(edge, entry) {
        if (typeof edge.getTotalLength !== "function" || typeof edge.getPointAtLength !== "function") {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} does not support specialized live travel`);
        }
        const pathLength = edge.getTotalLength();
        const sourceWidth = Number.parseFloat(edge.getAttribute("stroke-width") || "1.5");
        const bodyStrokeWidth = Number(Math.min(persistentStreamContract.bodyWidthMaximum, sourceWidth).toFixed(2));
        const motionStage = Number(edge.dataset.motionStage);
        const motionOrder = Number(edge.dataset.motionOrder);
        const initialPhase = (
          motionStage * persistentStreamContract.phaseStageMultiplier
          + motionOrder * persistentStreamContract.phaseOrderMultiplier
        ) % persistentStreamContract.dashPeriod;
        const sourceColor = edge.getAttribute("stroke") || "#64748b";
        const bodyPrimitive = selectedSceneContract.styleId === 12
          ? (entry.role === "critical-request" ? "incident-pulse-rail" : "telemetry-export-rail")
          : persistentStreamContract.bodyPrimitive;
        const stream = prepareDecoration(edge, bodyPrimitive, false);
        stream.setAttribute("fill", "none"); stream.setAttribute("stroke", sourceColor); stream.setAttribute("stroke-width", String(bodyStrokeWidth));
        stream.setAttribute("stroke-linecap", "round"); stream.setAttribute("stroke-linejoin", "round");
        stream.setAttribute("stroke-dasharray", persistentStreamContract.bodyDashPattern.join(" "));
        const signature = buildSpecializedSignature(edge, entry, sourceColor);
        const startDistance = persistentStreamContract.endpointClearance + signature.rearExtent;
        const endDistance = pathLength - persistentStreamContract.endpointClearance - signature.forwardExtent;
        const availableTravel = endDistance - startDistance;
        if (!Number.isFinite(pathLength) || availableTravel <= 0) {
          throw new Error(`Edge ${edge.dataset.edgeId || "unknown"} is too short for ${signature.primitive} clearance`);
        }
        const initialDistance = startDistance + (initialPhase % availableTravel);
        const initialPoint = edge.getPointAtLength(initialDistance);
        const initialRotation = tangentRotationAt(edge, initialDistance, pathLength);
        signature.group.setAttribute("transform", `translate(${initialPoint.x} ${initialPoint.y}) rotate(${initialRotation})`);
        motionLayer.append(stream, signature.group);

        effects.push((time) => {
          const frame = renderedFrameAtTime(time);
          if (frame < persistentStreamContract.start || frame > renderedFrameMax) {
            stream.setAttribute("display", "none"); stream.setAttribute("opacity", "0"); signature.group.setAttribute("opacity", "0");
            return;
          }
          const liveFrame = frame - persistentStreamContract.start;
          const dashOffset = initialPhase + liveFrame * persistentStreamContract.dashOffsetPerFrame;
          const distance = startDistance + (
            (initialDistance - startDistance + liveFrame * persistentStreamContract.advancePerFrame) % availableTravel
            + availableTravel
          ) % availableTravel;
          const point = edge.getPointAtLength(distance);
          const rotation = tangentRotationAt(edge, distance, pathLength);
          const fade = streamFadeIn(frame) * resetOpacity(frame);
          stream.setAttribute("display", "inline"); stream.setAttribute("opacity", String(persistentStreamContract.bodyOpacity * fade));
          stream.setAttribute("stroke-dashoffset", String(dashOffset));
          signature.group.setAttribute("transform", `translate(${point.x} ${point.y}) rotate(${rotation})`);
          signature.group.setAttribute("opacity", String(0.98 * fade));
        });

        const scheduleKey = routeKey(entry.role, entry.order, entry.stage);
        persistentStreamReports.push({
          edge_id: edge.dataset.edgeId || "", role: entry.role, schedule_key: scheduleKey,
          primitive: bodyPrimitive, rendered_frames: [persistentStreamContract.start, renderedFrameMax],
          fade_in_frames: [persistentStreamContract.start, persistentStreamContract.start + 2],
          full_opacity_frames: [persistentStreamContract.start + 2, fullOpacityEnd],
          stroke_width: bodyStrokeWidth, maximum_live_width: persistentStreamContract.bodyWidthMaximum,
          source_stroke_width: sourceWidth, dynamic_not_thicker_than_source: bodyStrokeWidth <= sourceWidth,
          color: sourceColor, dash_pattern: persistentStreamContract.bodyDashPattern,
          dash_period: persistentStreamContract.dashPeriod, dash_offset_per_rendered_frame: persistentStreamContract.dashOffsetPerFrame,
          initial_phase: initialPhase, phase_policy: persistentStreamContract.phasePolicy,
          motion_stage: motionStage, motion_order: motionOrder, direction: "source-to-target",
          opacity: persistentStreamContract.bodyOpacity, travel_easing: "linear", marker_free: true, filter_free: true,
        });
        specializedSignatureReports.push({
          edge_id: edge.dataset.edgeId || "", role: entry.role, schedule_key: scheduleKey,
          primitive: signature.primitive, signature_family: persistentStreamContract.signatureKind,
          geometry: signature.geometry, rendered_frames: [persistentStreamContract.start, renderedFrameMax],
          fade_in_frames: [persistentStreamContract.start, persistentStreamContract.start + 2],
          full_opacity_frames: [persistentStreamContract.start + 2, fullOpacityEnd],
          path_length: pathLength, endpoint_clearance: persistentStreamContract.endpointClearance,
          geometry_rear_extent: signature.rearExtent, geometry_forward_extent: signature.forwardExtent,
          center_travel_range: [startDistance, endDistance], initial_path_distance: initialDistance,
          initial_point: { x: initialPoint.x, y: initialPoint.y }, initial_tangent_rotation: initialRotation,
          path_advance_per_rendered_frame: persistentStreamContract.advancePerFrame,
          travel_pixels_per_rendered_frame_at_50_percent: persistentStreamContract.advancePerFrame / 2,
          direction: "source-to-target", wrap: "target-clearance-to-source-clearance",
          animated_attributes: ["transform", "opacity"], source_color: sourceColor,
          marker_free: true, filtered_element_count: signature.filteredElementCount,
          filter_boundary_valid: signature.filteredElementCount <= 1,
          appended_below_labels_and_nodes: true,
        });
      }

      const fixedPulseOpacity = (frame, period, minimum, maximum) => {
        const phase = ((frame - persistentStreamContract.start) % period + period) % period;
        const wave = 0.5 - 0.5 * Math.cos(2 * Math.PI * phase / period);
        return minimum + (maximum - minimum) * wave;
      };
      const addFixedNodeHalo = (config) => {
        const matches = nodes.filter((node) => node.dataset.nodeId === config.nodeId);
        if (matches.length !== 1) {
          throw new Error(`${selectedSceneContract.name} requires node ${config.nodeId} for its halo`);
        }
        const bounds = parseGraphBounds(matches[0]);
        const halo = document.createElementNS(SVG_NS, "rect");
        appendMotionAttributes(halo, config.primitive, config.nodeId);
        halo.setAttribute("x", String(bounds.x - 4)); halo.setAttribute("y", String(bounds.y - 4));
        halo.setAttribute("width", String(bounds.width + 8)); halo.setAttribute("height", String(bounds.height + 8)); halo.setAttribute("rx", "10");
        halo.setAttribute("fill", "none"); halo.setAttribute("stroke", selectedSceneContract.styleId === 12 ? "#f59e0b" : "#ffffff");
        halo.setAttribute("stroke-width", "2"); halo.setAttribute("opacity", "0");
        motionLayer.append(halo);
        effects.push((time) => {
          const frame = renderedFrameAtTime(time);
          if (frame < persistentStreamContract.start) { halo.setAttribute("opacity", "0"); return; }
          const opacity = fixedPulseOpacity(frame, config.periodFrames, config.minimumOpacity, config.maximumOpacity)
            * streamFadeIn(frame) * resetOpacity(frame);
          halo.setAttribute("opacity", String(opacity));
        });
        auxiliaryReports.push({
          primitive: config.primitive, node_id: config.nodeId, count: 1,
          movement: "opacity-only", animated_attributes: ["opacity"], period_frames: config.periodFrames,
          opacity_range: [config.minimumOpacity, config.maximumOpacity], geometry: { x: bounds.x - 4, y: bounds.y - 4, width: bounds.width + 8, height: bounds.height + 8 },
          below_node: true, source_geometry_mutated: false,
        });
      };
      const addContainerPairPulse = (config) => {
        const reports = [];
        config.containerIds.forEach((containerId) => {
          const container = root.querySelector(`[data-graph-role="container"][data-container-id="${containerId}"]`);
          if (!container) throw new Error(`${selectedSceneContract.name} requires container ${containerId}`);
          const sourceBoundary = container.querySelector("rect");
          if (!sourceBoundary) throw new Error(`${selectedSceneContract.name} container ${containerId} has no boundary`);
          const pulse = sourceBoundary.cloneNode(false);
          removeCloneIdentity(pulse); appendMotionAttributes(pulse, config.primitive, containerId);
          pulse.setAttribute("fill", "none"); pulse.setAttribute("opacity", "0"); pulse.removeAttribute("filter");
          motionLayer.append(pulse);
          effects.push((time) => {
            const frame = renderedFrameAtTime(time);
            if (frame < persistentStreamContract.start) { pulse.setAttribute("opacity", "0"); return; }
            const opacity = fixedPulseOpacity(frame, config.periodFrames, config.minimumOpacity, config.maximumOpacity)
              * streamFadeIn(frame) * resetOpacity(frame);
            pulse.setAttribute("opacity", String(opacity));
          });
          reports.push({
            container_id: containerId, geometry_attributes: ["x", "y", "width", "height", "rx"].reduce((result, name) => ({ ...result, [name]: pulse.getAttribute(name) }), {}),
            stroke_width: pulse.getAttribute("stroke-width"), source_stroke_width: sourceBoundary.getAttribute("stroke-width"), geometry_and_stroke_unchanged: pulse.getAttribute("stroke-width") === sourceBoundary.getAttribute("stroke-width"),
          });
        });
        auxiliaryReports.push({ primitive: config.primitive, count: reports.length, movement: "opacity-only", animated_attributes: ["opacity"], phase_locked: true, period_frames: config.periodFrames, opacity_range: [config.minimumOpacity, config.maximumOpacity], containers: reports, source_geometry_mutated: false });
      };
      const addStationDwellRings = (config) => {
        const mainEntries = drawSchedule.filter((entry) => entry.role === "topic-rail");
        const reports = [];
        mainEntries.forEach((entry) => {
          const edge = edgeForEntry(entry);
          const target = nodes.find((node) => node.dataset.nodeId === edge.dataset.target);
          if (!target) throw new Error(`Transit target ${edge.dataset.target || "unknown"} is unavailable`);
          const bounds = parseGraphBounds(target);
          const ring = document.createElementNS(SVG_NS, "rect");
          appendMotionAttributes(ring, config.primitive, edge.dataset.target || "");
          ring.setAttribute("x", String(bounds.x - 4)); ring.setAttribute("y", String(bounds.y - 4));
          ring.setAttribute("width", String(bounds.width + 8)); ring.setAttribute("height", String(bounds.height + 8)); ring.setAttribute("rx", "10");
          ring.setAttribute("fill", "none"); ring.setAttribute("stroke", edge.getAttribute("stroke") || "#e4475b"); ring.setAttribute("stroke-width", "1.2"); ring.setAttribute("opacity", "0");
          motionLayer.append(ring);
          effects.push((time) => {
            const frame = renderedFrameAtTime(time);
            const elapsed = frame - entry.end;
            const opacity = elapsed >= 0 && elapsed < config.periodFrames
              ? Math.sin(Math.PI * (elapsed + 1) / (config.periodFrames + 1)) * 0.32 * resetOpacity(frame)
              : 0;
            ring.setAttribute("opacity", String(opacity));
          });
          reports.push({ edge_id: edge.dataset.edgeId || "", target_node_id: edge.dataset.target || "", arrival_frame: entry.end, period_frames: config.periodFrames, movement: "opacity-only", geometry_expansion_per_frame: 0, outside_node_border: true, fixed_geometry: { x: bounds.x - 4, y: bounds.y - 4, width: bounds.width + 8, height: bounds.height + 8 } });
        });
        auxiliaryReports.push({ primitive: config.primitive, count: reports.length, rings: reports, source_geometry_mutated: false });
      };
      const addOpsScannerAndTraceReveal = () => {
        const spansById = new Map(nodes.filter((node) => node.dataset.spanId).map((node) => [node.dataset.spanId, node]));
        const schedule = [
          { id: "span-root", start: 24, end: 27 }, { id: "span-api", start: 27, end: 30 },
          { id: "span-checkout", start: 30, end: 33 }, { id: "span-payment", start: 33, end: 36 },
        ];
        if (schedule.some((entry) => !spansById.has(entry.id))) throw new Error("Ops Pulse trace-span source set changed");
        edgeHidingStyle.textContent += '[data-graph-role="node"][data-span-id]:not([data-span-id=""]){visibility:hidden!important}';
        transientTraceSpanSources.push(...schedule.map((entry) => spansById.get(entry.id)));
        const spanBounds = schedule.map((entry) => parseGraphBounds(spansById.get(entry.id)));
        const plot = {
          x: Math.min(...spanBounds.map((bounds) => bounds.x)),
          y: Math.min(...spanBounds.map((bounds) => bounds.y)),
          x2: Math.max(...spanBounds.map((bounds) => bounds.x + bounds.width)),
          y2: Math.max(...spanBounds.map((bounds) => bounds.y + bounds.height)),
        };
        const defs = document.createElementNS(SVG_NS, "defs");
        const scannerClip = document.createElementNS(SVG_NS, "clipPath");
        scannerClip.setAttribute("id", "fireworks-ops-scanner-clip");
        const scannerClipRect = document.createElementNS(SVG_NS, "rect");
        scannerClipRect.setAttribute("x", String(plot.x)); scannerClipRect.setAttribute("y", String(plot.y));
        scannerClipRect.setAttribute("width", String(plot.x2 - plot.x)); scannerClipRect.setAttribute("height", String(plot.y2 - plot.y)); scannerClip.append(scannerClipRect); defs.append(scannerClip);
        motionLayer.append(defs);
        const scanner = document.createElementNS(SVG_NS, "g");
        appendMotionAttributes(scanner, "waterfall-scanner", "trace-waterfall"); scanner.setAttribute("clip-path", "url(#fireworks-ops-scanner-clip)"); scanner.setAttribute("opacity", "0");
        const tail = document.createElementNS(SVG_NS, "rect");
        tail.setAttribute("x", "-12"); tail.setAttribute("y", String(plot.y)); tail.setAttribute("width", "12"); tail.setAttribute("height", String(plot.y2 - plot.y)); tail.setAttribute("fill", "#22d3ee"); tail.setAttribute("fill-opacity", "0.14"); tail.setAttribute("data-motion-component", "scanner-tail");
        const line = document.createElementNS(SVG_NS, "line");
        line.setAttribute("x1", "0"); line.setAttribute("x2", "0"); line.setAttribute("y1", String(plot.y)); line.setAttribute("y2", String(plot.y2)); line.setAttribute("stroke", "#67e8f9"); line.setAttribute("stroke-width", "2"); line.setAttribute("data-motion-component", "scanner-line");
        scanner.append(tail, line); motionLayer.append(scanner);
        effects.push((time) => {
          const frame = renderedFrameAtTime(time);
          if (frame < persistentStreamContract.start) { scanner.setAttribute("opacity", "0"); return; }
          const liveFrame = frame - persistentStreamContract.start;
          const progress = ((liveFrame % 34) + 34) % 34 / 33;
          const x = plot.x + progress * (plot.x2 - plot.x);
          scanner.setAttribute("transform", `translate(${x} 0)`);
          scanner.setAttribute("opacity", String(streamFadeIn(frame) * resetOpacity(frame)));
        });
        waterfallScannerReport = { primitive: "waterfall-scanner", width: 2, tail_width: 12, period_frames: 34, plot_bounds: [plot.x, plot.y, plot.x2, plot.y2], contained_by: "trace-waterfall", below_span_labels: true, movement: "horizontal-within-trace-plot", animated_attributes: ["transform", "opacity"] };

        schedule.forEach((entry, index) => {
          const source = spansById.get(entry.id);
          const bounds = spanBounds[index];
          const clip = document.createElementNS(SVG_NS, "clipPath");
          const clipId = `fireworks-trace-reveal-${index}`; clip.setAttribute("id", clipId);
          const clipRect = document.createElementNS(SVG_NS, "rect");
          clipRect.setAttribute("x", String(bounds.x)); clipRect.setAttribute("y", String(bounds.y)); clipRect.setAttribute("width", "0"); clipRect.setAttribute("height", String(bounds.height)); clip.append(clipRect); defs.append(clip);
          const clone = source.cloneNode(true);
          for (const element of [clone, ...clone.querySelectorAll("*")]) removeCloneIdentity(element);
          appendMotionAttributes(clone, "trace-span-reveal", entry.id); clone.setAttribute("clip-path", `url(#${clipId})`); clone.setAttribute("display", "none"); clone.setAttribute("opacity", "0"); motionLayer.append(clone);
          effects.push((time) => {
            const frame = renderedFrameAtTime(time);
            if (frame < entry.start) { clone.setAttribute("display", "none"); clone.setAttribute("opacity", "0"); clipRect.setAttribute("width", "0"); return; }
            const progress = frame >= entry.end ? 1 : clamped((frame - entry.start) / (entry.end - entry.start));
            clone.setAttribute("display", "inline"); clone.setAttribute("opacity", String(resetOpacity(frame))); clipRect.setAttribute("width", String(bounds.width * progress));
          });
          traceSpanRevealReports.push({ span_id: entry.id, parent_span: source.dataset.parentSpan || null, rendered_frames: [entry.start, entry.end], source_geometry: bounds, reveal: "left-to-right-clip", source_transiently_hidden: true, source_geometry_mutated: false, clone_geometry_immutable: true });
        });
      };
      const addSpecializedAuxiliaries = () => {
        if (selectedSceneContract.styleId === 12) {
          addOpsScannerAndTraceReveal();
        }
        const config = persistentStreamContract.auxiliary;
        if (!config) return;
        if (config.kind === "node-halo" || config.kind === "ops-pulse") addFixedNodeHalo(config);
        else if (config.kind === "container-pair-pulse") addContainerPairPulse(config);
        else if (config.kind === "station-dwell-rings") addStationDwellRings(config);
      };

      const addSettledOwnerDecorations = () => {
        if (![11, 12].includes(selectedSceneContract.styleId)) return;
        const casingLayer = document.createElementNS(SVG_NS, "g");
        casingLayer.setAttribute("data-graph-role", "decoration");
        casingLayer.setAttribute("data-motion-layer", "settled-route-casings");
        casingLayer.setAttribute("aria-hidden", "true");
        casingLayer.setAttribute("pointer-events", "none");
        settledOwnerDirectionLayer = document.createElementNS(SVG_NS, "g");
        settledOwnerDirectionLayer.setAttribute("data-graph-role", "decoration");
        settledOwnerDirectionLayer.setAttribute("data-motion-layer", "settled-route-directions");
        settledOwnerDirectionLayer.setAttribute("aria-hidden", "true");
        settledOwnerDirectionLayer.setAttribute("pointer-events", "none");
        const ownerRole = selectedSceneContract.styleId === 11 ? "topic-rail" : "critical-request";
        const expectedSourceCount = selectedSceneContract.styleId === 11 ? 8 : 9;
        const expectedPerOwner = selectedSceneContract.styleId === 11 ? 2 : 3;
        const mainEntries = drawSchedule.filter((entry) => entry.role === ownerRole);
        const expectedOwners = new Set(mainEntries.map((entry) => edgeForEntry(entry).dataset.edgeId || ""));
        const decorationsByOwner = new Map();
        routeOwnerDecorations.forEach((decoration) => {
          const owner = decoration.dataset.owner || "";
          const matches = decorationsByOwner.get(owner) || [];
          matches.push(decoration);
          decorationsByOwner.set(owner, matches);
        });
        if (
          routeOwnerDecorations.length !== expectedSourceCount
          || decorationsByOwner.size !== expectedOwners.size
          || [...decorationsByOwner.keys()].some((owner) => !expectedOwners.has(owner))
          || [...expectedOwners].some((owner) => (
            decorationsByOwner.get(owner) || []
          ).length !== expectedPerOwner)
        ) {
          throw new Error(`${selectedSceneContract.name} route-owned decoration source set changed`);
        }
        mainEntries.forEach((entry) => {
          const edge = edgeForEntry(entry);
          const owner = edge.dataset.edgeId || "";
          const sources = decorationsByOwner.get(owner) || [];
          const clones = sources.map((source) => {
            const clone = source.cloneNode(true);
            for (const element of [clone, ...clone.querySelectorAll("*")]) removeCloneIdentity(element);
            appendMotionAttributes(clone, "settled-owner-decoration", owner);
            clone.setAttribute("display", "none");
            clone.setAttribute("opacity", "0");
            const sourceOpacity = source.hasAttribute("opacity")
              ? Number(source.getAttribute("opacity"))
              : 1;
            if (!Number.isFinite(sourceOpacity) || sourceOpacity < 0 || sourceOpacity > 1) {
              throw new Error(`${selectedSceneContract.name} owner decoration ${source.id || owner} has invalid opacity`);
            }
            const cloneLayer = source.id.endsWith("-rail-casing")
              || source.id.endsWith("-critical-glow")
              ? casingLayer
              : settledOwnerDirectionLayer;
            cloneLayer.append(clone);
            settledOwnerDecorationClones.push(clone);
            return { clone, source, sourceOpacity };
          });
          effects.push((time) => {
            const frame = renderedFrameAtTime(time);
            clones.forEach(({ clone, sourceOpacity }) => {
              if (frame < entry.end) {
                clone.setAttribute("display", "none");
                clone.setAttribute("opacity", "0");
                return;
              }
              clone.setAttribute("display", "inline");
              clone.setAttribute("opacity", String(sourceOpacity * resetOpacity(frame)));
            });
          });
          settledOwnerDecorationReports.push({
            edge_id: owner,
            settle_frame: entry.end,
            source_ids: sources.map((source) => source.id),
            clone_count: clones.length,
            hidden_before_settle: true,
            source_geometry_mutated: false,
          });
        });
        motionLayer.append(casingLayer);
      };

      function addRouteLabel(label, edge, entry) {
        const labelClone = label.cloneNode(true);
        for (const element of [labelClone, ...labelClone.querySelectorAll("*")]) {
          for (const attribute of Array.from(element.attributes)) {
            if (attribute.name === "id" || attribute.name.startsWith("data-")) {
              element.removeAttribute(attribute.name);
            }
          }
        }
        labelClone.setAttribute("data-graph-role", "decoration");
        labelClone.setAttribute("data-motion-primitive", "route-label-arrival");
        labelClone.setAttribute("data-owner", edge.dataset.edgeId || "");
        labelClone.setAttribute("aria-hidden", "true");
        labelClone.setAttribute("pointer-events", "none");
        labelClone.setAttribute("display", "none");
        labelClone.setAttribute("opacity", "0");
        labelLayer.append(labelClone);
        effects.push((time) => {
          const frame = renderedFrameAtTime(time);
          if (frame < entry.end) {
            labelClone.setAttribute("display", "none");
            labelClone.setAttribute("opacity", "0");
            return;
          }
          labelClone.setAttribute("display", "inline");
          labelClone.setAttribute("opacity", String(resetOpacity(frame)));
        });
      }

      function addTerminalPromptCursor(contract) {
        const terminalNodes = nodes.filter((node) => node.dataset.nodeId === contract.nodeId);
        if (terminalNodes.length !== 1) {
          throw new Error(`${selectedSceneContract.name} requires exactly one terminal node`);
        }
        const sourceTexts = Array.from(terminalNodes[0].querySelectorAll("text"))
          .filter((text) => (text.textContent || "").trim() === contract.sourceText);
        if (sourceTexts.length !== 1) {
          throw new Error(`${selectedSceneContract.name} requires exactly one terminal prompt underscore`);
        }
        const sourceText = sourceTexts[0];
        const bounds = sourceText.getBBox();
        if (
          !Number.isFinite(bounds.x)
          || !Number.isFinite(bounds.y)
          || !Number.isFinite(bounds.width)
          || !Number.isFinite(bounds.height)
          || bounds.width <= 0
          || bounds.height < contract.height
        ) {
          throw new Error(`${selectedSceneContract.name} terminal prompt underscore has invalid bounds`);
        }

        const signatureLayer = document.createElementNS(SVG_NS, "g");
        signatureLayer.setAttribute("data-graph-role", "decoration");
        signatureLayer.setAttribute("data-motion-layer", "terminal-signature");
        signatureLayer.setAttribute("aria-hidden", "true");
        signatureLayer.setAttribute("pointer-events", "none");
        const cursor = document.createElementNS(SVG_NS, "rect");
        cursor.setAttribute("data-graph-role", "decoration");
        cursor.setAttribute("data-motion-primitive", contract.primitive);
        cursor.setAttribute("data-owner", contract.nodeId);
        cursor.setAttribute("x", String(bounds.x));
        cursor.setAttribute("y", String(bounds.y + bounds.height - contract.height));
        cursor.setAttribute("width", String(bounds.width));
        cursor.setAttribute("height", String(contract.height));
        cursor.setAttribute("fill", contract.fill);
        cursor.setAttribute("opacity", "0");
        cursor.setAttribute("aria-hidden", "true");
        cursor.setAttribute("pointer-events", "none");
        signatureLayer.append(cursor);
        root.append(signatureLayer);

        const markerFree = !cursor.hasAttribute("marker-start")
          && !cursor.hasAttribute("marker-mid")
          && !cursor.hasAttribute("marker-end");
        const filterFree = !cursor.hasAttribute("filter");
        if (!markerFree || !filterFree) {
          throw new Error("Terminal prompt cursor must be marker-free and filter-free");
        }

        effects.push((time) => {
          const frame = renderedFrameAtTime(time);
          let opacity = 0;
          if (frame >= resetRange[0]) {
            opacity = contract.brightOpacity * resetOpacity(frame);
          } else if (frame >= contract.start && frame <= fullOpacityEnd) {
            const cadenceFrame = Math.floor(frame - contract.start + 1e-9) % contract.periodFrames;
            opacity = cadenceFrame < contract.brightFrames ? contract.brightOpacity : 0;
          }
          cursor.setAttribute("opacity", String(opacity));
        });
        terminalPromptCursorReport = {
          primitive: contract.primitive,
          count: 1,
          node_id: contract.nodeId,
          source_text: contract.sourceText,
          source_text_hidden: false,
          source_text_mutated: false,
          geometry: "2.2px-high rectangle derived from underscore getBBox",
          source_text_bounds: {
            x: bounds.x,
            y: bounds.y,
            width: bounds.width,
            height: bounds.height,
          },
          rectangle: {
            x: bounds.x,
            y: bounds.y + bounds.height - contract.height,
            width: bounds.width,
            height: contract.height,
          },
          fill: contract.fill,
          movement: "opacity-only",
          animated_attributes: ["opacity"],
          settled_after: {
            role: "tool-call",
            order: 0,
            frame: contract.start,
          },
          cadence_frames: [contract.start, fullOpacityEnd],
          period_frames: contract.periodFrames,
          bright_frames_per_period: contract.brightFrames,
          absent_frames_per_period: contract.absentFrames,
          bright_opacity: contract.brightOpacity,
          reset_range: resetRange,
          reset_behavior: "bright opacity multiplied by shared reset opacity",
          marker_free: markerFree,
          filter_free: filterFree,
        };
      }

      addSettledOwnerDecorations();
      drawSchedule.forEach((entry) => {
        addDrawOnRoute(edgeForEntry(entry), entry);
      });
      if (settledOwnerDirectionLayer) motionLayer.append(settledOwnerDirectionLayer);
      drawSchedule.forEach((entry) => {
        if (selectedSceneContract.streamMode === "blueprint-registration-bead") {
          addBlueprintDistributionWave(edgeForEntry(entry), entry);
        } else if (selectedSceneContract.streamMode === "notion-memory-card-handoff") {
          addNotionMemoryCardHandoff(edgeForEntry(entry), entry);
        } else if (selectedSceneContract.streamMode === "specialized-live-signature") {
          addSpecializedLiveSignature(edgeForEntry(entry), entry);
        } else {
          addPersistentDataFlow(edgeForEntry(entry), entry);
        }
      });
      if (selectedSceneContract.streamMode === "specialized-live-signature") {
        addSpecializedAuxiliaries();
      }
      const greatestCommonDivisor = (left, right) => {
        let a = Math.abs(left);
        let b = Math.abs(right);
        while (b) {
          [a, b] = [b, a % b];
        }
        return a;
      };
      const phaseStep = Math.abs(persistentStreamContract.dashOffsetPerFrame);
      if (greatestCommonDivisor(persistentStreamContract.dashPeriod, phaseStep) !== 1) {
        throw new Error("Persistent stream dash period and travel step must be coprime");
      }
      const actualStreamPhases = persistentStreamReports.map((report) => report.initial_phase);
      if (
        actualStreamPhases.length !== expectedStreamPhases.length
        || actualStreamPhases.some((phase, index) => phase !== expectedStreamPhases[index])
      ) {
        throw new Error(
          `${selectedSceneContract.name} stream phases changed: expected ${expectedStreamPhases.join(",")}, got ${actualStreamPhases.join(",")}`,
        );
      }
      if (selectedSceneContract.streamMode === "blueprint-registration-bead") {
        const reportsForRole = (role) => persistentStreamReports.filter((report) => report.role === role);
        const fanout = reportsForRole("fanout");
        const dataWrite = reportsForRole("data-write");
        if (
          fanout.length !== 3
          || fanout.some((report) => report.initial_phase !== 21)
          || dataWrite.length !== 3
          || dataWrite.some((report) => report.initial_phase !== 28)
        ) {
          throw new Error("Blueprint fanout/data-write stage locks changed");
        }
        const dataWriteBeads = registrationBeadReports.filter((report) => report.role === "data-write");
        const pathLengths = dataWriteBeads.map((report) => report.path_length);
        const initialY = dataWriteBeads.map((report) => report.initial_point.y);
        if (
          dataWriteBeads.length !== 3
          || !pathLengths.every((length) => Math.abs(length - pathLengths[0]) < 0.001)
          || !initialY.every((value) => Math.abs(value - initialY[0]) < 0.001)
        ) {
          throw new Error("Blueprint data-write registration beads lost path-length/Y synchrony");
        }
      } else if (selectedSceneContract.streamMode === "notion-memory-card-handoff") {
        const progressVector = notionMemoryCardReports.map((report) => report.initial_normalized_progress);
        const rotationVector = notionMemoryCardReports.map((report) => report.tangent_rotation);
        const colorVector = notionMemoryCardReports.map((report) => report.semantic_color);
        if (
          progressVector.length !== persistentStreamContract.initialNormalizedProgress.length
          || progressVector.some((progress, index) => (
            progress !== persistentStreamContract.initialNormalizedProgress[index]
          ))
          || rotationVector.some((rotation, index) => (
            rotation !== persistentStreamContract.directionSentinels[index].tangentRotation
          ))
          || colorVector.some((color, index) => color !== persistentStreamContract.semanticColors[index])
        ) {
          throw new Error("Notion memory-card progress, tangent, or semantic-color vector changed");
        }
      }
      drawSchedule.forEach((entry) => {
        const edge = edgeForEntry(entry);
        const label = routeLabels.find((candidate) => candidate.dataset.owner === edge.dataset.edgeId);
        if (label) {
          addRouteLabel(label, edge, entry);
        }
      });
      motionLayer.append(labelLayer);
      if (signatureContract) {
        addTerminalPromptCursor(signatureContract);
      }

      const sampledDirections = (edge) => {
        const length = edge.getTotalLength();
        const sampleCount = Math.max(64, Math.ceil(length / 3));
        const directions = [];
        let previous = edge.getPointAtLength(0);
        for (let index = 1; index <= sampleCount; index += 1) {
          const point = edge.getPointAtLength(length * index / sampleCount);
          const deltaX = point.x - previous.x;
          const deltaY = point.y - previous.y;
          previous = point;
          if (Math.abs(deltaX) < 0.001 && Math.abs(deltaY) < 0.001) {
            continue;
          }
          const direction = Math.abs(deltaX) >= Math.abs(deltaY)
            ? (deltaX > 0 ? "right" : "left")
            : (deltaY > 0 ? "down" : "up");
          if (directions[directions.length - 1] !== direction) {
            directions.push(direction);
          }
        }
        return directions;
      };
      const containsOrderedDirections = (actual, expected) => {
        let expectedIndex = 0;
        actual.forEach((direction) => {
          if (direction === expected[expectedIndex]) {
            expectedIndex += 1;
          }
        });
        return expectedIndex === expected.length;
      };
      const directionSentinels = persistentStreamContract.directionSentinels.map((sentinel) => {
        const actual = sampledDirections(edgeForKey(sentinel.role, sentinel.order, sentinel.stage));
        const passed = selectedSceneContract.streamMode === "notion-memory-card-handoff"
          ? actual.length === sentinel.expected.length
            && actual.every((direction, index) => direction === sentinel.expected[index])
          : containsOrderedDirections(actual, sentinel.expected);
        const beadTravelPassed = selectedSceneContract.streamMode !== "blueprint-registration-bead"
          || persistentStreamContract.beadAdvancePerFrame > 0;
        const cardTravelPassed = selectedSceneContract.streamMode !== "notion-memory-card-handoff"
          || (
            persistentStreamContract.cardAdvancePerFrame > 0
            && notionMemoryCardReports.some((report) => (
              report.role === sentinel.role
              && report.motion_order === sentinel.order
              && report.tangent_rotation === sentinel.tangentRotation
            ))
          );
        if (
          !passed
          || persistentStreamContract.dashOffsetPerFrame >= 0
          || !beadTravelPassed
          || !cardTravelPassed
        ) {
          throw new Error(
            `Stream direction sentinel failed for ${routeKey(sentinel.role, sentinel.order, sentinel.stage)}: `
            + `expected ${sentinel.expected.join(" -> ")}, got ${actual.join(" -> ")}`,
          );
        }
        const report = {
          role: sentinel.role,
          expected: sentinel.expected,
          actual,
          dash_offset_per_rendered_frame: persistentStreamContract.dashOffsetPerFrame,
          ...(selectedSceneContract.streamMode === "blueprint-registration-bead" ? {
            bead_advance_per_rendered_frame: persistentStreamContract.beadAdvancePerFrame,
          } : selectedSceneContract.streamMode === "notion-memory-card-handoff" ? {
            card_advance_per_rendered_frame: persistentStreamContract.cardAdvancePerFrame,
            tangent_rotation: sentinel.tangentRotation,
          } : selectedSceneContract.streamMode === "specialized-live-signature" ? {
            signature_advance_per_rendered_frame: persistentStreamContract.advancePerFrame,
          } : {}),
          passed,
        };
        if (!selectedSceneContract.legacyStyleOneReport) {
          report.order = sentinel.order;
          if (sentinel.stage !== undefined) report.stage = sentinel.stage;
          report.schedule_key = routeKey(sentinel.role, sentinel.order, sentinel.stage);
        }
        return report;
      });
      if (!effects.length) {
        throw new Error(`${selectedSceneContract.name} did not create connector draw-on effects`);
      }

      window.__fireworksSetMotionTime = (time) => {
        effects.forEach((effect) => effect(time));
        assertStaticDomUnchanged();
      };
      window.__fireworksSetMotionFrame = (frameIndex) => {
        if (!Number.isInteger(frameIndex) || frameIndex < 0 || frameIndex > renderedFrameMax) {
          throw new Error(`Motion frame index ${frameIndex} is outside 0-${renderedFrameMax}`);
        }
        window.__fireworksSetMotionTime((frameIndex + 0.5) / selectedFps);
      };
      window.__fireworksSetMotionFrame(0);
      const isRenderedVisible = (element) => {
        const style = window.getComputedStyle(element);
        return style.display !== "none"
          && style.visibility !== "hidden"
          && Number(style.opacity || "1") > 0;
      };
      const ownerDecorationSourceOpeningVisibleCount = [11, 12].includes(selectedSceneContract.styleId)
        ? routeOwnerDecorations.filter(isRenderedVisible).length
        : 0;
      const ownerDecorationCloneOpeningVisibleCount = [11, 12].includes(selectedSceneContract.styleId)
        ? settledOwnerDecorationClones.filter(isRenderedVisible).length
        : 0;
      const traceSpanSourceOpeningVisibleCount = selectedSceneContract.styleId === 12
        ? transientTraceSpanSources.filter(isRenderedVisible).length
        : 0;
      const nonTraceSpanNodeOpeningHiddenCount = selectedSceneContract.styleId === 12
        ? nodes.filter((node) => !node.dataset.spanId).filter((node) => !isRenderedVisible(node)).length
        : 0;
      if (ownerDecorationSourceOpeningVisibleCount || ownerDecorationCloneOpeningVisibleCount) {
        throw new Error(`${selectedSceneContract.name} route-owner decoration leaked into the empty opening frame`);
      }
      if (traceSpanSourceOpeningVisibleCount) {
        throw new Error("Ops Pulse source trace spans leaked into the empty opening frame");
      }
      if (nonTraceSpanNodeOpeningHiddenCount) {
        throw new Error("Ops Pulse non-trace nodes were hidden in the empty opening frame");
      }

      return {
        effects: effects.length,
        edges: edges.length,
        nodes: nodes.length,
        fps: selectedFps,
        frame_count: selectedFrameCount,
        scene_report: {
          grammar_version: "3.4",
          preset: selectedPreset,
          primitive: "connector-draw-on-with-persistent-data-flow",
          empty_opening_frame: emptyOpeningFrame,
          connectors_visible_at_opening: false,
          nodes_visible_every_frame: true,
          topology_draw_on: true,
          settled_topology_dynamic: true,
          static_dom_guard: true,
          source_edges_hidden_by_transient_css: true,
          source_edges: edges.length,
          source_route_labels: routeLabels.length,
          draw_clones: drawReports.length,
          settled_marker_clones: drawReports.length,
          stream_count: persistentStreamReports.length,
          packet_head_count: persistentPacketHeadReports.length,
          ...(selectedSceneContract.streamMode === "blueprint-registration-bead" ? {
            registration_bead_count: registrationBeadReports.length,
          } : selectedSceneContract.streamMode === "notion-memory-card-handoff" ? {
            notion_memory_card_count: notionMemoryCardReports.length,
          } : selectedSceneContract.streamMode === "specialized-live-signature" ? {
            specialized_signature_count: specializedSignatureReports.length,
          } : {}),
          route_label_clones: routeLabels.length,
          node_motion: 0,
          text_motion: 0,
          text_geometry_motion: 0,
          route_label_opacity_states: routeLabels.length,
          halo_count: selectedSceneContract.streamMode === "specialized-live-signature"
            ? auxiliaryReports.filter((report) => String(report.primitive || "").includes("halo")).reduce((total, report) => total + Number(report.count || 0), 0)
            : 0,
          ripple_count: 0,
          maximum_concurrent_draws: maximumConcurrentDraws,
          draw_schedule: drawReports,
          persistent_streams: persistentStreamReports,
          persistent_packet_heads: persistentPacketHeadReports,
          ...(selectedSceneContract.streamMode === "blueprint-registration-bead" ? {
            blueprint_registration_beads: registrationBeadReports,
          } : selectedSceneContract.streamMode === "notion-memory-card-handoff" ? {
            notion_memory_cards: notionMemoryCardReports,
          } : selectedSceneContract.streamMode === "specialized-live-signature" ? {
            specialized_signatures: specializedSignatureReports,
            auxiliary_primitives: auxiliaryReports,
            trace_span_reveals: traceSpanRevealReports,
            waterfall_scanner: waterfallScannerReport,
          } : {}),
          direction_sentinels: directionSentinels,
          ...(selectedSceneContract.styleId === 2 ? {
            schedule_key: "(data-motion-role, data-motion-order)",
            cursor_count: terminalPromptCursorReport ? 1 : 0,
            terminal_prompt_cursor: terminalPromptCursorReport,
            extra_scene_primitives: terminalPromptCursorReport ? ["terminal-prompt-cursor"] : [],
            node_glow_pulse_count: 0,
            terminal_text_typing_count: 0,
            scan_line_count: 0,
            camera_motion_count: 0,
            animated_background_count: 0,
          } : selectedSceneContract.styleId === 3 ? {
            schedule_key: "(data-motion-role, data-motion-order)",
            extra_scene_primitives: [],
            node_glow_pulse_count: 0,
            terminal_text_typing_count: 0,
            terminal_cursor_count: 0,
            scan_line_count: 0,
            camera_motion_count: 0,
            animated_background_count: 0,
            blur_count: 0,
            shadow_count: 0,
            stage_locks: {
              fanout: { orders: [0, 1, 2], phase: 21 },
              data_write: { orders: [0, 1, 2], phase: 28, equal_length_paths: true },
            },
          } : selectedSceneContract.styleId === 4 ? {
            schedule_key: "(data-motion-role, data-motion-order)",
            extra_scene_primitives: ["notion-memory-card"],
            node_glow_pulse_count: 0,
            terminal_text_typing_count: 0,
            terminal_cursor_count: 0,
            circular_bead_count: 0,
            scan_line_count: 0,
            camera_motion_count: 0,
            animated_background_count: 0,
            blur_count: 0,
            shadow_count: 0,
            semantic_color_vector: persistentStreamContract.semanticColors,
            initial_progress_vector: persistentStreamContract.initialNormalizedProgress,
          } : selectedSceneContract.styleId >= 5 ? {
            schedule_key: selectedSceneContract.stageAwareRouteKey
              ? "(data-motion-role, data-motion-stage, data-motion-order)"
              : "(data-motion-role, data-motion-order)",
            scene_identity: selectedSceneContract.name,
            signature_kind: persistentStreamContract.signatureKind,
            extra_scene_primitives: [...new Set([
              ...specializedSignatureReports.map((report) => report.primitive),
              ...auxiliaryReports.map((report) => report.primitive),
              ...traceSpanRevealReports.map(() => "trace-span-reveal"),
              ...(waterfallScannerReport ? ["waterfall-scanner"] : []),
            ])],
            maximum_live_rail_width: Math.max(...persistentStreamReports.map((report) => report.stroke_width)),
            live_rail_width_ceiling: persistentStreamContract.bodyWidthMaximum,
            live_rail_width_ceiling_passed: persistentStreamReports.every((report) => report.stroke_width <= persistentStreamContract.bodyWidthMaximum),
            dynamic_not_thicker_than_source: persistentStreamReports.every((report) => report.dynamic_not_thicker_than_source),
            signature_travel_at_100_percent: persistentStreamContract.advancePerFrame,
            signature_travel_at_50_percent: persistentStreamContract.advancePerFrame / 2,
            source_geometry_mutation_count: 0,
            source_text_mutation_count: 0,
            source_marker_mutation_count: 0,
            node_motion_count: 0,
            text_motion_count: 0,
            camera_motion_count: 0,
            animated_background_count: 0,
            ...(selectedSceneContract.styleId === 8 ? {
              gem_filter_boundary: {
                filtered_elements_per_tracer: specializedSignatureReports.map((report) => report.filtered_element_count),
                only_gem_halo_filtered: specializedSignatureReports.every((report) => report.filtered_element_count === 1),
              },
            } : selectedSceneContract.styleId === 10 ? {
              pair_phase_locks: {
                global_route: persistentStreamReports.filter((report) => report.role === "global-route").map((report) => report.initial_phase),
                regional_write: persistentStreamReports.filter((report) => report.role === "regional-write").map((report) => report.initial_phase),
              },
              replication_direction: "left-to-right-only",
            } : selectedSceneContract.styleId === 11 ? {
              station_dwell_ring_count: auxiliaryReports.filter((report) => report.primitive === "station-dwell-ring").reduce((total, report) => total + Number(report.count || 0), 0),
              accepted_station_geometry_mutated: false,
              source_owner_decoration_count: routeOwnerDecorations.length,
              settled_owner_decoration_clone_count: settledOwnerDecorationClones.length,
              owner_decoration_source_opening_visible_count: ownerDecorationSourceOpeningVisibleCount,
              owner_decoration_clone_opening_visible_count: ownerDecorationCloneOpeningVisibleCount,
              owner_decorations: settledOwnerDecorationReports,
            } : selectedSceneContract.styleId === 12 ? {
              trace_span_reveal_count: traceSpanRevealReports.length,
              trace_span_source_count: transientTraceSpanSources.length,
              trace_span_source_opening_visible_count: traceSpanSourceOpeningVisibleCount,
              non_trace_span_node_opening_hidden_count: nonTraceSpanNodeOpeningHiddenCount,
              source_owner_decoration_count: routeOwnerDecorations.length,
              settled_owner_decoration_clone_count: settledOwnerDecorationClones.length,
              owner_decoration_source_opening_visible_count: ownerDecorationSourceOpeningVisibleCount,
              owner_decoration_clone_opening_visible_count: ownerDecorationCloneOpeningVisibleCount,
              owner_decorations: settledOwnerDecorationReports,
              status_card_blink_count: 0,
              metric_blink_count: 0,
              scanner_contained: Boolean(waterfallScannerReport),
              scanner_below_span_labels: Boolean(waterfallScannerReport?.below_span_labels),
            } : {}),
          } : {}),
          reset_range: resetRange,
          reset_opacity_samples: resetOpacitySamples,
          draw_contract: {
            source_geometry: "exact immutable edge clone",
            marker_during_draw: false,
            marker_after_arrival: true,
            easing: "linear",
          },
          persistent_stream_contract: {
            primitive: persistentStreamContract.bodyPrimitive,
            ...(selectedSceneContract.streamMode === "blueprint-registration-bead" ? {
              registration_bead_primitive: persistentStreamContract.beadPrimitive,
            } : selectedSceneContract.streamMode === "notion-memory-card-handoff" ? {
              memory_card_primitive: persistentStreamContract.cardPrimitive,
            } : selectedSceneContract.streamMode === "specialized-live-signature" ? {
              signature_primitive: persistentStreamContract.signaturePrimitive,
            } : {
              packet_head_primitive: persistentStreamContract.headPrimitive,
            }),
            stream_count: persistentStreamReports.length,
            ...(selectedSceneContract.streamMode === "blueprint-registration-bead" ? {
              registration_bead_count: registrationBeadReports.length,
            } : selectedSceneContract.streamMode === "notion-memory-card-handoff" ? {
              memory_card_count: notionMemoryCardReports.length,
            } : selectedSceneContract.streamMode === "specialized-live-signature" ? {
              signature_count: specializedSignatureReports.length,
            } : {
              packet_head_count: persistentPacketHeadReports.length,
            }),
            rendered_frames: [persistentStreamContract.start, renderedFrameMax],
            fade_in_frames: [persistentStreamContract.start, persistentStreamContract.start + 2],
            fade_in_factors: persistentStreamContract.fadeInFactors,
            full_opacity_frames: [persistentStreamContract.start + 2, fullOpacityEnd],
            body: {
              primitive: persistentStreamContract.bodyPrimitive,
              stroke_width: persistentStreamContract.bodyWidthDescription,
              ...(selectedSceneContract.styleId === 1 ? {
                resolved_style_1_source_stroke_width: persistentStreamContract.sourceStrokeWidth,
                resolved_style_1_stroke_width: persistentStreamContract.resolvedStrokeWidth,
              } : selectedSceneContract.styleId === 2 ? {
                resolved_style_2_source_stroke_width: persistentStreamContract.sourceStrokeWidth,
                resolved_style_2_stroke_width: persistentStreamContract.resolvedStrokeWidth,
                source_colors_in_schedule_order: persistentStreamContract.expectedSourceColors,
                semantic_colors: {
                  control: "#a855f7",
                  tool_read: "#38bdf8",
                  index_write: "#22c55e",
                  grounding_data: "#fb7185",
                  answer: "#f97316",
                },
              } : selectedSceneContract.styleId === 3 ? {
                resolved_style_3_source_stroke_width: persistentStreamContract.sourceStrokeWidth,
                resolved_style_3_stroke_width: persistentStreamContract.resolvedStrokeWidth,
                resolved_style_3_stroke_width_at_50_percent:
                  persistentStreamContract.resolvedStrokeWidth / 2,
                source_colors_in_schedule_order: persistentStreamContract.expectedSourceColors,
              } : selectedSceneContract.styleId === 4 ? {
                resolved_style_4_source_stroke_width: persistentStreamContract.sourceStrokeWidth,
                resolved_style_4_stroke_width: persistentStreamContract.resolvedStrokeWidth,
                resolved_style_4_stroke_width_at_50_percent:
                  persistentStreamContract.resolvedStrokeWidth / 2,
                source_colors_in_schedule_order: persistentStreamContract.expectedSourceColors,
                semantic_colors_in_schedule_order: persistentStreamContract.semanticColors,
              } : {
                maximum_live_width: persistentStreamContract.bodyWidthMaximum,
                resolved_widths_in_schedule_order: persistentStreamReports.map((report) => report.stroke_width),
                source_widths_in_schedule_order: persistentStreamReports.map((report) => report.source_stroke_width),
                dynamic_not_thicker_than_source: persistentStreamReports.every((report) => report.dynamic_not_thicker_than_source),
              }),
              color: persistentStreamContract.bodyColor || "inherit-source-stroke",
              opacity: persistentStreamContract.bodyOpacity,
              dash_pattern: persistentStreamContract.bodyDashPattern,
              linecap: "round",
              linejoin: "round",
              marker_free: true,
              filter_free: true,
            },
            ...(selectedSceneContract.streamMode === "blueprint-registration-bead" ? {
              registration_bead: {
                primitive: persistentStreamContract.beadPrimitive,
                shape: "circle",
                radius: persistentStreamContract.beadRadius,
                diameter_at_960px: persistentStreamContract.beadRadius * 2,
                diameter_at_50_percent: persistentStreamContract.beadRadius,
                fill: persistentStreamContract.beadFill,
                stroke: "inherit-source-stroke",
                stroke_width: persistentStreamContract.beadStrokeWidth,
                opacity: persistentStreamContract.beadOpacity,
                initial_path_distance: "stage-locked-phase",
                path_advance_per_rendered_frame: persistentStreamContract.beadAdvancePerFrame,
                direction: "source-to-target",
                wrap: "target-end-to-source-start",
                animated_attributes: ["cx", "cy", "opacity"],
                marker_free: true,
                filter_free: true,
              },
              bead_advance_per_rendered_frame: persistentStreamContract.beadAdvancePerFrame,
              stage_locks: {
                fanout: { orders: [0, 1, 2], phase: 21 },
                data_write: { orders: [0, 1, 2], phase: 28, equal_length_paths: true },
              },
            } : selectedSceneContract.streamMode === "notion-memory-card-handoff" ? {
              memory_card: {
                primitive: persistentStreamContract.cardPrimitive,
                shape: "group",
                outer_rect: {
                  ...persistentStreamContract.outerRect,
                  fill: persistentStreamContract.cardFill,
                  stroke: "semantic-memory-destination",
                  stroke_width: persistentStreamContract.cardStrokeWidth,
                },
                ink_lines: persistentStreamContract.inkLines,
                ink_stroke: "semantic-memory-destination",
                ink_stroke_width: persistentStreamContract.inkStrokeWidth,
                ink_linecap: persistentStreamContract.inkLinecap,
                ink_shape_rendering: persistentStreamContract.inkShapeRendering,
                opacity: persistentStreamContract.cardOpacity,
                semantic_colors_in_schedule_order: persistentStreamContract.semanticColors,
                initial_normalized_progress_by_stage:
                  persistentStreamContract.initialNormalizedProgress,
                initial_path_distance: "8 + progress * (pathLength - 16)",
                endpoint_clearance: persistentStreamContract.endpointClearance,
                path_advance_per_rendered_frame: persistentStreamContract.cardAdvancePerFrame,
                tangent_rotations_in_schedule_order:
                  persistentStreamContract.directionSentinels.map((sentinel) => sentinel.tangentRotation),
                direction: "source-to-target",
                wrap: "target-clearance-to-source-clearance",
                animated_attributes: ["transform", "opacity"],
                marker_free: true,
                filter_free: true,
                shadow_free: true,
                appended_below_labels_and_nodes: true,
              },
              card_advance_per_rendered_frame: persistentStreamContract.cardAdvancePerFrame,
            } : selectedSceneContract.streamMode === "specialized-live-signature" ? {
              signature: {
                primitive: persistentStreamContract.signaturePrimitive,
                signature_kind: persistentStreamContract.signatureKind,
                endpoint_clearance: persistentStreamContract.endpointClearance,
                path_advance_per_rendered_frame: persistentStreamContract.advancePerFrame,
                direction: "source-to-target",
                wrap: "target-clearance-to-source-clearance",
                tangent_aware_rotation: true,
                animated_attributes: ["transform", "opacity"],
                appended_below_labels_and_nodes: true,
                geometry_by_route: specializedSignatureReports.map((report) => ({
                  schedule_key: report.schedule_key,
                  primitive: report.primitive,
                  geometry: report.geometry,
                })),
              },
              signature_advance_per_rendered_frame: persistentStreamContract.advancePerFrame,
            } : {
              packet_head: {
                primitive: persistentStreamContract.headPrimitive,
                stroke_width: persistentStreamContract.headStrokeWidth,
                color: persistentStreamContract.headColor,
                opacity: persistentStreamContract.headOpacity,
                dash_pattern: persistentStreamContract.headDashPattern,
                dash_offset_from_body: persistentStreamContract.headLeadOffset,
                linecap: "round",
                linejoin: "round",
                marker_free: true,
                filter_free: true,
                appended_immediately_after_body: true,
              },
            }),
            dash_period: persistentStreamContract.dashPeriod,
            dash_offset_per_rendered_frame: persistentStreamContract.dashOffsetPerFrame,
            travel_user_units_per_rendered_frame: phaseStep,
            travel_pixels_per_frame_at_960px: selectedSceneContract.streamMode === "specialized-live-signature" ? persistentStreamContract.advancePerFrame : 6,
            travel_pixels_per_second_at_960px_20fps: (selectedSceneContract.streamMode === "specialized-live-signature" ? persistentStreamContract.advancePerFrame : 6) * 20,
            travel_pixels_per_frame_at_50_percent: (selectedSceneContract.streamMode === "specialized-live-signature" ? persistentStreamContract.advancePerFrame : 6) / 2,
            travel_pixels_per_second_at_50_percent_20fps: (selectedSceneContract.streamMode === "specialized-live-signature" ? persistentStreamContract.advancePerFrame : 6) * 10,
            phase_policy: persistentStreamContract.phasePolicy,
            expected_initial_phases: expectedStreamPhases,
            period_step_coprime: true,
            stream_interval_frame_count: renderedFrameMax - persistentStreamContract.start + 1,
            phase_repeat_within_stream_interval:
              renderedFrameMax - persistentStreamContract.start + 1 > persistentStreamContract.dashPeriod,
            direction: "source-to-target",
            travel_easing: "linear",
            minimum_review_scale: "50%",
            reset_range: resetRange,
            reset_opacity_samples: resetOpacitySamples,
            reset_behavior: persistentStreamContract.resetBehavior || "live rails, signatures, and scene auxiliaries keep advancing while the shared reset opacity fades to 0.03",
          },
        },
      };
    },
    {
      selectedPreset: preset,
      selectedFps: fps,
      selectedFrameCount: frameCount,
      selectedSceneContract: sceneContract,
      minimumFrameCount: MINIMUM_FRAME_COUNT,
      emptyOpeningFrame: EMPTY_OPENING_FRAME,
      resetOpacitySamples: RESET_OPACITY_SAMPLES,
    },
  );
}

async function renderFrames(arguments_) {
  const input = path.resolve(arguments_.input || "");
  const framesDirectory = path.resolve(arguments_["frames-dir"] || "");
  const preset = arguments_.preset || "";
  const sceneContract = SCENE_CONTRACTS[preset];
  const duration = Number(arguments_.duration);
  const fps = Number(arguments_.fps);
  const width = Number(arguments_.width);
  const height = Number(arguments_.height);
  if (!fs.existsSync(input) || !fs.statSync(input).isFile()) {
    throw new Error(`Input does not exist: ${input}`);
  }
  if (!fs.existsSync(framesDirectory) || !fs.statSync(framesDirectory).isDirectory()) {
    throw new Error(`Frames directory does not exist: ${framesDirectory}`);
  }
  if (
    !PRESETS.has(preset) ||
    !Number.isFinite(duration) || duration < 0.5 || duration > 20 ||
    !Number.isInteger(fps) || fps < 1 || fps > 25 ||
    !Number.isInteger(width) || width < 320 || width > 4096 ||
    !Number.isInteger(height) || height < 1 || height > 4096
  ) {
    throw new Error("Preset, duration, fps, or dimensions are invalid");
  }
  if (height > 4096 || width * height > 16777216) {
    throw new Error("Output must stay within 4096px per side and 16 megapixels");
  }

  const svg = fs.readFileSync(input, "utf8");
  const viewBoxMatch = svg.match(/viewBox="([^"]+)"/i);
  if (!viewBoxMatch) {
    throw new Error("SVG viewBox is unavailable");
  }
  const viewBox = viewBoxMatch[1].trim().split(/[\s,]+/).map(Number);
  if (viewBox.length !== 4 || viewBox.some((value) => !Number.isFinite(value))) {
    throw new Error("SVG viewBox is invalid");
  }
  const requestedFrames = duration * fps;
  const frameCount = Math.round(requestedFrames);
  if (Math.abs(requestedFrames - frameCount) > 1e-9 || frameCount > 500) {
    throw new Error("Duration multiplied by fps must be a whole number of at most 500 frames");
  }
  if (frameCount < MINIMUM_FRAME_COUNT) {
    throw new Error(`${sceneContract.name} requires at least ${MINIMUM_FRAME_COUNT} rendered frames`);
  }
  if (width * height * frameCount > 600000000) {
    throw new Error("Output dimensions multiplied by frame count may not exceed 600 million pixels");
  }

  const loaded = loadRenderer();
  const executablePath = chromeExecutable(loaded.api);
  if (!executablePath) {
    throw new Error("No compatible Chrome or Chromium executable was found");
  }
  const noSandbox = process.env.FIREWORKS_CHROME_NO_SANDBOX === "1";
  const chromeArguments = [
    "--disable-dev-shm-usage",
    "--disable-background-timer-throttling",
    "--disable-renderer-backgrounding",
    "--force-color-profile=srgb",
    "--font-render-hinting=none",
    "--hide-scrollbars",
  ];
  if (noSandbox) {
    chromeArguments.unshift("--no-sandbox", "--disable-setuid-sandbox");
  }
  const browser = await loaded.api.launch({
    headless: "new",
    executablePath,
    args: chromeArguments,
  });

  try {
    const page = await browser.newPage();
    await page.setViewport({ width, height, deviceScaleFactor: 1 });
    await page.setRequestInterception(true);
    page.on("request", (request) => {
      const url = request.url();
      if (url === "about:blank" || url.startsWith("data:") || url.startsWith("blob:")) {
        request.continue();
      } else {
        request.abort("blockedbyclient");
      }
    });
    await page.setContent(
      `<html><head><meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src data:; style-src 'unsafe-inline'">` +
      `<style>html,body{margin:0;width:${width}px;height:${height}px;overflow:hidden;background:transparent}` +
      `*{animation:none!important;transition:none!important;caret-color:transparent!important}` +
      `svg{display:block;width:${width}px!important;height:${height}px!important}</style></head>` +
      `<body><main id="diagram"></main></body></html>`,
      { waitUntil: "load" },
    );
    await page.$eval("#diagram", (container, source) => { container.innerHTML = source; }, svg);
    await page.evaluate(() => document.fonts.ready);
    const setup = await installMotionRuntime(page, preset, fps, frameCount);
    await page.evaluate(async () => {
      await document.fonts.ready;
      await new Promise((resolve) => {
        requestAnimationFrame(() => requestAnimationFrame(resolve));
      });
    });

    for (let index = 0; index < frameCount; index += 1) {
      await page.evaluate(async (frameIndex) => {
        window.__fireworksSetMotionFrame(frameIndex);
        await new Promise((resolve) => {
          requestAnimationFrame(() => requestAnimationFrame(resolve));
        });
      }, index);
      const framePath = path.join(framesDirectory, `frame-${String(index).padStart(6, "0")}.png`);
      await page.screenshot({
        path: framePath,
        type: "png",
        omitBackground: true,
        captureBeyondViewport: false,
      });
    }
    await page.close();
    return {
      ok: true,
      engine: "chromium-svg-draw-on-persistent-data-flow",
      module: loaded.module,
      resolved_module: loaded.resolvedModule,
      module_version: loaded.moduleVersion,
      chrome: executablePath,
      preset,
      duration_seconds: duration,
      fps,
      frame_count: frameCount,
      width,
      height,
      input_kind: "svg",
      effects: setup.effects,
      scene_report: setup.scene_report,
      loop_frame_policy: "integer-frame-index-centers-with-steady-state-plus-reset-boundary-repeat-scope",
      paint_barrier: "fonts-ready-plus-two-animation-frames-before-capture",
      sandbox: noSandbox ? "disabled-by-explicit-env" : "enabled",
    };
  } finally {
    await browser.close();
  }
}

async function main() {
  const arguments_ = parseArguments(process.argv.slice(2));
  const result = arguments_.probe ? probe() : await renderFrames(arguments_);
  process.stdout.write(`${JSON.stringify(result)}\n`);
}

main().catch((error) => {
  process.stderr.write(`${error.stack || error.message}\n`);
  process.exit(1);
});
skills/fireworks-tech-graph/scripts/generate-diagram.sh
#!/bin/bash
# SVG Diagram Generation - Validates and exports SVG diagrams with PNG export

set -euo pipefail

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'

# Default values
STYLE="1"
WIDTH="1920"
OUTPUT_DIR="."
VALIDATE=true

# Valid diagram types
VALID_TYPES="architecture|data-flow|flowchart|sequence|comparison|timeline|mind-map|agent|memory|use-case|class|state-machine|er-diagram|network-topology"

usage() {
    cat << USAGE
Usage: $0 [OPTIONS]

Options:
    -t, --type TYPE        Diagram type ($VALID_TYPES)
    -s, --style STYLE      Style number (1-12, default: 1)
    -o, --output PATH      Output path (default: current directory)
    -w, --width WIDTH      PNG width in pixels (default: 1920)
    --no-validate          Skip validation
    -h, --help             Show this help

Examples:
    $0 -t architecture -s 1 -o ./output/arch.svg
    $0 -t class -s 2 -w 2400
    $0 -t sequence -s 6
USAGE
    exit "${1:-0}"
}

# Parse arguments
require_arg() { if [[ $# -lt 2 || "$2" == -* ]]; then echo -e "${RED}Error: $1 requires a value${NC}"; usage 1; fi; }
while [[ $# -gt 0 ]]; do
    case $1 in
        -t|--type)
            require_arg "$@"
            TYPE="$2"
            shift 2
            ;;
        -s|--style)
            require_arg "$@"
            STYLE="$2"
            shift 2
            ;;
        -o|--output)
            require_arg "$@"
            OUTPUT_PATH="$2"
            shift 2
            ;;
        -w|--width)
            require_arg "$@"
            WIDTH="$2"
            shift 2
            ;;
        --no-validate)
            VALIDATE=false
            shift
            ;;
        -h|--help)
            usage
            ;;
        *)
            echo -e "${RED}Unknown option: $1${NC}"
            usage 1
            ;;
    esac
done

# Check required parameters
if [ -z "${TYPE:-}" ]; then
    echo -e "${RED}Error: Diagram type is required${NC}"
    usage 1
fi

# Validate type
VALID_TYPE=false
for t in architecture data-flow flowchart sequence comparison timeline mind-map agent memory use-case class state-machine er-diagram network-topology; do
    if [ "$TYPE" = "$t" ]; then
        VALID_TYPE=true
        break
    fi
done

if [ "$VALID_TYPE" = false ]; then
    echo -e "${RED}Error: Invalid diagram type '$TYPE'${NC}"
    echo "Valid types: $VALID_TYPES"
    exit 1
fi

if ! [[ "$WIDTH" =~ ^[1-9][0-9]*$ ]]; then
    echo -e "${RED}Error: Width must be a positive integer${NC}"
    exit 1
fi

# Determine output path
if [ -z "${OUTPUT_PATH:-}" ]; then
    BASENAME="${TYPE}-style${STYLE}"
    SVG_FILE="${OUTPUT_DIR}/${BASENAME}.svg"
    PNG_FILE="${OUTPUT_DIR}/${BASENAME}.png"
else
    SVG_FILE="$OUTPUT_PATH"
    PNG_FILE="${OUTPUT_PATH%.svg}.png"
fi

echo -e "${BLUE}Processing ${TYPE} diagram (style ${STYLE})...${NC}"
echo "Output: $SVG_FILE"

# Load style reference
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
STYLE_FILE=$(find "${SKILL_DIR}/references" -maxdepth 1 -type f -name "style-${STYLE}-*.md" | head -n 1)

if [ -z "${STYLE_FILE:-}" ] || [ ! -f "$STYLE_FILE" ]; then
    echo -e "${RED}Error: Style file not found: ${STYLE_FILE}${NC}"
    echo "Available styles: 1-12"
    exit 1
fi

# Note: Actual SVG generation is done by the invoking AI coding agent
# This script provides validation and export only

echo -e "${YELLOW}Note: SVG content generation is handled by Codex or Claude Code${NC}"
echo -e "${YELLOW}This script provides validation and export only${NC}"

# Validate if SVG exists
if [ -f "$SVG_FILE" ]; then
    if [ "$VALIDATE" = true ]; then
        echo -e "\n${BLUE}Validating SVG...${NC}"
        if "${SKILL_DIR}/scripts/validate-svg.sh" "$SVG_FILE"; then
            echo -e "${GREEN}Validation passed${NC}"
        else
            echo -e "${RED}Validation failed${NC}"
            exit 1
        fi
    fi

    # Export PNG
    echo -e "\n${BLUE}Exporting PNG (width: ${WIDTH}px)...${NC}"

    PNG_OK=false

    # Method 1 (preferred): cairosvg — best CSS support, good fidelity
    if python3 -c "import cairosvg" 2>/dev/null; then
        echo -e "${BLUE}Using cairosvg (recommended)...${NC}"
        if python3 -c "import sys, cairosvg; cairosvg.svg2png(url=sys.argv[1], write_to=sys.argv[2], output_width=int(sys.argv[3]))" "$SVG_FILE" "$PNG_FILE" "$WIDTH" 2>/dev/null; then
            PNG_OK=true
        else
            echo -e "${YELLOW}cairosvg failed, falling back...${NC}"
        fi
    fi

    # Method 2 (fallback): rsvg-convert — may drop CSS / foreignObject
    if [ "$PNG_OK" = false ] && command -v rsvg-convert &> /dev/null; then
        echo -e "${BLUE}Using rsvg-convert (fallback)...${NC}"
        echo -e "${YELLOW}Warning: rsvg-convert may drop CSS styles or <foreignObject> — install cairosvg for better fidelity${NC}"
        echo -e "${YELLOW}  python3 -m pip install cairosvg${NC}"
        if rsvg-convert -w "$WIDTH" "$SVG_FILE" -o "$PNG_FILE" 2>/dev/null; then
            PNG_OK=true
        fi
    fi

    if [ "$PNG_OK" = true ]; then
        PNG_SIZE=$(du -h "$PNG_FILE" | cut -f1)
        echo -e "${GREEN}PNG exported: $PNG_FILE (${PNG_SIZE})${NC}"
    else
        echo -e "${RED}PNG export failed${NC}"
        echo -e "${YELLOW}Install one of:${NC}"
        echo -e "  ${YELLOW}python3 -m pip install cairosvg${NC} (recommended)"
        echo -e "  ${YELLOW}brew install librsvg${NC}        (macOS)  /  apt install librsvg2-bin (Debian)"
        exit 1
    fi
else
    echo -e "${YELLOW}SVG file not found. Generate it first with Codex or Claude Code.${NC}"
    exit 1
fi

echo -e "\n${GREEN}Done${NC}"
skills/fireworks-tech-graph/scripts/composition_quality.py
#!/usr/bin/env python3
"""Shared composition-quality contract for generated and authored diagrams.

Geometry safety answers whether a diagram is technically valid.  This module
adds the stricter presentation rules used by the official showcase fixtures:
short orthogonal routes, deliberate whitespace, clear labels, and zero bridge
crossings whenever the topology can be composed without them.
"""

from __future__ import annotations

import math
from dataclasses import dataclass
from typing import Any, Mapping, Optional, Sequence


Point = tuple[float, float]
Bounds = tuple[float, float, float, float]


@dataclass(frozen=True)
class CompositionContract:
    profile: str
    max_bends_per_edge: int
    max_total_bends: int
    max_route_stretch: float
    max_bridged_crossings: int
    min_node_gap: float
    min_container_gutter: float
    min_label_clearance: float
    min_segment_length: float

    def as_dict(self) -> dict[str, Any]:
        return {
            "profile": self.profile,
            "max_bends_per_edge": self.max_bends_per_edge,
            "max_total_bends": self.max_total_bends,
            "max_route_stretch": self.max_route_stretch,
            "max_bridged_crossings": self.max_bridged_crossings,
            "min_node_gap": self.min_node_gap,
            "min_container_gutter": self.min_container_gutter,
            "min_label_clearance": self.min_label_clearance,
            "min_segment_length": self.min_segment_length,
        }


PROFILES: dict[str, CompositionContract] = {
    "standard": CompositionContract(
        profile="standard",
        max_bends_per_edge=12,
        max_total_bends=100,
        max_route_stretch=5.0,
        max_bridged_crossings=8,
        min_node_gap=0.0,
        min_container_gutter=0.0,
        min_label_clearance=2.0,
        min_segment_length=0.0,
    ),
    "showcase": CompositionContract(
        profile="showcase",
        max_bends_per_edge=2,
        max_total_bends=8,
        max_route_stretch=1.35,
        max_bridged_crossings=0,
        min_node_gap=40.0,
        min_container_gutter=20.0,
        min_label_clearance=4.0,
        min_segment_length=16.0,
    ),
}


def _number(value: Any, fallback: float) -> float:
    try:
        number = float(value)
    except (TypeError, ValueError):
        return fallback
    return number if math.isfinite(number) else fallback


def resolve_contract(raw: Any = None) -> CompositionContract:
    """Resolve a profile name or a mapping with profile overrides."""

    if isinstance(raw, Mapping):
        profile = str(raw.get("profile", "standard")).strip().lower()
        overrides = raw
    else:
        profile = str(raw or "standard").strip().lower()
        overrides = {}
    if profile not in PROFILES:
        raise ValueError(f"unsupported composition profile: {profile}")
    base = PROFILES[profile]
    values = base.as_dict()
    for key in values:
        if key == "profile" or key not in overrides:
            continue
        values[key] = _number(overrides[key], float(values[key]))
    for key in ("max_bends_per_edge", "max_total_bends", "max_bridged_crossings"):
        values[key] = int(values[key])
    if any(float(values[key]) < 0 for key in values if key != "profile"):
        raise ValueError("composition quality limits must be non-negative")
    return CompositionContract(**values)


def rectangle_gap(first: Bounds, second: Bounds) -> float:
    """Return the Euclidean whitespace between two axis-aligned rectangles."""

    horizontal = max(first[0] - second[2], second[0] - first[2], 0.0)
    vertical = max(first[1] - second[3], second[1] - first[3], 0.0)
    return math.hypot(horizontal, vertical)


def bounds_area(bounds: Bounds) -> float:
    return max(0.0, bounds[2] - bounds[0]) * max(0.0, bounds[3] - bounds[1])


def containing_container(node: Bounds, containers: Sequence[tuple[str, Bounds]]) -> Optional[tuple[str, Bounds]]:
    center = ((node[0] + node[2]) / 2, (node[1] + node[3]) / 2)
    matches = [
        item
        for item in containers
        if item[1][0] <= center[0] <= item[1][2] and item[1][1] <= center[1] <= item[1][3]
    ]
    return min(matches, key=lambda item: bounds_area(item[1])) if matches else None


def container_gutter(node: Bounds, container: Bounds) -> float:
    return min(
        node[0] - container[0],
        node[1] - container[1],
        container[2] - node[2],
        container[3] - node[3],
    )


def route_stretch(points: Sequence[Point]) -> float:
    if len(points) < 2:
        return 1.0
    length = sum(abs(x2 - x1) + abs(y2 - y1) for (x1, y1), (x2, y2) in zip(points, points[1:]))
    direct = abs(points[-1][0] - points[0][0]) + abs(points[-1][1] - points[0][1])
    return 1.0 if direct <= 1e-9 else length / direct


def segment_lengths(points: Sequence[Point]) -> list[float]:
    return [abs(x2 - x1) + abs(y2 - y1) for (x1, y1), (x2, y2) in zip(points, points[1:])]


def assess_composition(
    *,
    nodes: Sequence[tuple[str, Bounds]],
    containers: Sequence[tuple[str, Bounds]],
    edges: Sequence[Mapping[str, Any]],
    contract: CompositionContract,
) -> dict[str, Any]:
    """Return deterministic showcase metrics and actionable violations."""

    violations: list[dict[str, Any]] = []
    total_bends = 0
    total_bridges = 0
    max_stretch = 1.0
    shortest_segment: Optional[float] = None

    for edge in edges:
        edge_id = str(edge.get("id", "edge"))
        points = [tuple(map(float, point)) for point in edge.get("route", [])]
        bends = int(edge.get("bends", max(0, len(points) - 2)))
        bridges = len(edge.get("bridges", []))
        stretch = route_stretch(points)
        lengths = [length for length in segment_lengths(points) if length > 1e-6]
        local_shortest = min(lengths) if lengths else None
        total_bends += bends
        total_bridges += bridges
        max_stretch = max(max_stretch, stretch)
        if local_shortest is not None:
            shortest_segment = local_shortest if shortest_segment is None else min(shortest_segment, local_shortest)
        if bends > contract.max_bends_per_edge:
            violations.append({
                "code": "EDGE_BEND_BUDGET",
                "element": edge_id,
                "actual": bends,
                "limit": contract.max_bends_per_edge,
            })
        if stretch > contract.max_route_stretch + 1e-6:
            violations.append({
                "code": "EDGE_ROUTE_STRETCH",
                "element": edge_id,
                "actual": round(stretch, 3),
                "limit": contract.max_route_stretch,
            })
        if local_shortest is not None and local_shortest + 1e-6 < contract.min_segment_length:
            violations.append({
                "code": "EDGE_MICRO_SEGMENT",
                "element": edge_id,
                "actual": round(local_shortest, 2),
                "limit": contract.min_segment_length,
            })

    if total_bends > contract.max_total_bends:
        violations.append({
            "code": "TOTAL_BEND_BUDGET",
            "element": "diagram",
            "actual": total_bends,
            "limit": contract.max_total_bends,
        })
    if total_bridges > contract.max_bridged_crossings:
        violations.append({
            "code": "BRIDGE_BUDGET",
            "element": "diagram",
            "actual": total_bridges,
            "limit": contract.max_bridged_crossings,
        })

    minimum_gap: Optional[float] = None
    for index, (first_id, first) in enumerate(nodes):
        for second_id, second in nodes[index + 1 :]:
            gap = rectangle_gap(first, second)
            minimum_gap = gap if minimum_gap is None else min(minimum_gap, gap)
            if gap + 1e-6 < contract.min_node_gap:
                violations.append({
                    "code": "NODE_GAP",
                    "element": f"{first_id},{second_id}",
                    "actual": round(gap, 2),
                    "limit": contract.min_node_gap,
                })

    minimum_gutter: Optional[float] = None
    for node_id, node in nodes:
        match = containing_container(node, containers)
        if not match:
            continue
        container_id, container = match
        gutter = container_gutter(node, container)
        minimum_gutter = gutter if minimum_gutter is None else min(minimum_gutter, gutter)
        if gutter + 1e-6 < contract.min_container_gutter:
            violations.append({
                "code": "CONTAINER_GUTTER",
                "element": f"{node_id}@{container_id}",
                "actual": round(gutter, 2),
                "limit": contract.min_container_gutter,
            })

    penalty = (
        len(violations) * 12
        + total_bridges * 8
        + max(0, total_bends - len(edges)) * 2
    )
    return {
        "profile": contract.profile,
        "ok": not violations,
        "score": max(0, 100 - penalty),
        "metrics": {
            "total_bends": total_bends,
            "bridged_crossings": total_bridges,
            "max_route_stretch": round(max_stretch, 3),
            "minimum_node_gap": round(minimum_gap, 2) if minimum_gap is not None else None,
            "minimum_container_gutter": round(minimum_gutter, 2) if minimum_gutter is not None else None,
            "shortest_segment": round(shortest_segment, 2) if shortest_segment is not None else None,
        },
        "limits": contract.as_dict(),
        "violations": violations,
    }


__all__ = [
    "Bounds",
    "CompositionContract",
    "PROFILES",
    "assess_composition",
    "resolve_contract",
    "route_stretch",
]
skills/fireworks-tech-graph/scripts/fireworks.py
#!/usr/bin/env python3
"""Unified command line for rendering, validating, inspecting, and exporting diagrams."""

from __future__ import annotations

import argparse
import importlib.util
import json
import shutil
import sys
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Optional, Sequence


SCRIPT_DIR = Path(__file__).resolve().parent
ROOT = SCRIPT_DIR.parent
if str(SCRIPT_DIR) not in sys.path:
    sys.path.insert(0, str(SCRIPT_DIR))

from diagram_ir import normalize_diagram  # noqa: E402
from interactive_html import build_interactive_html  # noqa: E402
from motion import DEFAULT_MOTION_DURATION, MOTION_PRESETS, probe_motion_runtime, render_motion_gif  # noqa: E402
from validate_svg import run_check  # noqa: E402


def _load_generator():
    spec = importlib.util.spec_from_file_location("fireworks_template_generator", SCRIPT_DIR / "generate-from-template.py")
    if spec is None or spec.loader is None:
        raise RuntimeError("cannot load diagram generator")
    module = importlib.util.module_from_spec(spec)
    sys.modules[spec.name] = module
    spec.loader.exec_module(module)
    return module


def command_doctor(_: argparse.Namespace) -> int:
    motion_runtime = probe_motion_runtime()
    probes = {
        "python": {"ok": sys.version_info >= (3, 9), "value": sys.version.split()[0]},
        "cairosvg": {"ok": importlib.util.find_spec("cairosvg") is not None},
        "rsvg-convert": {"ok": shutil.which("rsvg-convert") is not None},
        "node": {"ok": shutil.which("node") is not None},
        "ffmpeg": {"ok": shutil.which("ffmpeg") is not None},
        "motion_renderer": motion_runtime,
    }
    probes["raster_export"] = {"ok": probes["cairosvg"]["ok"] or probes["rsvg-convert"]["ok"]}
    probes["animation_export"] = {"ok": motion_runtime["ok"], "optional": True}
    print(json.dumps(probes, indent=2, sort_keys=True))
    return 0 if probes["python"]["ok"] and probes["raster_export"]["ok"] else 1


def _read_json(path: Path) -> dict[str, object]:
    return json.loads(path.read_text(encoding="utf-8"))


def command_validate(args: argparse.Namespace) -> int:
    diagram = normalize_diagram(_read_json(args.input), args.mode)
    # Validation covers the same renderer boundary as ``render``. Style 8 is
    # intentionally AI-authored/static, so accepting JSON for it here would
    # promise a render path that does not exist.
    _load_generator().parse_style(diagram.style_index)
    print(json.dumps({
        "ok": True,
        "schema_version": diagram.schema_version,
        "input_schema": diagram.input_schema,
        "mode": diagram.mode,
        "style": {
            "id": diagram.style_index,
            "name": diagram.semantic_report["visual_theme"],
        },
        "semantics": dict(diagram.semantic_report),
        "nodes": len(diagram.nodes),
        "edges": len(diagram.edges),
    }, indent=2, sort_keys=True))
    return 0


def command_render(args: argparse.Namespace) -> int:
    generator = _load_generator()
    data = _read_json(args.input)
    svg, report = generator.build_svg_with_report(args.mode, data)
    args.output.parent.mkdir(parents=True, exist_ok=True)
    args.output.write_text(svg, encoding="utf-8")
    if args.report:
        args.report.parent.mkdir(parents=True, exist_ok=True)
        args.report.write_text(json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
    print(json.dumps({"ok": True, "svg": str(args.output), "report": str(args.report) if args.report else None}, sort_keys=True))
    return 0


def command_check(args: argparse.Namespace) -> int:
    checks = args.check or ["xml", "markers", "geometry", "composition"]
    results: dict[str, object] = {}
    ok = True
    for check in checks:
        passed, details = run_check(args.svg, check)
        results[check] = {"ok": passed, "details": details}
        ok = ok and passed
    print(json.dumps({"ok": ok, "checks": results}, indent=2, sort_keys=True))
    return 0 if ok else 1


def command_inspect(args: argparse.Namespace) -> int:
    root = ET.parse(args.svg).getroot()
    roles: dict[str, int] = {}
    for element in root.iter():
        role = element.get("data-graph-role")
        if role:
            roles[role] = roles.get(role, 0) + 1
    print(json.dumps({
        "generator": root.get("data-generator"),
        "schema_version": root.get("data-schema-version"),
        "style_id": root.get("data-style-id"),
        "visual_theme": root.get("data-visual-theme"),
        "diagram_type": root.get("data-diagram-type"),
        "semantic_profile": root.get("data-semantic-profile"),
        "semantic_valid": root.get("data-semantic-valid"),
        "viewBox": root.get("viewBox"),
        "roles": roles,
    }, indent=2, sort_keys=True))
    return 0


def command_export_html(args: argparse.Namespace) -> int:
    output = build_interactive_html(
        args.svg.read_text(encoding="utf-8"),
        args.title or args.svg.stem,
        {"slug": args.slug or args.svg.stem},
    )
    args.output.parent.mkdir(parents=True, exist_ok=True)
    args.output.write_text(output, encoding="utf-8")
    print(json.dumps({"ok": True, "html": str(args.output)}, sort_keys=True))
    return 0


def command_animate(args: argparse.Namespace) -> int:
    report = args.report or args.output.with_suffix(".motion.json")
    result = render_motion_gif(
        args.input,
        args.output,
        report_path=report,
        preset=args.preset,
        duration=args.duration,
        fps=args.fps,
        width=args.width,
        dry_run=args.dry_run,
    )
    print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
    return 0


def command_examples(_: argparse.Namespace) -> int:
    examples = [str(path.relative_to(ROOT)) for path in sorted((ROOT / "fixtures").glob("*")) if path.suffix in {".json", ".svg"}]
    print(json.dumps({"examples": examples}, indent=2))
    return 0


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description=__doc__)
    subparsers = parser.add_subparsers(dest="command", required=True)
    subparsers.add_parser("doctor").set_defaults(func=command_doctor)

    validate = subparsers.add_parser("validate", help="validate and normalize diagram JSON")
    validate.add_argument("mode")
    validate.add_argument("input", type=Path)
    validate.set_defaults(func=command_validate)

    render = subparsers.add_parser("render", help="render diagram JSON to SVG")
    render.add_argument("mode")
    render.add_argument("input", type=Path)
    render.add_argument("output", type=Path)
    render.add_argument("--report", type=Path)
    render.set_defaults(func=command_render)

    check = subparsers.add_parser("check", help="check an SVG artifact")
    check.add_argument("svg", type=Path)
    check.add_argument("--check", action="append", choices=("xml", "markers", "collisions", "geometry", "composition"))
    check.set_defaults(func=command_check)

    inspect = subparsers.add_parser("inspect", help="print semantic SVG metadata")
    inspect.add_argument("svg", type=Path)
    inspect.set_defaults(func=command_inspect)

    export = subparsers.add_parser("export-html", help="create an offline interactive HTML viewer")
    export.add_argument("svg", type=Path)
    export.add_argument("output", type=Path)
    export.add_argument("--title")
    export.add_argument("--slug")
    export.set_defaults(func=command_export_html)

    animate = subparsers.add_parser(
        "animate",
        help="create a validated animated GIF from a generated semantic SVG",
    )
    animate.add_argument("input", type=Path)
    animate.add_argument("output", type=Path)
    animate.add_argument("--preset", choices=("auto", *MOTION_PRESETS), default="auto")
    animate.add_argument("--duration", type=float, default=DEFAULT_MOTION_DURATION)
    animate.add_argument("--fps", type=int, default=20)
    animate.add_argument("--width", type=int, default=960)
    animate.add_argument("--report", type=Path)
    animate.add_argument("--dry-run", action="store_true")
    animate.set_defaults(func=command_animate)

    subparsers.add_parser("examples").set_defaults(func=command_examples)
    return parser


def main(argv: Optional[Sequence[str]] = None) -> int:
    args = build_parser().parse_args(argv)
    try:
        return int(args.func(args))
    except (OSError, RuntimeError, ValueError, json.JSONDecodeError, ET.ParseError) as error:
        print(json.dumps({"ok": False, "error": str(error)}, ensure_ascii=False), file=sys.stderr)
        return 1


if __name__ == "__main__":
    raise SystemExit(main())
skills/fireworks-tech-graph/scripts/test-all-styles.sh
#!/bin/bash
# Batch Test Script
# Renders regression fixtures, validates SVGs, and exports PNGs

set -euo pipefail

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'

SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TEST_DIR="${TEST_OUTPUT_DIR:-${SKILL_DIR}/test-output}"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)

echo -e "${BLUE}=== Fireworks Tech Graph - Batch Test ===${NC}"
echo "Test directory: $TEST_DIR"
echo "Timestamp: $TIMESTAMP"
echo ""

# Create test directory
mkdir -p "$TEST_DIR"

# Test configuration
STYLES=(1 2 3 4 5 6 7 8 9 10 11 12)
STYLE_NAMES=("Flat Icon" "Dark Terminal" "Blueprint" "Notion Clean" "Glassmorphism" "Claude Official" "OpenAI" "Dark Luxury" "C4 Review Canvas" "Cloud Fabric" "Event Transit" "Ops Pulse")
PNG_WIDTH=1920

export_png() {
    local svg_file="$1"
    local png_file="$2"

    # Keep both renderer branches on the same public 1920px contract. Using
    # CairoSVG's scale would make output width depend on each SVG viewBox.
    if python3 -c "import cairosvg" 2>/dev/null \
        && python3 -c "import sys, cairosvg; cairosvg.svg2png(url=sys.argv[1], write_to=sys.argv[2], output_width=int(sys.argv[3]))" \
            "$svg_file" "$png_file" "$PNG_WIDTH" 2>/dev/null; then
        return 0
    fi
    if command -v rsvg-convert &> /dev/null \
        && rsvg-convert -w "$PNG_WIDTH" "$svg_file" -o "$png_file" 2>/dev/null; then
        return 0
    fi
    return 1
}

# Summary counters
TOTAL=0
PASSED=0
FAILED=0

FIXTURES_DIR="${SKILL_DIR}/fixtures"

echo -e "${BLUE}Testing all styles...${NC}"
echo "----------------------------------------"

for i in "${!STYLES[@]}"; do
    STYLE="${STYLES[$i]}"
    STYLE_NAME="${STYLE_NAMES[$i]}"

    echo -e "\n${YELLOW}Style $STYLE: $STYLE_NAME${NC}"

    # Check if style reference exists
    STYLE_FILE=$(find "${SKILL_DIR}/references" -maxdepth 1 -type f -name "style-${STYLE}-*.md" | head -n 1)
    if [ -z "${STYLE_FILE:-}" ] || [ ! -f "$STYLE_FILE" ]; then
        echo -e "${RED}✗ Style file not found: $STYLE_FILE${NC}"
        FAILED=$((FAILED + 1))
        TOTAL=$((TOTAL + 1))
        continue
    fi

    echo -e "${GREEN}✓ Style file found${NC}"

    if [ ! -d "$FIXTURES_DIR" ]; then
        echo -e "${RED}✗ Fixtures directory not found: $FIXTURES_DIR${NC}"
        FAILED=$((FAILED + 1))
        TOTAL=$((TOTAL + 1))
        continue
    fi

    FIXTURE_FILES=$(find "$FIXTURES_DIR" -maxdepth 1 -type f -name "*.json" | sort || true)
    MATCHED_FIXTURES=()
    MATCHED_COUNT=0
    for FIXTURE in $FIXTURE_FILES; do
        FIXTURE_STYLE=$(python3 - "$FIXTURE" <<'PY'
import json
import sys
from pathlib import Path
data = json.loads(Path(sys.argv[1]).read_text(encoding='utf-8'))
print(data.get("style", ""))
PY
)
        if [ "$FIXTURE_STYLE" = "$STYLE" ]; then
            MATCHED_FIXTURES+=("$FIXTURE")
            MATCHED_COUNT=$((MATCHED_COUNT + 1))
        fi
    done

    STATIC_FIXTURE_FILES=$(find "$FIXTURES_DIR" -maxdepth 1 -type f -name "*-style${STYLE}.svg" | sort || true)
    if [ "$MATCHED_COUNT" -eq 0 ] && [ -z "$STATIC_FIXTURE_FILES" ]; then
        echo -e "${RED}✗ No regression fixtures found for style $STYLE${NC}"
        FAILED=$((FAILED + 1))
        TOTAL=$((TOTAL + 1))
        continue
    fi

    # Render, validate, and export each fixture
    if [ "$MATCHED_COUNT" -gt 0 ]; then
      for FIXTURE in "${MATCHED_FIXTURES[@]}"; do
        BASENAME=$(basename "$FIXTURE" .json)
        SVG_FILE="${TEST_DIR}/${BASENAME}_${TIMESTAMP}.svg"
        PNG_FILE="${TEST_DIR}/${BASENAME}_${TIMESTAMP}.png"
        TEMPLATE_TYPE=$(python3 - "$FIXTURE" <<'PY'
import json
import sys
from pathlib import Path
data = json.loads(Path(sys.argv[1]).read_text(encoding='utf-8'))
print(data.get("template_type", "architecture"))
PY
)

        echo -n "  Rendering $BASENAME... "
        TOTAL=$((TOTAL + 1))

        if python3 "${SKILL_DIR}/scripts/generate-from-template.py" "$TEMPLATE_TYPE" "$SVG_FILE" "$(cat "$FIXTURE")" > /dev/null 2>&1 \
            && "${SKILL_DIR}/scripts/validate-svg.sh" "$SVG_FILE" > /dev/null 2>&1; then
            # Prefer CairoSVG (best CSS support); fall back to rsvg-convert.
            if export_png "$SVG_FILE" "$PNG_FILE"; then
                PNG_SIZE=$(du -h "$PNG_FILE" | cut -f1)
                echo -e "${GREEN}✓ Pass${NC} (${PNG_SIZE})"
                PASSED=$((PASSED + 1))
            else
                echo -e "${RED}✗ Fail${NC} (PNG export failed)"
                FAILED=$((FAILED + 1))
            fi
        else
            echo -e "${RED}✗ Fail${NC}"
            FAILED=$((FAILED + 1))
            if [ -f "$SVG_FILE" ]; then
                "${SKILL_DIR}/scripts/validate-svg.sh" "$SVG_FILE" 2>&1 | grep -E "✗|Error" | sed 's/^/    /' || true
            fi
        fi
      done
    fi

    # AI-authored styles use static SVG fixtures because the template generator
    # intentionally does not own their visual composition.
    for FIXTURE in $STATIC_FIXTURE_FILES; do
        BASENAME=$(basename "$FIXTURE" .svg)
        SVG_FILE="${TEST_DIR}/${BASENAME}_${TIMESTAMP}.svg"
        PNG_FILE="${TEST_DIR}/${BASENAME}_${TIMESTAMP}.png"
        echo -n "  Validating $BASENAME... "
        TOTAL=$((TOTAL + 1))
        cp "$FIXTURE" "$SVG_FILE"

        if "${SKILL_DIR}/scripts/validate-svg.sh" "$SVG_FILE" > /dev/null 2>&1; then
            if export_png "$SVG_FILE" "$PNG_FILE"; then
                PNG_SIZE=$(du -h "$PNG_FILE" | cut -f1)
                echo -e "${GREEN}✓ Pass${NC} (${PNG_SIZE})"
                PASSED=$((PASSED + 1))
            else
                echo -e "${RED}✗ Fail${NC} (PNG export failed)"
                FAILED=$((FAILED + 1))
            fi
        else
            echo -e "${RED}✗ Fail${NC}"
            FAILED=$((FAILED + 1))
            "${SKILL_DIR}/scripts/validate-svg.sh" "$SVG_FILE" 2>&1 | grep -E "✗|Error|intersects|missing marker" | sed 's/^/    /' || true
        fi
    done
done

# Print summary
echo ""
echo "========================================"
echo -e "${BLUE}Test Summary${NC}"
echo "----------------------------------------"
echo "Total tests: $TOTAL"
echo -e "${GREEN}Passed: $PASSED${NC}"
echo -e "${RED}Failed: $FAILED${NC}"

if [ "$FAILED" -eq 0 ]; then
    echo -e "\n${GREEN}✓ All tests passed!${NC}"
    exit 0
else
    echo -e "\n${RED}✗ Some tests failed${NC}"
    exit 1
fi
skills/fireworks-tech-graph/scripts/validate-svg.sh
#!/bin/bash
# SVG Validation Script
# Checks SVG syntax and reports detailed errors

set -euo pipefail

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

if [ $# -eq 0 ]; then
    echo "Usage: $0 <svg-file>"
    exit 1
fi

SVG_FILE="$1"

if [ ! -f "$SVG_FILE" ]; then
    echo -e "${RED}Error: File not found: $SVG_FILE${NC}"
    exit 1
fi

echo "Validating SVG: $SVG_FILE"
echo "----------------------------------------"

FAILURES=0

# Check 0: XML structure and attribute syntax
echo -n "Checking XML structure... "
if XML_ERR=$(python3 "${SCRIPT_DIR}/validate_svg.py" "$SVG_FILE" --check xml 2>&1); then
    echo -e "${GREEN}✓ Pass${NC}"
else
    echo -e "${RED}✗ Fail${NC}"
    echo "$XML_ERR"
    FAILURES=$((FAILURES + 1))
fi

# Check 1: Marker references
echo -n "Checking marker references... "
if MARKER_ERR=$(python3 "${SCRIPT_DIR}/validate_svg.py" "$SVG_FILE" --check markers 2>&1); then
    echo -e "${GREEN}✓ Pass${NC}"
else
    echo -e "${RED}✗ Fail${NC}"
    echo "$MARKER_ERR"
    FAILURES=$((FAILURES + 1))
fi

# Check 2: Arrow-component collision
echo -n "Checking arrow collisions... "
set +e
COLLISION_ERR=$(python3 "${SCRIPT_DIR}/validate_svg.py" "$SVG_FILE" --check collisions 2>&1)
COLLISION_EXIT=$?
set -e

if [ "$COLLISION_EXIT" -eq 0 ]; then
    echo -e "${GREEN}✓ Pass${NC}"
else
    echo -e "${RED}✗ Fail${NC}"
    echo "$COLLISION_ERR" | sed -n '1,8p'
    FAILURES=$((FAILURES + 1))
fi

# Check 3: semantic geometry contract for generated artifacts
echo -n "Checking semantic geometry... "
if GEOMETRY_ERR=$(python3 "${SCRIPT_DIR}/validate_svg.py" "$SVG_FILE" --check geometry 2>&1); then
    echo -e "${GREEN}✓ Pass${NC}"
else
    echo -e "${RED}✗ Fail${NC}"
    echo "$GEOMETRY_ERR" | sed -n '1,12p'
    FAILURES=$((FAILURES + 1))
fi

# Check 4: composition-quality budget
echo -n "Checking composition quality... "
if COMPOSITION_ERR=$(python3 "${SCRIPT_DIR}/validate_svg.py" "$SVG_FILE" --check composition 2>&1); then
    echo -e "${GREEN}✓ Pass${NC}"
else
    echo -e "${RED}✗ Fail${NC}"
    echo "$COMPOSITION_ERR" | sed -n '1,12p'
    FAILURES=$((FAILURES + 1))
fi

# Check 5: render validation (cairosvg preferred, rsvg-convert fallback)
echo -n "Running render validation... "
RENDER_OK=false
RENDER_TOOL=""
RENDER_ERR=""
RENDER_OUTPUT=$(mktemp "${TMPDIR:-/tmp}/fireworks-tech-graph.XXXXXX")
trap 'rm -f "$RENDER_OUTPUT"' EXIT

if python3 -c "import cairosvg" 2>/dev/null; then
    RENDER_TOOL="cairosvg"
    if RENDER_ERR=$(python3 -c "import sys, cairosvg; cairosvg.svg2png(url=sys.argv[1], write_to=sys.argv[2])" "$SVG_FILE" "$RENDER_OUTPUT" 2>&1); then
        RENDER_OK=true
    fi
elif command -v rsvg-convert &> /dev/null; then
    RENDER_TOOL="rsvg-convert"
    if RENDER_ERR=$(rsvg-convert "$SVG_FILE" -o "$RENDER_OUTPUT" 2>&1); then
        RENDER_OK=true
    fi
fi

if [ "$RENDER_OK" = true ]; then
    echo -e "${GREEN}✓ Pass${NC} (via ${RENDER_TOOL})"
    rm -f "$RENDER_OUTPUT"
elif [ -n "$RENDER_TOOL" ]; then
    echo -e "${RED}✗ Fail${NC} (via ${RENDER_TOOL})"
    echo "${RENDER_TOOL} error:"
    echo "$RENDER_ERR"
    FAILURES=$((FAILURES + 1))
else
    echo -e "${RED}✗ Fail${NC} (no renderer found — install with: python3 -m pip install cairosvg)"
    FAILURES=$((FAILURES + 1))
fi

echo "----------------------------------------"
if [ "$FAILURES" -eq 0 ]; then
    echo "Validation complete"
    exit 0
fi

echo -e "${RED}Validation failed (${FAILURES} error(s))${NC}"
exit 1
skills/fireworks-tech-graph/scripts/validate_svg.py
#!/usr/bin/env python3
"""Structured SVG checks used by validate-svg.sh.

The validator intentionally uses only the Python standard library so it works
inside a freshly installed skill without adding another runtime dependency.
"""

from __future__ import annotations

import argparse
import math
import re
import sys
import xml.etree.ElementTree as ET
from dataclasses import dataclass
from pathlib import Path
from typing import Iterator, Optional, Sequence


SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
    sys.path.insert(0, str(SCRIPT_DIR))

import fireworks_geometry as geometry  # noqa: E402
import composition_quality as quality  # noqa: E402


NUMBER_RE = re.compile(r"[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?")
PATH_TOKEN_RE = re.compile(r"[AaCcHhLlMmQqSsTtVvZz]|[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?")
URL_REF_RE = re.compile(r"url\(\s*#([^\s)]+)\s*\)")
MARKER_ATTRIBUTES = ("marker-start", "marker-mid", "marker-end")
EXCLUDED_ROLES = {"background", "bridge-mask", "container", "decoration", "label", "legend", "reserved"}
IDENTITY = (1.0, 0.0, 0.0, 1.0, 0.0, 0.0)

Point = tuple[float, float]
Matrix = tuple[float, float, float, float, float, float]


@dataclass(frozen=True)
class Bounds:
    left: float
    top: float
    right: float
    bottom: float


@dataclass(frozen=True)
class Collision:
    edge: str
    obstacle: str


@dataclass(frozen=True)
class ElementContext:
    element: ET.Element
    matrix: Matrix
    role: Optional[str]
    in_defs: bool


def local_name(tag: str) -> str:
    return tag.split("}", 1)[-1]


def parse_number(value: Optional[str], default: Optional[float] = 0.0) -> Optional[float]:
    if value is None:
        return default
    match = NUMBER_RE.match(value.strip())
    if not match:
        return default
    return float(match.group(0))


def multiply(left: Matrix, right: Matrix) -> Matrix:
    a1, b1, c1, d1, e1, f1 = left
    a2, b2, c2, d2, e2, f2 = right
    return (
        a1 * a2 + c1 * b2,
        b1 * a2 + d1 * b2,
        a1 * c2 + c1 * d2,
        b1 * c2 + d1 * d2,
        a1 * e2 + c1 * f2 + e1,
        b1 * e2 + d1 * f2 + f1,
    )


def transform_point(matrix: Matrix, point: Point) -> Point:
    a, b, c, d, e, f = matrix
    x, y = point
    return (a * x + c * y + e, b * x + d * y + f)


def parse_transform(value: Optional[str]) -> Matrix:
    result = IDENTITY
    if not value:
        return result
    for name, raw_values in re.findall(r"([A-Za-z]+)\s*\(([^)]*)\)", value):
        values = [float(item) for item in NUMBER_RE.findall(raw_values)]
        name = name.lower()
        current = IDENTITY
        if name == "matrix" and len(values) == 6:
            current = tuple(values)  # type: ignore[assignment]
        elif name == "translate" and values:
            current = (1, 0, 0, 1, values[0], values[1] if len(values) > 1 else 0)
        elif name == "scale" and values:
            current = (values[0], 0, 0, values[1] if len(values) > 1 else values[0], 0, 0)
        elif name == "rotate" and values:
            angle = math.radians(values[0])
            rotation = (math.cos(angle), math.sin(angle), -math.sin(angle), math.cos(angle), 0, 0)
            if len(values) >= 3:
                cx, cy = values[1], values[2]
                current = multiply(
                    multiply((1, 0, 0, 1, cx, cy), rotation),
                    (1, 0, 0, 1, -cx, -cy),
                )
            else:
                current = rotation
        elif name == "skewx" and len(values) == 1:
            current = (1, 0, math.tan(math.radians(values[0])), 1, 0, 0)
        elif name == "skewy" and len(values) == 1:
            current = (1, math.tan(math.radians(values[0])), 0, 1, 0, 0)
        result = multiply(result, current)
    return result


def infer_role(element: ET.Element, inherited: Optional[str]) -> Optional[str]:
    explicit = element.get("data-graph-role")
    if explicit:
        return explicit.strip().lower()
    identity = " ".join(filter(None, (element.get("id"), element.get("class")))).lower()
    if any(token in identity for token in ("legend", "key-box", "key_box")):
        return "legend"
    if local_name(element.tag) == "g":
        text = " ".join("".join(child.itertext()) for child in element if local_name(child.tag) == "text").lower()
        if "legend" in text:
            return "legend"
    return inherited


def walk(element: ET.Element, matrix: Matrix = IDENTITY, role: Optional[str] = None, in_defs: bool = False) -> Iterator[ElementContext]:
    current_matrix = multiply(matrix, parse_transform(element.get("transform")))
    current_role = infer_role(element, role)
    current_in_defs = in_defs or local_name(element.tag) == "defs"
    yield ElementContext(element, current_matrix, current_role, current_in_defs)
    child_role = current_role if current_role in {"background", "bridge-mask", "decoration", "label", "legend", "node", "reserved"} else None
    for child in element:
        yield from walk(child, current_matrix, child_role, current_in_defs)


def canvas_size(root: ET.Element) -> Point:
    values = [float(item) for item in NUMBER_RE.findall(root.get("viewBox", ""))]
    if len(values) == 4:
        return values[2], values[3]
    return (
        float(parse_number(root.get("width"), 0.0) or 0.0),
        float(parse_number(root.get("height"), 0.0) or 0.0),
    )


def metadata_bounds(context: ElementContext) -> Optional[Bounds]:
    values = [float(item) for item in NUMBER_RE.findall(context.element.get("data-graph-bounds", ""))]
    if len(values) != 4:
        return None
    left, top, right, bottom = values
    if not all(math.isfinite(value) for value in values) or right < left or bottom < top:
        return None
    return transformed_bounds(
        context.matrix,
        ((left, top), (right, top), (right, bottom), (left, bottom)),
    )


def bounds_from_points(points: Sequence[Point]) -> Optional[Bounds]:
    if not points:
        return None
    xs = [point[0] for point in points]
    ys = [point[1] for point in points]
    return Bounds(min(xs), min(ys), max(xs), max(ys))


def transformed_bounds(matrix: Matrix, points: Sequence[Point]) -> Optional[Bounds]:
    return bounds_from_points([transform_point(matrix, point) for point in points])


def shape_bounds(context: ElementContext, canvas: Point) -> Optional[Bounds]:
    element = context.element
    tag = local_name(element.tag)
    role = context.role
    if context.in_defs or role in EXCLUDED_ROLES:
        return None

    declared = metadata_bounds(context)
    if declared is not None and role == "node":
        return declared

    if tag == "rect":
        x = float(parse_number(element.get("x"), 0.0) or 0.0)
        y = float(parse_number(element.get("y"), 0.0) or 0.0)
        width = parse_number(element.get("width"), None)
        height = parse_number(element.get("height"), None)
        if width is None or height is None or width <= 0 or height <= 0:
            return None
        canvas_width, canvas_height = canvas
        if role != "node":
            if element.get("x") is None and element.get("y") is None:
                return None
            if height <= 28 or width <= 8:
                return None
            nearly_canvas = canvas_width > 0 and canvas_height > 0 and width >= canvas_width * 0.9 and height >= canvas_height * 0.9
            if nearly_canvas:
                return None
            container_like = bool(element.get("stroke-dasharray")) or element.get("fill", "").strip().lower() == "none"
            if container_like and (
                (canvas_width > 0 and width >= canvas_width * 0.45)
                or (canvas_height > 0 and height >= canvas_height * 0.45)
            ):
                return None
        return transformed_bounds(
            context.matrix,
            ((x, y), (x + width, y), (x + width, y + height), (x, y + height)),
        )

    if tag in {"circle", "ellipse"}:
        cx = float(parse_number(element.get("cx"), 0.0) or 0.0)
        cy = float(parse_number(element.get("cy"), 0.0) or 0.0)
        rx = float(parse_number(element.get("r") or element.get("rx"), 0.0) or 0.0)
        ry = float(parse_number(element.get("r") or element.get("ry"), 0.0) or 0.0)
        if rx < 12 or ry < 12:
            return None
        return transformed_bounds(
            context.matrix,
            ((cx - rx, cy - ry), (cx + rx, cy - ry), (cx + rx, cy + ry), (cx - rx, cy + ry)),
        )

    if tag in {"polygon", "polyline"} and not has_marker(element):
        values = [float(item) for item in NUMBER_RE.findall(element.get("points", ""))]
        points = list(zip(values[::2], values[1::2]))
        if len(points) < 3:
            return None
        return transformed_bounds(context.matrix, points)
    return None


def has_marker(element: ET.Element) -> bool:
    return any(element.get(attribute) for attribute in MARKER_ATTRIBUTES)


def marker_references(root: ET.Element) -> tuple[set[str], set[str]]:
    definitions = {
        element.get("id", "")
        for element in root.iter()
        if local_name(element.tag) == "marker" and element.get("id")
    }
    references: set[str] = set()
    for element in root.iter():
        for attribute in MARKER_ATTRIBUTES:
            value = element.get(attribute, "")
            references.update(URL_REF_RE.findall(value))
    return definitions, references


def sample_quadratic(start: Point, control: Point, end: Point, steps: int = 12) -> list[Point]:
    return [
        (
            (1 - t) ** 2 * start[0] + 2 * (1 - t) * t * control[0] + t**2 * end[0],
            (1 - t) ** 2 * start[1] + 2 * (1 - t) * t * control[1] + t**2 * end[1],
        )
        for t in (index / steps for index in range(1, steps + 1))
    ]


def sample_cubic(start: Point, first: Point, second: Point, end: Point, steps: int = 16) -> list[Point]:
    return [
        (
            (1 - t) ** 3 * start[0] + 3 * (1 - t) ** 2 * t * first[0] + 3 * (1 - t) * t**2 * second[0] + t**3 * end[0],
            (1 - t) ** 3 * start[1] + 3 * (1 - t) ** 2 * t * first[1] + 3 * (1 - t) * t**2 * second[1] + t**3 * end[1],
        )
        for t in (index / steps for index in range(1, steps + 1))
    ]


def sample_arc(
    start: Point,
    rx: float,
    ry: float,
    rotation: float,
    large_arc: bool,
    sweep: bool,
    end: Point,
    steps: int = 20,
) -> list[Point]:
    """Sample an SVG endpoint-parameterized elliptical arc."""

    rx, ry = abs(rx), abs(ry)
    if rx <= 1e-9 or ry <= 1e-9 or start == end:
        return [end]
    phi = math.radians(rotation % 360)
    cos_phi, sin_phi = math.cos(phi), math.sin(phi)
    dx = (start[0] - end[0]) / 2
    dy = (start[1] - end[1]) / 2
    x_prime = cos_phi * dx + sin_phi * dy
    y_prime = -sin_phi * dx + cos_phi * dy
    scale = (x_prime * x_prime) / (rx * rx) + (y_prime * y_prime) / (ry * ry)
    if scale > 1:
        factor = math.sqrt(scale)
        rx *= factor
        ry *= factor
    numerator = max(
        0.0,
        rx * rx * ry * ry - rx * rx * y_prime * y_prime - ry * ry * x_prime * x_prime,
    )
    denominator = rx * rx * y_prime * y_prime + ry * ry * x_prime * x_prime
    coefficient = 0.0 if denominator <= 1e-12 else math.sqrt(numerator / denominator)
    if large_arc == sweep:
        coefficient = -coefficient
    center_x_prime = coefficient * (rx * y_prime / ry)
    center_y_prime = coefficient * (-ry * x_prime / rx)
    center_x = cos_phi * center_x_prime - sin_phi * center_y_prime + (start[0] + end[0]) / 2
    center_y = sin_phi * center_x_prime + cos_phi * center_y_prime + (start[1] + end[1]) / 2

    def angle(vector: Point) -> float:
        return math.atan2(vector[1], vector[0])

    start_angle = angle(((x_prime - center_x_prime) / rx, (y_prime - center_y_prime) / ry))
    end_angle = angle(((-x_prime - center_x_prime) / rx, (-y_prime - center_y_prime) / ry))
    delta = end_angle - start_angle
    if sweep and delta < 0:
        delta += math.tau
    elif not sweep and delta > 0:
        delta -= math.tau
    return [
        (
            center_x + rx * math.cos(start_angle + delta * index / steps) * cos_phi
            - ry * math.sin(start_angle + delta * index / steps) * sin_phi,
            center_y + rx * math.cos(start_angle + delta * index / steps) * sin_phi
            + ry * math.sin(start_angle + delta * index / steps) * cos_phi,
        )
        for index in range(1, steps + 1)
    ]


def path_routes(path_data: str, *, arc_chords: bool = False) -> list[list[Point]]:
    tokens = PATH_TOKEN_RE.findall(path_data or "")
    routes: list[list[Point]] = []
    points: list[Point] = []
    index = 0
    command = ""
    current = (0.0, 0.0)
    start = current
    previous_cubic: Optional[Point] = None
    previous_quadratic: Optional[Point] = None

    def read(count: int) -> Optional[list[float]]:
        nonlocal index
        if index + count > len(tokens) or any(re.fullmatch(r"[A-Za-z]", token) for token in tokens[index : index + count]):
            return None
        values = [float(token) for token in tokens[index : index + count]]
        index += count
        return values

    def absolute(x: float, y: float, relative: bool) -> Point:
        return (current[0] + x, current[1] + y) if relative else (x, y)

    while index < len(tokens):
        if re.fullmatch(r"[A-Za-z]", tokens[index]):
            command = tokens[index]
            index += 1
        if not command:
            return []
        relative = command.islower()
        op = command.upper()
        if op == "Z":
            if current != start:
                points.append(start)
            current = start
            previous_cubic = previous_quadratic = None
            command = ""
            continue
        count = {"M": 2, "L": 2, "H": 1, "V": 1, "C": 6, "S": 4, "Q": 4, "T": 2, "A": 7}.get(op)
        if count is None:
            return []
        values = read(count)
        if values is None:
            return []

        if op == "M":
            if points:
                routes.append(points)
            current = absolute(values[0], values[1], relative)
            start = current
            points = [current]
            command = "l" if relative else "L"
        elif op == "L":
            current = absolute(values[0], values[1], relative)
            points.append(current)
        elif op == "H":
            current = (current[0] + values[0], current[1]) if relative else (values[0], current[1])
            points.append(current)
        elif op == "V":
            current = (current[0], current[1] + values[0]) if relative else (current[0], values[0])
            points.append(current)
        elif op == "C":
            first = absolute(values[0], values[1], relative)
            second = absolute(values[2], values[3], relative)
            end = absolute(values[4], values[5], relative)
            points.extend(sample_cubic(current, first, second, end))
            current, previous_cubic = end, second
            previous_quadratic = None
        elif op == "S":
            first = (2 * current[0] - previous_cubic[0], 2 * current[1] - previous_cubic[1]) if previous_cubic else current
            second = absolute(values[0], values[1], relative)
            end = absolute(values[2], values[3], relative)
            points.extend(sample_cubic(current, first, second, end))
            current, previous_cubic = end, second
            previous_quadratic = None
        elif op == "Q":
            control = absolute(values[0], values[1], relative)
            end = absolute(values[2], values[3], relative)
            points.extend(sample_quadratic(current, control, end))
            current, previous_quadratic = end, control
            previous_cubic = None
        elif op == "T":
            control = (2 * current[0] - previous_quadratic[0], 2 * current[1] - previous_quadratic[1]) if previous_quadratic else current
            end = absolute(values[0], values[1], relative)
            points.extend(sample_quadratic(current, control, end))
            current, previous_quadratic = end, control
            previous_cubic = None
        elif op == "A":
            end = absolute(values[5], values[6], relative)
            if arc_chords:
                points.append(end)
            else:
                points.extend(
                    sample_arc(
                        current,
                        values[0],
                        values[1],
                        values[2],
                        bool(values[3]),
                        bool(values[4]),
                        end,
                    )
                )
            current = end
            previous_cubic = previous_quadratic = None
        if op not in {"C", "S", "Q", "T"}:
            previous_cubic = previous_quadratic = None
    if points:
        routes.append(points)
    return routes


def edge_routes(context: ElementContext) -> list[list[Point]]:
    element = context.element
    tag = local_name(element.tag)
    explicit_edge = context.role == "edge"
    if (
        context.in_defs
        or context.role in {"background", "bridge-mask", "decoration", "label", "legend", "node", "reserved"}
        or (not explicit_edge and not has_marker(element))
    ):
        return []
    if tag == "line":
        routes = [[
            (float(parse_number(element.get("x1"), 0.0) or 0.0), float(parse_number(element.get("y1"), 0.0) or 0.0)),
            (float(parse_number(element.get("x2"), 0.0) or 0.0), float(parse_number(element.get("y2"), 0.0) or 0.0)),
        ]]
    elif tag == "polyline":
        values = [float(item) for item in NUMBER_RE.findall(element.get("points", ""))]
        routes = [list(zip(values[::2], values[1::2]))]
    elif tag == "path":
        routes = path_routes(
            element.get("d", ""),
            arc_chords=bool(element.get("data-bridges")),
        )
    else:
        return []
    return [
        [transform_point(context.matrix, point) for point in route]
        for route in routes
    ]


def segment_hits_bounds(start: Point, end: Point, bounds: Bounds, epsilon: float = 1e-5) -> bool:
    left, right = bounds.left + epsilon, bounds.right - epsilon
    top, bottom = bounds.top + epsilon, bounds.bottom - epsilon
    if left >= right or top >= bottom:
        return False
    x1, y1 = start
    dx, dy = end[0] - x1, end[1] - y1
    low, high = 0.0, 1.0
    for p, q in ((-dx, x1 - left), (dx, right - x1), (-dy, y1 - top), (dy, bottom - y1)):
        if abs(p) < epsilon:
            if q < 0:
                return False
            continue
        ratio = q / p
        if p < 0:
            low = max(low, ratio)
        else:
            high = min(high, ratio)
        if low > high:
            return False
    return high - low > epsilon and high > epsilon and low < 1 - epsilon


def points_within_bounds(points: Sequence[Point], bounds: Bounds, epsilon: float = 1e-5) -> bool:
    return bool(points) and all(
        bounds.left - epsilon <= x <= bounds.right + epsilon
        and bounds.top - epsilon <= y <= bounds.bottom + epsilon
        for x, y in points
    )


def find_collisions(root: ET.Element) -> list[Collision]:
    contexts = list(walk(root))
    obstacles = [
        (context, bounds)
        for context in contexts
        if (bounds := shape_bounds(context, canvas_size(root))) is not None
    ]
    edges = [(context, edge_routes(context)) for context in contexts]
    legend_obstacles = [
        (context, bounds)
        for context in contexts
        if context.role == "legend" and (bounds := role_bounds(context)) is not None
    ]
    obstacles.extend(legend_obstacles)
    collisions: list[Collision] = []
    for edge_context, routes in edges:
        if not any(len(route) >= 2 for route in routes):
            continue
        # A marker path wholly inside a recognized legend is decoration. The
        # legend remains a hard obstacle for every business edge outside it.
        if routes and all(
            any(points_within_bounds(route, bounds) for _, bounds in legend_obstacles)
            for route in routes
        ):
            continue
        edge = edge_context.element
        for obstacle_context, bounds in obstacles:
            obstacle = obstacle_context.element
            if any(
                segment_hits_bounds(first, second, bounds)
                for route in routes
                for first, second in zip(route, route[1:])
            ):
                collisions.append(
                    Collision(
                        describe_element(edge),
                        describe_element(obstacle),
                    )
                )
                break
    return collisions


def describe_element(element: ET.Element) -> str:
    tag = local_name(element.tag)
    if element.get("id"):
        return f"{tag}#{element.get('id')}"
    if tag == "path":
        path_data = re.sub(r"\s+", " ", element.get("d", "")).strip()
        return f"path[d={path_data[:72]}]"
    attributes = []
    for name in ("x", "y", "width", "height", "cx", "cy", "r", "rx", "ry"):
        if element.get(name) is not None:
            attributes.append(f"{name}={element.get(name)}")
    return f"{tag}[{' '.join(attributes)}]" if attributes else tag


def role_bounds(context: ElementContext) -> Optional[Bounds]:
    """Return explicit node/reserved/label bounds for the strict geometry gate."""

    declared = metadata_bounds(context)
    if declared is not None:
        return declared
    element = context.element
    tag = local_name(element.tag)
    if tag == "rect":
        x = float(parse_number(element.get("x"), 0.0) or 0.0)
        y = float(parse_number(element.get("y"), 0.0) or 0.0)
        width = parse_number(element.get("width"), None)
        height = parse_number(element.get("height"), None)
        if width is not None and height is not None and width >= 0 and height >= 0:
            return transformed_bounds(
                context.matrix,
                ((x, y), (x + width, y), (x + width, y + height), (x, y + height)),
            )
    if tag == "text" and context.role == "label":
        x = float(parse_number(element.get("x"), 0.0) or 0.0)
        y = float(parse_number(element.get("y"), 0.0) or 0.0)
        font_size = float(parse_number(element.get("font-size"), 12.0) or 12.0)
        anchor = element.get("text-anchor", "start")
        local = geometry.estimate_text_bounds(x, y, "".join(element.itertext()), font_size=font_size, anchor=anchor)
        return transformed_bounds(
            context.matrix,
            (
                (local[0], local[1]),
                (local[2], local[1]),
                (local[2], local[3]),
                (local[0], local[3]),
            ),
        )
    return None


def bounds_inside_canvas(bounds: Bounds, canvas: Bounds, epsilon: float = 1e-5) -> bool:
    return (
        bounds.left >= canvas.left - epsilon
        and bounds.top >= canvas.top - epsilon
        and bounds.right <= canvas.right + epsilon
        and bounds.bottom <= canvas.bottom + epsilon
    )


def geometry_check(root: ET.Element) -> list[str]:
    """Audit generated business geometry using explicit semantic SVG roles."""

    contexts = list(walk(root))
    paint_order = {id(context.element): index for index, context in enumerate(contexts)}
    width, height = canvas_size(root)
    canvas = Bounds(0.0, 0.0, width, height)
    details: list[str] = []

    obstacles: list[tuple[ElementContext, Bounds]] = []
    labels: list[tuple[ElementContext, Bounds]] = []
    bridge_masks: dict[str, ElementContext] = {}
    edges: list[tuple[ElementContext, list[list[Point]]]] = []
    matched_bridges: dict[str, list[Point]] = {}

    for context in contexts:
        if context.in_defs:
            continue
        if context.role in {"node", "reserved"}:
            if context.role == "node" and context.element.get("data-graph-role") != "node" and metadata_bounds(context) is None:
                continue
            bounds = role_bounds(context)
            if bounds is not None:
                obstacles.append((context, bounds))
        elif context.role == "label":
            if context.element.get("data-graph-role") != "label" and metadata_bounds(context) is None:
                continue
            bounds = role_bounds(context)
            if bounds is not None:
                labels.append((context, bounds))
        elif context.role == "bridge-mask":
            owner = context.element.get("data-owner", "")
            if owner:
                bridge_masks[owner] = context
        elif context.role == "edge":
            routes = edge_routes(context)
            if any(len(route) >= 2 for route in routes):
                edges.append((context, routes))

    for context, bounds in [*obstacles, *labels]:
        if not bounds_inside_canvas(bounds, canvas):
            details.append(f"canvas_clip: {describe_element(context.element)} exceeds viewBox")

    for edge_context, routes in edges:
        edge = edge_context.element
        edge_name = describe_element(edge)
        if not all(
            geometry.route_inside_canvas(route, (canvas.left, canvas.top, canvas.right, canvas.bottom))
            for route in routes
            if len(route) >= 2
        ):
            details.append(f"canvas_clip: {edge_name} exceeds viewBox")
        if root.get("data-generator") == "fireworks-tech-graph" and not all(
            geometry.route_is_orthogonal(route)
            for route in routes
            if len(route) >= 2
        ):
            details.append(f"non_orthogonal: {edge_name}")
        for obstacle_context, bounds in obstacles:
            obstacle_node_id = obstacle_context.element.get("data-node-id", "")
            if obstacle_node_id and obstacle_node_id in {
                edge.get("data-source", ""),
                edge.get("data-target", ""),
            }:
                continue
            if any(
                segment_hits_bounds(first, second, bounds)
                for route in routes
                for first, second in zip(route, route[1:])
            ):
                role = obstacle_context.role or "obstacle"
                details.append(
                    f"edge_{role}: {edge_name} intersects {describe_element(obstacle_context.element)}"
                )

    for first_index, (first_context, first_routes) in enumerate(edges):
        first_element = first_context.element
        first_name = describe_element(first_element)
        for second_context, second_routes in edges[first_index + 1 :]:
            second_element = second_context.element
            second_name = describe_element(second_element)
            interactions = [
                geometry.route_interactions(
                    first_route,
                    [route for route in second_routes if len(route) >= 2],
                )
                for first_route in first_routes
                if len(first_route) >= 2
            ]
            if any(item.overlap_count for item in interactions):
                details.append(f"edge_overlap: {first_name} overlaps {second_name}")
            for point in [point for item in interactions for point in item.crossings]:
                first_bridges = geometry.parse_bridge_points(first_element.get("data-bridges"))
                second_bridges = geometry.parse_bridge_points(second_element.get("data-bridges"))
                first_owner = first_element.get("data-edge-id") or first_element.get("id", "")
                second_owner = second_element.get("data-edge-id") or second_element.get("id", "")
                first_mask = bridge_masks.get(first_owner)
                second_mask = bridge_masks.get(second_owner)
                first_valid = (
                    geometry.bridge_declared(point, first_bridges)
                    and first_mask is not None
                    and paint_order[id(second_element)] < paint_order[id(first_mask.element)] < paint_order[id(first_element)]
                    and first_mask.element.get("d") == first_element.get("d")
                    and first_mask.matrix == first_context.matrix
                )
                second_valid = (
                    geometry.bridge_declared(point, second_bridges)
                    and second_mask is not None
                    and paint_order[id(first_element)] < paint_order[id(second_mask.element)] < paint_order[id(second_element)]
                    and second_mask.element.get("d") == second_element.get("d")
                    and second_mask.matrix == second_context.matrix
                )
                # Backwards compatibility for authored SVGs predating bridge masks.
                if root.get("data-generator") != "fireworks-tech-graph":
                    first_valid = first_valid or geometry.bridge_declared(point, first_bridges)
                    second_valid = second_valid or geometry.bridge_declared(point, second_bridges)
                if not (first_valid or second_valid):
                    declared = (
                        geometry.bridge_declared(point, first_bridges)
                        or geometry.bridge_declared(point, second_bridges)
                    )
                    code = "bridge_paint_order" if declared else "edge_crossing"
                    details.append(
                        f"{code}: {first_name} crosses {second_name} at {point[0]:.2f},{point[1]:.2f}"
                    )
                if first_valid:
                    matched_bridges.setdefault(first_owner, []).append(point)
                if second_valid:
                    matched_bridges.setdefault(second_owner, []).append(point)

    for label_index, (label_context, label_bounds) in enumerate(labels):
        label_name = describe_element(label_context.element)
        owner = label_context.element.get("data-owner", "")
        label_tuple = (label_bounds.left, label_bounds.top, label_bounds.right, label_bounds.bottom)
        for obstacle_context, obstacle_bounds in obstacles:
            obstacle_tuple = (obstacle_bounds.left, obstacle_bounds.top, obstacle_bounds.right, obstacle_bounds.bottom)
            if geometry.bounds_intersect(label_tuple, obstacle_tuple):
                details.append(f"label_obstacle: {label_name} intersects {describe_element(obstacle_context.element)}")
        for edge_context, edge_routes_for_context in edges:
            edge = edge_context.element
            edge_owner = edge.get("data-edge-id") or edge.get("id", "")
            if edge_owner == owner:
                continue
            if any(
                segment_hits_bounds(first, second, label_bounds)
                for route in edge_routes_for_context
                for first, second in zip(route, route[1:])
            ):
                details.append(f"label_edge: {label_name} intersects {describe_element(edge)}")
        for other_context, other_bounds in labels[label_index + 1 :]:
            other_tuple = (other_bounds.left, other_bounds.top, other_bounds.right, other_bounds.bottom)
            if geometry.bounds_intersect(label_tuple, other_tuple):
                details.append(f"label_overlap: {label_name} intersects {describe_element(other_context.element)}")

    for edge_context, _ in edges:
        edge = edge_context.element
        owner = edge.get("data-edge-id") or edge.get("id", "")
        bridges = geometry.parse_bridge_points(edge.get("data-bridges"))
        if bridges and owner not in bridge_masks and root.get("data-generator") == "fireworks-tech-graph":
            details.append(f"bridge_mask_missing: {describe_element(edge)}")
        for bridge in bridges:
            if not geometry.bridge_declared(bridge, matched_bridges.get(owner, [])):
                details.append(
                    f"bridge_without_crossing: {describe_element(edge)} declares {bridge[0]:.2f},{bridge[1]:.2f}"
                )

    return sorted(set(details))


def composition_contract(root: ET.Element) -> quality.CompositionContract:
    """Read the portable quality contract embedded in the SVG root."""

    raw: dict[str, object] = {
        "profile": root.get("data-quality-profile", "standard"),
    }
    attributes = {
        "max_bends_per_edge": "data-max-bends-per-edge",
        "max_total_bends": "data-max-total-bends",
        "max_route_stretch": "data-max-route-stretch",
        "max_bridged_crossings": "data-max-bridged-crossings",
        "min_node_gap": "data-min-node-gap",
        "min_container_gutter": "data-min-container-gutter",
        "min_label_clearance": "data-min-label-clearance",
        "min_segment_length": "data-min-segment-length",
    }
    for key, attribute in attributes.items():
        if root.get(attribute) is not None:
            raw[key] = root.get(attribute, "")
    return quality.resolve_contract(raw)


def composition_check(root: ET.Element) -> list[str]:
    """Enforce visual-composition budgets in addition to collision safety."""

    contexts = list(walk(root))
    contract = composition_contract(root)
    nodes: list[tuple[str, tuple[float, float, float, float]]] = []
    containers: list[tuple[str, tuple[float, float, float, float]]] = []
    labels: list[tuple[ElementContext, Bounds]] = []
    edges: list[tuple[ElementContext, list[list[Point]]]] = []

    for context in contexts:
        if context.in_defs:
            continue
        explicit = context.element.get("data-graph-role")
        bounds = role_bounds(context)
        if context.role == "node" and bounds is not None and (explicit == "node" or metadata_bounds(context) is not None):
            nodes.append(
                (
                    context.element.get("data-node-id") or context.element.get("id") or describe_element(context.element),
                    (bounds.left, bounds.top, bounds.right, bounds.bottom),
                )
            )
        elif context.role == "container" and bounds is not None and explicit == "container":
            containers.append(
                (
                    context.element.get("id") or describe_element(context.element),
                    (bounds.left, bounds.top, bounds.right, bounds.bottom),
                )
            )
        elif context.role == "label" and bounds is not None and (explicit == "label" or metadata_bounds(context) is not None):
            labels.append((context, bounds))
        if context.role == "edge" or (context.role is None and has_marker(context.element)):
            routes = edge_routes(context)
            if any(len(route) >= 2 for route in routes):
                edges.append((context, routes))

    edge_records = []
    for context, routes in edges:
        edge = context.element
        edge_id = edge.get("data-edge-id") or edge.get("id") or describe_element(edge)
        valid_routes = [route for route in routes if len(route) >= 2]
        declared_bridges = geometry.parse_bridge_points(edge.get("data-bridges"))
        for index, route in enumerate(valid_routes):
            edge_records.append(
                {
                    "id": edge_id if len(valid_routes) == 1 else f"{edge_id}:{index + 1}",
                    "route": route,
                    "bends": geometry.bend_count(route),
                    # Bridges are declared once per SVG edge element even when
                    # its path contains multiple independently drawn subpaths.
                    "bridges": declared_bridges if index == 0 else [],
                }
            )
    assessment = quality.assess_composition(
        nodes=nodes,
        containers=containers,
        edges=edge_records,
        contract=contract,
    )
    details = [
        f'composition_{str(item["code"]).lower()}: {item["element"]} '
        f'actual={item["actual"]} limit={item["limit"]}'
        for item in assessment["violations"]
    ]

    clearance = contract.min_label_clearance
    obstacle_bounds = [Bounds(*bounds) for _, bounds in nodes]
    for index, (label_context, label_bounds) in enumerate(labels):
        owner = label_context.element.get("data-owner", "")
        expanded = Bounds(
            label_bounds.left - clearance,
            label_bounds.top - clearance,
            label_bounds.right + clearance,
            label_bounds.bottom + clearance,
        )
        for obstacle in obstacle_bounds:
            if geometry.bounds_intersect(
                (expanded.left, expanded.top, expanded.right, expanded.bottom),
                (obstacle.left, obstacle.top, obstacle.right, obstacle.bottom),
            ):
                details.append(
                    f"composition_label_clearance: {describe_element(label_context.element)} is too close to a node"
                )
                break
        for edge_context, routes in edges:
            edge = edge_context.element
            edge_owner = edge.get("data-edge-id") or edge.get("id", "")
            if edge_owner == owner:
                continue
            if any(
                segment_hits_bounds(first, second, expanded)
                for route in routes
                for first, second in zip(route, route[1:])
            ):
                details.append(
                    f"composition_label_clearance: {describe_element(label_context.element)} is too close to {describe_element(edge)}"
                )
        for other_context, other_bounds in labels[index + 1 :]:
            if geometry.bounds_intersect(
                (expanded.left, expanded.top, expanded.right, expanded.bottom),
                (other_bounds.left, other_bounds.top, other_bounds.right, other_bounds.bottom),
            ):
                details.append(
                    f"composition_label_clearance: {describe_element(label_context.element)} is too close to {describe_element(other_context.element)}"
                )
    return sorted(set(details))


def parse_svg(path: Path) -> ET.Element:
    return ET.parse(path).getroot()


def run_check(path: Path, check: str) -> tuple[bool, list[str]]:
    try:
        root = parse_svg(path)
    except (ET.ParseError, OSError) as error:
        return False, [str(error)]
    if check == "xml":
        return True, []
    if check == "markers":
        definitions, references = marker_references(root)
        missing = sorted(references - definitions)
        return not missing, [f"missing marker: {marker}" for marker in missing]
    if check == "geometry":
        details = geometry_check(root)
        return not details, details
    if check == "composition":
        details = composition_check(root)
        return not details, details
    collisions = find_collisions(root)
    details = [
        f"{item.edge} intersects {item.obstacle}"
        for item in collisions
    ]
    return not collisions, details


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("svg_file", type=Path)
    parser.add_argument("--check", choices=("xml", "markers", "collisions", "geometry", "composition"), required=True)
    return parser.parse_args()


def main() -> int:
    args = parse_args()
    ok, details = run_check(args.svg_file, args.check)
    for detail in details:
        print(detail)
    return 0 if ok else 1


if __name__ == "__main__":
    raise SystemExit(main())
skills/fireworks-tech-graph/tests/test_interactive_html.py
from __future__ import annotations

import importlib.util
import sys
import unittest
from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]
SPEC = importlib.util.spec_from_file_location("interactive_html", ROOT / "scripts" / "interactive_html.py")
assert SPEC and SPEC.loader
interactive = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = interactive
SPEC.loader.exec_module(interactive)


SAFE_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 80"><rect width="100" height="80"/></svg>'


class InteractiveHTMLTest(unittest.TestCase):
    def test_builds_offline_viewer_with_complete_controls(self) -> None:
        output = interactive.build_interactive_html(SAFE_SVG, "Architecture <v1>", {"slug": "sample"})
        self.assertIn("Architecture &lt;v1&gt;", output)
        self.assertIn('data-action="zoom-in"', output)
        self.assertIn('data-action="reset"', output)
        self.assertIn('data-action="theme"', output)
        self.assertIn('data-action="copy"', output)
        for image_format in ("SVG", "PNG", "JPEG", "WebP"):
            self.assertIn(f"<option>{image_format}</option>", output)
        self.assertIn("Content-Security-Policy", output)
        self.assertNotIn("https://", output)

    def test_sanitizer_rejects_active_and_external_content(self) -> None:
        unsafe = (
            '<svg xmlns="http://www.w3.org/2000/svg">'
            '<script>alert(1)</script><image href="https://example.com/a.png" onload="x()"/>'
            '</svg>'
        )
        with self.assertRaisesRegex(ValueError, "unsupported SVG element"):
            interactive.sanitize_svg(unsafe)
        with self.assertRaisesRegex(ValueError, "external reference"):
            interactive.sanitize_svg(
                '<svg xmlns="http://www.w3.org/2000/svg"><rect width="10" height="10" fill="url(https://example.com/a.svg#paint)"/></svg>'
            )
        with self.assertRaisesRegex(ValueError, "event handler"):
            interactive.sanitize_svg(
                '<svg xmlns="http://www.w3.org/2000/svg"><rect onclick="x()"/></svg>'
            )

    def test_sanitizer_rejects_foreign_object_and_external_css(self) -> None:
        with self.assertRaisesRegex(ValueError, "foreignobject"):
            interactive.sanitize_svg(
                '<svg xmlns="http://www.w3.org/2000/svg"><foreignObject/></svg>'
            )
        with self.assertRaisesRegex(ValueError, "external or active CSS"):
            interactive.sanitize_svg(
                '<svg xmlns="http://www.w3.org/2000/svg"><style>@import url(https://example.com/x.css)</style></svg>'
            )
        with self.assertRaisesRegex(ValueError, "DTD"):
            interactive.sanitize_svg(
                '<!DOCTYPE svg [<!ENTITY x "boom">]><svg xmlns="http://www.w3.org/2000/svg"><text>&x;</text></svg>'
            )

    def test_sanitizer_rejects_smil_navigation_and_external_paint(self) -> None:
        unsafe_samples = (
            '<svg xmlns="http://www.w3.org/2000/svg"><set href="#go" attributeName="href" to="javascript:alert(1)" begin="0s"/></svg>',
            '<svg xmlns="http://www.w3.org/2000/svg"><a href="data:text/html,&lt;script&gt;alert(1)&lt;/script&gt;">go</a></svg>',
            '<svg xmlns="http://www.w3.org/2000/svg"><rect width="10" height="10" filter="url(https://attacker.invalid/f.svg#x)"/></svg>',
        )
        for sample in unsafe_samples:
            with self.subTest(sample=sample):
                with self.assertRaises(ValueError):
                    interactive.sanitize_svg(sample)

    def test_sanitizer_accepts_static_local_paint_servers(self) -> None:
        safe = (
            '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 80">'
            '<defs><filter id="shadow"><feDropShadow dx="1" dy="2" stdDeviation="2"/></filter>'
            '<linearGradient id="gradient"><stop offset="0" stop-color="#fff"/></linearGradient>'
            '<marker id="arrow" markerWidth="8" markerHeight="8" refX="8" refY="4" orient="auto">'
            '<path d="M0 0 L8 4 L0 8 Z" fill="#111"/></marker></defs>'
            '<rect width="80" height="40" fill="url(#gradient)" filter="url(#shadow)"/>'
            '<path d="M10 60 H90" marker-end="url(#arrow)"/></svg>'
        )
        sanitized = interactive.sanitize_svg(safe)
        self.assertIn("url(#gradient)", sanitized)
        self.assertIn("url(#arrow)", sanitized)


if __name__ == "__main__":
    unittest.main()
skills/fireworks-tech-graph/scripts/interactive_html.py
#!/usr/bin/env python3
"""Build a safe, self-contained interactive HTML viewer for an SVG diagram."""

from __future__ import annotations

import argparse
import html
import json
import re
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Mapping, Optional, Sequence


ET.register_namespace("", "http://www.w3.org/2000/svg")
SVG_NAMESPACE = "http://www.w3.org/2000/svg"
XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace"
SAFE_ELEMENTS = {
    "circle",
    "clippath",
    "defs",
    "desc",
    "ellipse",
    "feblend",
    "fecolormatrix",
    "fecomposite",
    "fedropshadow",
    "feflood",
    "fegaussianblur",
    "femerge",
    "femergenode",
    "feoffset",
    "filter",
    "g",
    "line",
    "lineargradient",
    "marker",
    "mask",
    "path",
    "pattern",
    "polygon",
    "polyline",
    "radialgradient",
    "rect",
    "stop",
    "style",
    "svg",
    "text",
    "title",
    "tspan",
}
COMMON_ATTRIBUTES = {
    "aria-describedby",
    "aria-hidden",
    "aria-label",
    "aria-labelledby",
    "class",
    "clip-path",
    "clip-rule",
    "color",
    "color-interpolation",
    "color-interpolation-filters",
    "display",
    "fill",
    "fill-opacity",
    "fill-rule",
    "filter",
    "focusable",
    "id",
    "mask",
    "marker-end",
    "marker-mid",
    "marker-start",
    "opacity",
    "paint-order",
    "role",
    "shape-rendering",
    "stroke",
    "stroke-dasharray",
    "stroke-dashoffset",
    "stroke-linecap",
    "stroke-linejoin",
    "stroke-miterlimit",
    "stroke-opacity",
    "stroke-width",
    "tabindex",
    "text-rendering",
    "transform",
    "vector-effect",
    "visibility",
}
ELEMENT_ATTRIBUTES = {
    "svg": {"height", "preserveaspectratio", "viewbox", "width", "x", "y"},
    "rect": {"height", "rx", "ry", "width", "x", "y"},
    "circle": {"cx", "cy", "r"},
    "ellipse": {"cx", "cy", "rx", "ry"},
    "line": {"x1", "x2", "y1", "y2"},
    "polyline": {"pathlength", "points"},
    "polygon": {"pathlength", "points"},
    "path": {"d", "pathlength"},
    "text": {
        "dominant-baseline",
        "dx",
        "dy",
        "font-family",
        "font-size",
        "font-style",
        "font-weight",
        "letter-spacing",
        "text-anchor",
        "word-spacing",
        "x",
        "y",
    },
    "tspan": {"dominant-baseline", "dx", "dy", "text-anchor", "x", "y"},
    "marker": {
        "markerheight",
        "markerunits",
        "markerwidth",
        "orient",
        "preserveaspectratio",
        "refx",
        "refy",
        "viewbox",
    },
    "lineargradient": {"gradienttransform", "gradientunits", "spreadmethod", "x1", "x2", "y1", "y2"},
    "radialgradient": {"cx", "cy", "fr", "fx", "fy", "gradienttransform", "gradientunits", "r", "spreadmethod"},
    "stop": {"offset", "stop-color", "stop-opacity"},
    "pattern": {
        "height",
        "patterncontentunits",
        "patterntransform",
        "patternunits",
        "preserveaspectratio",
        "viewbox",
        "width",
        "x",
        "y",
    },
    "clippath": {"clippathunits"},
    "mask": {"height", "maskcontentunits", "maskunits", "width", "x", "y"},
    "filter": {"filterunits", "height", "primitiveunits", "width", "x", "y"},
    "fedropshadow": {"dx", "dy", "flood-color", "flood-opacity", "stddeviation"},
    "fegaussianblur": {"in", "result", "stddeviation"},
    "feoffset": {"dx", "dy", "in", "result"},
    "feflood": {"flood-color", "flood-opacity", "result"},
    "fecomposite": {"in", "in2", "k1", "k2", "k3", "k4", "operator", "result"},
    "femerge": {"result"},
    "femergenode": {"in"},
    "fecolormatrix": {"in", "result", "type", "values"},
    "feblend": {"in", "in2", "mode", "result"},
}
LOCAL_URL_ATTRIBUTES = {"clip-path", "fill", "filter", "marker-end", "marker-mid", "marker-start", "mask", "stroke"}
LOCAL_URL_RE = re.compile(r"url\(\s*(['\"]?)#[A-Za-z_][\w:.-]*\1\s*\)\Z", re.IGNORECASE)
ACTIVE_VALUE_RE = re.compile(r"(?:javascript\s*:|vbscript\s*:|data\s*:)", re.IGNORECASE)
UNSAFE_CSS_RE = re.compile(
    r"(?:@|\\|url\s*\(|expression\s*\(|javascript\s*:|vbscript\s*:|data\s*:|https?\s*:|//|behavior\s*:|-moz-binding)",
    re.IGNORECASE,
)


def _local_name(value: str) -> str:
    return value.rsplit("}", 1)[-1].lower()


def _namespace(value: str) -> str:
    return value[1:].split("}", 1)[0] if value.startswith("{") else ""


def sanitize_svg(svg_text: str) -> str:
    """Return safe inline SVG or raise ValueError for active/external content."""

    if len(svg_text.encode("utf-8")) > 20 * 1024 * 1024:
        raise ValueError("SVG exceeds the 20 MiB interactive-export limit")
    if re.search(r"<!\s*(?:DOCTYPE|ENTITY)\b", svg_text, re.IGNORECASE):
        raise ValueError("DTD and entity declarations are not allowed")
    try:
        root = ET.fromstring(svg_text)
    except ET.ParseError as error:
        raise ValueError(f"invalid SVG: {error}") from error
    if _local_name(root.tag) != "svg":
        raise ValueError("input root must be <svg>")

    for element in root.iter():
        tag = _local_name(element.tag)
        if _namespace(element.tag) not in {"", SVG_NAMESPACE}:
            raise ValueError(f"foreign XML namespace is not allowed: {tag}")
        if tag not in SAFE_ELEMENTS:
            raise ValueError(f"unsupported SVG element: {tag}")
        if tag == "style" and UNSAFE_CSS_RE.search(element.text or ""):
            raise ValueError("external or active CSS is not allowed")
        allowed_attributes = COMMON_ATTRIBUTES | ELEMENT_ATTRIBUTES.get(tag, set())
        for raw_name, raw_value in element.attrib.items():
            name = _local_name(raw_name)
            value = raw_value.strip()
            if name.startswith("on"):
                raise ValueError(f"event handler attribute is not allowed: {name}")
            namespace = _namespace(raw_name)
            if namespace and not (namespace == XML_NAMESPACE and name == "space"):
                raise ValueError(f"foreign attribute namespace is not allowed: {name}")
            if name != "space" and name not in allowed_attributes and not name.startswith(("aria-", "data-")):
                raise ValueError(f"unsupported SVG attribute on <{tag}>: {name}")
            if any(ord(character) < 32 and character not in "\t\n\r" for character in value):
                raise ValueError(f"control character is not allowed in attribute: {name}")
            if ACTIVE_VALUE_RE.search(value):
                raise ValueError(f"active reference is not allowed: {name}")
            if "url(" in value.lower():
                if name not in LOCAL_URL_ATTRIBUTES or not LOCAL_URL_RE.fullmatch(value):
                    raise ValueError(f"external reference is not allowed: {value}")
    root.set("role", root.get("role", "img"))
    root.set("focusable", "false")
    return ET.tostring(root, encoding="unicode")


def build_interactive_html(
    svg_text: str,
    title: str,
    metadata: Optional[Mapping[str, object]] = None,
) -> str:
    safe_svg = sanitize_svg(svg_text)
    safe_title = html.escape(title, quote=True)
    metadata_json = json.dumps(dict(metadata or {}), ensure_ascii=False, sort_keys=True).replace("<", "\\u003c")
    return f"""<!doctype html>
<html lang="en" data-theme="dark">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; img-src data: blob:">
  <title>{safe_title}</title>
  <style>
    :root {{ color-scheme: dark; --page:#0f0f1a; --panel:#0f172a; --border:#334155; --text:#e2e8f0; --muted:#94a3b8; --accent:#a855f7; }}
    html[data-theme="light"] {{ color-scheme:light; --page:#f8fafc; --panel:#fff; --border:#cbd5e1; --text:#0f172a; --muted:#475569; --accent:#7c3aed; }}
    * {{ box-sizing:border-box; }}
    html,body {{ width:100%; height:100%; margin:0; overflow:hidden; background:linear-gradient(135deg,var(--page),#1a1a2e); color:var(--text); font-family:'SF Mono','Fira Code',ui-monospace,monospace; }}
    body {{ display:grid; grid-template-rows:auto 1fr; }}
    .toolbar {{ display:flex; flex-wrap:wrap; align-items:center; gap:8px; padding:10px 14px; background:color-mix(in srgb,var(--panel) 92%,transparent); border-bottom:1px solid var(--border); z-index:2; }}
    .title {{ margin-right:auto; font-size:13px; font-weight:700; }}
    button,select {{ min-height:34px; padding:6px 10px; color:var(--text); background:var(--panel); border:1px solid var(--border); border-radius:8px; font:inherit; cursor:pointer; }}
    button:hover,button:focus-visible,select:focus-visible {{ border-color:var(--accent); outline:2px solid color-mix(in srgb,var(--accent) 35%,transparent); outline-offset:1px; }}
    #stage {{ position:relative; overflow:hidden; touch-action:none; cursor:grab; }}
    #stage.dragging {{ cursor:grabbing; }}
    #canvas {{ width:100%; height:100%; display:grid; place-items:center; transform-origin:0 0; will-change:transform; }}
    #canvas svg {{ max-width:calc(100vw - 40px); max-height:calc(100vh - 92px); width:auto; height:auto; filter:drop-shadow(0 22px 60px rgba(0,0,0,.28)); user-select:none; }}
    #status {{ min-width:76px; color:var(--muted); font-size:12px; text-align:center; }}
    .sr-only {{ position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; }}
    @media (prefers-reduced-motion:reduce) {{ * {{ scroll-behavior:auto!important; transition:none!important; }} }}
  </style>
</head>
<body>
  <header class="toolbar" aria-label="Diagram controls">
    <span class="title">{safe_title}</span>
    <button type="button" data-action="zoom-out" aria-label="Zoom out">−</button>
    <button type="button" data-action="reset" aria-label="Reset view">Reset</button>
    <button type="button" data-action="zoom-in" aria-label="Zoom in">+</button>
    <span id="status" aria-live="polite">100%</span>
    <button type="button" data-action="theme" aria-label="Toggle theme">Theme</button>
    <button type="button" data-action="copy" aria-label="Copy SVG source">Copy SVG</button>
    <select id="scale" aria-label="Raster export scale"><option value="1">1×</option><option value="2" selected>2×</option><option value="3">3×</option><option value="4">4×</option></select>
    <select id="format" aria-label="Export format"><option>SVG</option><option>PNG</option><option>JPEG</option><option>WebP</option></select>
    <button type="button" data-action="download">Export</button>
  </header>
  <main id="stage" tabindex="0" aria-label="Interactive diagram. Drag to pan; use plus and minus to zoom.">
    <div id="canvas">{safe_svg}</div>
    <p class="sr-only">Keyboard shortcuts: plus and minus zoom, zero resets, T toggles theme, S exports.</p>
  </main>
  <script>
  (() => {{
    'use strict';
    const metadata = {metadata_json};
    const stage = document.getElementById('stage');
    const canvas = document.getElementById('canvas');
    const svg = canvas.querySelector('svg');
    const status = document.getElementById('status');
    const scaleSelect = document.getElementById('scale');
    const formatSelect = document.getElementById('format');
    let view = {{ x:0, y:0, scale:1 }};
    let drag = null;
    const clamp = value => Math.max(.2, Math.min(8, value));
    const render = () => {{
      canvas.style.transform = `translate(${{view.x}}px,${{view.y}}px) scale(${{view.scale}})`;
      status.textContent = `${{Math.round(view.scale * 100)}}%`;
    }};
    const zoom = (factor, originX=stage.clientWidth/2, originY=stage.clientHeight/2) => {{
      const next = clamp(view.scale * factor);
      const ratio = next / view.scale;
      view.x = originX - (originX - view.x) * ratio;
      view.y = originY - (originY - view.y) * ratio;
      view.scale = next; render();
    }};
    const reset = () => {{ view = {{x:0,y:0,scale:1}}; render(); }};
    stage.addEventListener('wheel', event => {{ event.preventDefault(); const rect=stage.getBoundingClientRect(); zoom(event.deltaY < 0 ? 1.12 : .89, event.clientX-rect.left, event.clientY-rect.top); }}, {{passive:false}});
    stage.addEventListener('pointerdown', event => {{ drag={{id:event.pointerId,x:event.clientX,y:event.clientY,vx:view.x,vy:view.y}}; stage.setPointerCapture(event.pointerId); stage.classList.add('dragging'); }});
    stage.addEventListener('pointermove', event => {{ if(!drag||drag.id!==event.pointerId)return; view.x=drag.vx+event.clientX-drag.x; view.y=drag.vy+event.clientY-drag.y; render(); }});
    const endDrag = () => {{ drag=null; stage.classList.remove('dragging'); }};
    stage.addEventListener('pointerup', endDrag); stage.addEventListener('pointercancel', endDrag);
    const source = () => new XMLSerializer().serializeToString(svg);
    const saveBlob = (blob, extension) => {{ const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download=`${{(metadata.slug||'fireworks-tech-graph')}}.${{extension}}`; a.click(); setTimeout(()=>URL.revokeObjectURL(a.href),1000); }};
    const copySource = async () => {{
      const value=source();
      try {{ await navigator.clipboard.writeText(value); status.textContent='Copied'; return; }} catch {{}}
      const area=document.createElement('textarea'); area.value=value; area.setAttribute('readonly',''); area.style.position='fixed'; area.style.opacity='0'; document.body.appendChild(area); area.select();
      status.textContent=document.execCommand('copy')?'Copied':'Copy failed'; area.remove();
    }};
    const exportDiagram = async () => {{
      const format = formatSelect.value.toLowerCase();
      if(format==='svg') {{ saveBlob(new Blob([source()],{{type:'image/svg+xml;charset=utf-8'}}),'svg'); return; }}
      const scale = Math.max(1,Math.min(4,Number(scaleSelect.value)||2));
      const box = svg.viewBox.baseVal; const width=box.width||svg.clientWidth; const height=box.height||svg.clientHeight;
      const image = new Image(); const url=URL.createObjectURL(new Blob([source()],{{type:'image/svg+xml'}}));
      await new Promise((resolve,reject)=>{{ image.onload=resolve; image.onerror=reject; image.src=url; }});
      const raster=document.createElement('canvas'); raster.width=Math.round(width*scale); raster.height=Math.round(height*scale);
      const ctx=raster.getContext('2d'); if(format==='jpeg'){{ctx.fillStyle='#ffffff';ctx.fillRect(0,0,raster.width,raster.height);}} ctx.drawImage(image,0,0,raster.width,raster.height); URL.revokeObjectURL(url);
      const mime=format==='jpeg'?'image/jpeg':format==='webp'?'image/webp':'image/png';
      const blob=await new Promise(resolve=>raster.toBlob(resolve,mime,.94)); if(blob) saveBlob(blob,format==='jpeg'?'jpg':format);
    }};
    document.querySelector('.toolbar').addEventListener('click', async event => {{
      const action=event.target.closest('[data-action]')?.dataset.action; if(!action)return;
      if(action==='zoom-in')zoom(1.2); else if(action==='zoom-out')zoom(.83); else if(action==='reset')reset();
      else if(action==='theme')document.documentElement.dataset.theme=document.documentElement.dataset.theme==='dark'?'light':'dark';
      else if(action==='download')await exportDiagram();
      else if(action==='copy') await copySource();
    }});
    document.addEventListener('keydown', event => {{
      if(event.target.matches('select'))return;
      if(event.key==='+'||event.key==='=')zoom(1.2); else if(event.key==='-')zoom(.83); else if(event.key==='0'||event.key.toLowerCase()==='r')reset();
      else if(event.key.toLowerCase()==='t')document.querySelector('[data-action=theme]').click(); else if(event.key.toLowerCase()==='s'){{event.preventDefault();exportDiagram();}}
    }});
    render();
  }})();
  </script>
</body>
</html>
"""


def parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("svg_file", type=Path)
    parser.add_argument("output_html", type=Path)
    parser.add_argument("--title")
    parser.add_argument("--slug", default="fireworks-tech-graph")
    return parser.parse_args(argv)


def main(argv: Optional[Sequence[str]] = None) -> int:
    args = parse_args(argv)
    svg_text = args.svg_file.read_text(encoding="utf-8")
    output = build_interactive_html(svg_text, args.title or args.svg_file.stem, {"slug": args.slug})
    args.output_html.parent.mkdir(parents=True, exist_ok=True)
    args.output_html.write_text(output, encoding="utf-8")
    print(f"✓ Interactive HTML: {args.output_html}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
skills/fireworks-tech-graph/scripts/semantic_contracts.py
#!/usr/bin/env python3
"""Style catalog and semantic contracts for engineering-specific diagrams.

The visual style and engineering meaning are separate inputs.  Styles 9-12
select a useful default semantic profile, while ``semantic_profile: generic``
keeps the visual theme available to other diagram types and regression tests.
"""

from __future__ import annotations

import json
import math
from functools import lru_cache
from pathlib import Path
from typing import Any, Mapping, MutableMapping, Optional, Sequence


class SemanticContractError(ValueError):
    """Raised when a diagram violates a selected engineering contract."""


STYLE_NAMES: dict[int, str] = {
    1: "Flat Icon",
    2: "Dark Terminal",
    3: "Blueprint",
    4: "Notion Clean",
    5: "Glassmorphism",
    6: "Claude Official",
    7: "OpenAI",
    8: "Dark Luxury",
    9: "C4 Review Canvas",
    10: "Cloud Fabric",
    11: "Event Transit",
    12: "Ops Pulse",
}

STYLE_DEFAULT_PROFILES = {
    9: "c4-review",
    10: "cloud-fabric",
    11: "event-transit",
    12: "ops-pulse",
}

PROFILE_ALIASES = {
    "generic": "generic",
    "none": "generic",
    "c4": "c4-review",
    "c4 review": "c4-review",
    "c4 review canvas": "c4-review",
    "architecture review board": "c4-review",
    "c4 评审画布": "c4-review",
    "架构评审画布": "c4-review",
    "c4-review": "c4-review",
    "cloud": "cloud-fabric",
    "cloud deployment": "cloud-fabric",
    "deployment topology": "cloud-fabric",
    "multi region deployment map": "cloud-fabric",
    "云部署拓扑": "cloud-fabric",
    "多区域部署图": "cloud-fabric",
    "cloud fabric": "cloud-fabric",
    "cloud-fabric": "cloud-fabric",
    "event stream": "event-transit",
    "event-stream": "event-transit",
    "event metro map": "event-transit",
    "topic rail map": "event-transit",
    "事件地铁图": "event-transit",
    "事件轨道图": "event-transit",
    "kafka": "event-transit",
    "event transit": "event-transit",
    "event-transit": "event-transit",
    "observability": "ops-pulse",
    "reliability pulse": "ops-pulse",
    "golden signals trace": "ops-pulse",
    "可靠性脉冲": "ops-pulse",
    "sre trace 评审": "ops-pulse",
    "otel": "ops-pulse",
    "ops pulse": "ops-pulse",
    "ops-pulse": "ops-pulse",
}


def _token(value: object) -> str:
    return " ".join(str(value).strip().lower().replace("_", " ").replace("-", " ").split())


STYLE_ALIASES: dict[str, int] = {}
for _style_id, _style_name in STYLE_NAMES.items():
    STYLE_ALIASES[_token(_style_name)] = _style_id
    STYLE_ALIASES[f"style {_style_id}"] = _style_id
    STYLE_ALIASES[f"风格 {_style_id}"] = _style_id
    STYLE_ALIASES[f"风格{_style_id}"] = _style_id
STYLE_ALIASES.update(
    {
        "flat": 1,
        "terminal": 2,
        "dark terminal": 2,
        "notion": 4,
        "glass": 5,
        "claude": 6,
        "openai official": 7,
        "review canvas": 9,
        "c4 canvas": 9,
        "c4 review": 9,
        "adr review canvas": 9,
        "architecture review board": 9,
        "c4 评审": 9,
        "c4 评审画布": 9,
        "adr 评审图": 9,
        "架构评审画布": 9,
        "职责边界评审图": 9,
        "cloud deployment": 10,
        "deployment topology": 10,
        "multi region deployment map": 10,
        "region vpc ownership map": 10,
        "cloud landing zone map": 10,
        "云部署拓扑": 10,
        "多区域部署图": 10,
        "region vpc 归属图": 10,
        "云 landing zone 图": 10,
        "event stream": 11,
        "event metro": 11,
        "event metro map": 11,
        "topic rail map": 11,
        "kafka topology": 11,
        "stream choreography map": 11,
        "事件轨道图": 11,
        "事件地铁图": 11,
        "topic 线路图": 11,
        "kafka 拓扑图": 11,
        "sre": 12,
        "observability": 12,
        "reliability pulse": 12,
        "incident investigation view": 12,
        "sre trace review": 12,
        "golden signals trace": 12,
        "运维脉冲图": 12,
        "可靠性脉冲": 12,
        "事故排查视图": 12,
        "sre trace 评审": 12,
        "黄金信号追踪图": 12,
    }
)


def resolve_style_index(data: Mapping[str, Any]) -> int:
    """Resolve numeric/name selectors and reject ambiguous or unknown themes."""

    selectors: list[tuple[str, object]] = []
    if data.get("style") is not None:
        selectors.append(("style", data["style"]))
    if data.get("visual_theme") is not None:
        selectors.append(("visual_theme", data["visual_theme"]))
    if not selectors:
        return 1

    resolved: list[tuple[str, int]] = []
    for field, raw in selectors:
        if isinstance(raw, bool):
            raise SemanticContractError(f"STYLE_SELECTOR: {field} must be a style id or name")
        if isinstance(raw, int):
            style_id = raw
        else:
            text = str(raw).strip()
            if text.isdigit():
                style_id = int(text)
            else:
                normalized = _token(text)
                if normalized not in STYLE_ALIASES:
                    raise SemanticContractError(f"STYLE_SELECTOR: unsupported {field}: {raw}")
                style_id = STYLE_ALIASES[normalized]
        if style_id not in STYLE_NAMES:
            raise SemanticContractError(f"STYLE_SELECTOR: unsupported {field}: {raw}")
        resolved.append((field, style_id))

    if len({style_id for _, style_id in resolved}) > 1:
        details = ", ".join(f"{field}={style_id}" for field, style_id in resolved)
        raise SemanticContractError(f"STYLE_SELECTOR_CONFLICT: {details}")
    return resolved[0][1]


def _fail(code: str, message: str) -> None:
    raise SemanticContractError(f"{code}: {message}")


def _require_text(item: Mapping[str, Any], field: str, path: str) -> str:
    value = str(item.get(field, "")).strip()
    if not value:
        _fail("SEMANTIC_REQUIRED", f"{path}.{field} is required")
    return value


def _number(value: Any, path: str) -> float:
    try:
        number = float(value)
    except (TypeError, ValueError) as error:
        raise SemanticContractError(f"SEMANTIC_NUMBER: {path} must be finite") from error
    if not math.isfinite(number):
        _fail("SEMANTIC_NUMBER", f"{path} must be finite")
    return number


def _bounds(item: Mapping[str, Any], path: str) -> tuple[float, float, float, float]:
    x = _number(item.get("x"), f"{path}.x")
    y = _number(item.get("y"), f"{path}.y")
    width = _number(item.get("width"), f"{path}.width")
    height = _number(item.get("height"), f"{path}.height")
    if width <= 0 or height <= 0:
        _fail("SEMANTIC_BOUNDS", f"{path} must have positive width and height")
    return (x, y, x + width, y + height)


def _inside(inner: Sequence[float], outer: Sequence[float], inset: float) -> bool:
    return (
        inner[0] >= outer[0] + inset
        and inner[1] >= outer[1] + inset
        and inner[2] <= outer[2] - inset
        and inner[3] <= outer[3] - inset
    )


def _rectangle_gap(first: Sequence[float], second: Sequence[float]) -> float:
    horizontal = max(first[0] - second[2], second[0] - first[2], 0.0)
    vertical = max(first[1] - second[3], second[1] - first[3], 0.0)
    return math.hypot(horizontal, vertical)


def _node_map(data: Mapping[str, Any]) -> dict[str, MutableMapping[str, Any]]:
    return {
        str(node.get("id")): node
        for node in data.get("nodes", [])
        if isinstance(node, MutableMapping)
    }


def _edge_map(data: Mapping[str, Any]) -> dict[str, MutableMapping[str, Any]]:
    return {
        str(edge.get("id")): edge
        for edge in data.get("arrows", [])
        if isinstance(edge, MutableMapping)
    }


def _require_graph_endpoints(
    data: Mapping[str, Any], nodes: Mapping[str, Mapping[str, Any]]
) -> dict[str, MutableMapping[str, Any]]:
    """Keep engineering profiles tied to semantic nodes, never loose coordinates."""

    edges = _edge_map(data)
    for edge_id, edge in edges.items():
        source_id = str(edge.get("source", "")).strip()
        target_id = str(edge.get("target", "")).strip()
        if not source_id or not target_id:
            _fail(
                "SEMANTIC_EDGE_ENDPOINT",
                f"arrows[{edge_id}] must declare both source and target node ids",
            )
        if source_id not in nodes or target_id not in nodes:
            _fail(
                "SEMANTIC_EDGE_ENDPOINT",
                f"arrows[{edge_id}] references unknown endpoint {source_id!r} -> {target_id!r}",
            )
    return edges


def _validate_c4(data: MutableMapping[str, Any]) -> dict[str, Any]:
    if str(data.get("diagram_type", "")).strip() != "c4":
        _fail("C4_DIAGRAM_TYPE", "diagram_type must be 'c4'")
    level = _require_text(data, "c4_level", "diagram")
    if level not in {"context", "container", "component"}:
        _fail("C4_LEVEL", f"unsupported c4_level: {level}")
    _require_text(data, "title", "diagram")
    _require_text(data, "scope", "diagram")
    seed = data.get("rough_seed")
    if isinstance(seed, bool) or not isinstance(seed, int):
        _fail("C4_ROUGH_SEED", "rough_seed must be an integer")
    if not isinstance(data.get("legend"), list) or not data.get("legend"):
        _fail("C4_LEGEND", "a non-empty legend is required")

    allowed = {
        "context": {"person", "software_system", "external_system"},
        "container": {"person", "software_system", "external_system", "container"},
        "component": {"person", "software_system", "external_system", "container", "component"},
    }[level]
    boundaries = {
        str(item.get("id")): item
        for item in data.get("containers", [])
        if isinstance(item, Mapping)
    }
    nodes = _node_map(data)
    for node_id, node in nodes.items():
        c4_type = _require_text(node, "c4_type", f"nodes[{node_id}]")
        if c4_type not in allowed:
            _fail("C4_MIXED_ABSTRACTION", f"{node_id} type {c4_type!r} is invalid for {level} view")
        _require_text(node, "label", f"nodes[{node_id}]")
        _require_text(node, "description", f"nodes[{node_id}]")
        if c4_type in {"container", "component"}:
            _require_text(node, "technology", f"nodes[{node_id}]")
            bounds = _bounds(node, f"nodes[{node_id}]")
            if bounds[2] - bounds[0] < 170 or bounds[3] - bounds[1] < 96:
                _fail("C4_CARD_SIZE", f"{node_id} must be at least 170x96")
        parent = str(node.get("parent", "")).strip()
        if parent and parent not in boundaries:
            _fail("C4_PARENT", f"{node_id} references unknown boundary {parent}")
        if parent and not _inside(
            _bounds(node, f"nodes[{node_id}]"),
            _bounds(boundaries[parent], f"containers[{parent}]"),
            20,
        ):
            _fail("C4_BOUNDARY_ESCAPE", f"{node_id} must stay 20px inside {parent}")

    edges = _require_graph_endpoints(data, nodes)
    for edge_id, edge in edges.items():
        _require_text(edge, "label", f"arrows[{edge_id}]")
        _require_text(edge, "protocol", f"arrows[{edge_id}]")
    return {"level": level, "rough_seed": seed, "elements": len(nodes)}


@lru_cache(maxsize=1)
def _cloud_icons() -> tuple[str, dict[str, Mapping[str, Any]]]:
    path = Path(__file__).resolve().parents[1] / "assets" / "icons" / "cloud" / "manifest-v1.json"
    payload = json.loads(path.read_text(encoding="utf-8"))
    version = str(payload.get("version", ""))
    icons: dict[str, Mapping[str, Any]] = {}
    aliases: set[str] = set()
    for item in payload.get("icons", []):
        icon_id = str(item.get("id", "")).strip()
        if not icon_id or icon_id in icons:
            _fail("CLOUD_ICON_MANIFEST", f"duplicate or empty icon id: {icon_id}")
        icons[icon_id] = item
        for alias in item.get("aliases", []):
            normalized = _token(alias)
            if normalized in aliases:
                _fail("CLOUD_ICON_MANIFEST", f"duplicate icon alias: {alias}")
            aliases.add(normalized)
    return version, icons


def _validate_cloud(data: MutableMapping[str, Any]) -> dict[str, Any]:
    if str(data.get("diagram_type", "")).strip() != "deployment":
        _fail("CLOUD_DIAGRAM_TYPE", "diagram_type must be 'deployment'")
    platform = _require_text(data, "platform_profile", "diagram")
    if platform not in {"provider-neutral", "aws", "azure", "gcp", "kubernetes"}:
        _fail("CLOUD_PLATFORM", f"unsupported platform_profile: {platform}")
    manifest_version, icon_catalog = _cloud_icons()
    if str(data.get("icon_manifest_version", "")) != manifest_version:
        _fail("CLOUD_ICON_VERSION", f"icon_manifest_version must be {manifest_version}")

    containers = {
        str(item.get("id")): item
        for item in data.get("containers", [])
        if isinstance(item, Mapping)
    }
    if not containers or not any(item.get("deployment_kind") == "region" for item in containers.values()):
        _fail("CLOUD_BOUNDARY", "at least one region deployment boundary is required")

    def depth(container_id: str, trail: tuple[str, ...] = ()) -> int:
        if container_id in trail:
            _fail("CLOUD_BOUNDARY_CYCLE", " -> ".join((*trail, container_id)))
        parent = str(containers[container_id].get("parent", "")).strip()
        if not parent:
            return 1
        if parent not in containers:
            _fail("CLOUD_BOUNDARY_PARENT", f"{container_id} references unknown parent {parent}")
        return 1 + depth(parent, (*trail, container_id))

    depths = {container_id: depth(container_id) for container_id in containers}
    if max(depths.values()) > 4:
        _fail("CLOUD_BOUNDARY_DEPTH", "deployment nesting depth cannot exceed 4")
    for container_id, container in containers.items():
        _require_text(container, "deployment_kind", f"containers[{container_id}]")
        parent = str(container.get("parent", "")).strip()
        if parent and not _inside(_bounds(container, f"containers[{container_id}]"), _bounds(containers[parent], f"containers[{parent}]"), 16):
            _fail("CLOUD_BOUNDARY_ESCAPE", f"{container_id} must be inset inside {parent}")
    ordered_containers = list(containers.items())
    for index, (first_id, first) in enumerate(ordered_containers):
        first_parent = str(first.get("parent", "")).strip()
        for second_id, second in ordered_containers[index + 1 :]:
            if first_parent != str(second.get("parent", "")).strip():
                continue
            gap = _rectangle_gap(
                _bounds(first, f"containers[{first_id}]"),
                _bounds(second, f"containers[{second_id}]"),
            )
            if gap < 16:
                _fail("CLOUD_BOUNDARY_GAP", f"siblings {first_id} and {second_id} need 16px clearance")

    nodes = _node_map(data)
    for node_id, node in nodes.items():
        deployment_id = _require_text(node, "deployment_id", f"nodes[{node_id}]")
        if deployment_id not in containers:
            _fail("CLOUD_DEPLOYMENT", f"{node_id} references unknown deployment {deployment_id}")
        if not _inside(_bounds(node, f"nodes[{node_id}]"), _bounds(containers[deployment_id], f"containers[{deployment_id}]"), 20):
            _fail("CLOUD_NODE_ESCAPE", f"{node_id} must stay 20px inside {deployment_id}")
        icon_id = _require_text(node, "icon_id", f"nodes[{node_id}]")
        if icon_id not in icon_catalog:
            _fail("CLOUD_ICON_UNKNOWN", f"unknown icon_id: {icon_id}")
        icon = icon_catalog[icon_id]
        node.setdefault("icon_badge", icon.get("badge", "CLOUD"))
        node.setdefault("icon_color", icon.get("color", "#2563eb"))
        node.setdefault("glyph", icon.get("glyph", "service"))
        node.setdefault("icon_source", "builtin-neutral")
        node.setdefault("icon_version", manifest_version)
        lineage: list[str] = []
        current = deployment_id
        while current:
            boundary = containers[current]
            lineage.append(str(boundary.get("label", current)))
            current = str(boundary.get("parent", "")).strip()
        node.setdefault("deployment_path", " › ".join(reversed(lineage)))

    edges = _require_graph_endpoints(data, nodes)
    for edge_id, edge in edges.items():
        source = nodes[str(edge["source"])]
        target = nodes[str(edge["target"])]
        if source.get("deployment_id") != target.get("deployment_id"):
            _require_text(edge, "via", f"arrows[{edge_id}]")
    return {
        "platform": platform,
        "manifest_version": manifest_version,
        "boundaries": len(containers),
        "max_depth": max(depths.values()),
    }


def _validate_event(data: MutableMapping[str, Any]) -> dict[str, Any]:
    if str(data.get("diagram_type", "")).strip() != "event_stream":
        _fail("EVENT_DIAGRAM_TYPE", "diagram_type must be 'event_stream'")
    topics = data.get("topics", [])
    if not isinstance(topics, list) or not topics:
        _fail("EVENT_TOPICS", "topics must be a non-empty array")
    if len(topics) > 4:
        _fail("EVENT_TOPIC_LIMIT", "showcase diagrams support at most four topic rails")
    topic_colors: dict[str, str] = {}
    for index, topic in enumerate(topics):
        if not isinstance(topic, Mapping):
            _fail("EVENT_TOPIC", f"topics[{index}] must be an object")
        topic_id = _require_text(topic, "id", f"topics[{index}]")
        color = _require_text(topic, "color", f"topics[{index}]")
        if topic_id in topic_colors:
            _fail("EVENT_TOPIC_DUPLICATE", f"duplicate topic id: {topic_id}")
        topic_colors[topic_id] = color

    nodes = _node_map(data)
    allowed_roles = {"producer", "station", "junction", "consumer", "dlq", "state_store"}
    orders: dict[tuple[str, int], str] = {}
    for node_id, node in nodes.items():
        role = _require_text(node, "transit_role", f"nodes[{node_id}]")
        if role not in allowed_roles:
            _fail("EVENT_ROLE", f"unsupported transit_role {role!r} on {node_id}")
        topic_id = str(node.get("topic_id", "")).strip()
        if topic_id and topic_id not in topic_colors:
            _fail("EVENT_TOPIC_UNKNOWN", f"{node_id} references unknown topic {topic_id}")
        if topic_id:
            node.setdefault("rail_color", topic_colors[topic_id])
        if role in {"station", "junction"}:
            _require_text(node, "operation", f"nodes[{node_id}]")
        if role == "consumer":
            _require_text(node, "consumer_group", f"nodes[{node_id}]")
        if role not in {"dlq", "state_store"}:
            order = node.get("station_order")
            if isinstance(order, bool) or not isinstance(order, int):
                _fail("EVENT_STATION_ORDER", f"{node_id}.station_order must be an integer")
            key = (topic_id, order)
            if key in orders:
                _fail("EVENT_STATION_ORDER", f"duplicate order {order} for topic {topic_id}")
            orders[key] = node_id

    edges = _require_graph_endpoints(data, nodes)
    rail_outgoing: dict[str, int] = {}
    for edge_id, edge in edges.items():
        transit_type = _require_text(edge, "transit_type", f"arrows[{edge_id}]")
        source = nodes[str(edge["source"])]
        target = nodes[str(edge["target"])]
        if transit_type == "rail":
            topic_id = _require_text(edge, "topic_id", f"arrows[{edge_id}]")
            if source.get("topic_id") != topic_id or target.get("topic_id") != topic_id:
                _fail("EVENT_TOPIC_DRIFT", f"{edge_id} must connect nodes on topic {topic_id}")
            if int(target.get("station_order", -1)) != int(source.get("station_order", -2)) + 1:
                _fail("EVENT_RAIL_ORDER", f"{edge_id} must connect adjacent increasing stations")
            source_bounds = _bounds(source, f"nodes[{source.get('id')}]")
            target_bounds = _bounds(target, f"nodes[{target.get('id')}]")
            if abs((source_bounds[1] + source_bounds[3]) - (target_bounds[1] + target_bounds[3])) > 1e-6:
                _fail("EVENT_RAIL_ALIGNMENT", f"{edge_id} rail endpoints must share one horizontal centerline")
            if target_bounds[0] - source_bounds[2] < 64:
                _fail("EVENT_RAIL_LENGTH", f"{edge_id} rail must have at least 64px clearance")
            if edge.get("source_port") != "right" or edge.get("target_port") != "left":
                _fail("EVENT_RAIL_PORT", f"{edge_id} must use right-to-left ports")
            rail_outgoing[str(edge.get("source"))] = rail_outgoing.get(str(edge.get("source")), 0) + 1
        elif transit_type == "dead_letter":
            if target.get("transit_role") != "dlq":
                _fail("EVENT_DLQ_TARGET", f"{edge_id} must target a dlq node")
            edge.setdefault("dashed", True)
        elif transit_type == "branch":
            if source.get("transit_role") != "junction":
                _fail("EVENT_BRANCH_JUNCTION", f"{edge_id} must depart from a junction node")
        elif transit_type not in {"publish", "branch", "consume", "retry", "state"}:
            _fail("EVENT_EDGE_TYPE", f"unsupported transit_type: {transit_type}")
    for node_id, count in rail_outgoing.items():
        if count > 1 and nodes[node_id].get("transit_role") != "junction":
            _fail("EVENT_BRANCH_JUNCTION", f"{node_id} branches without a junction role")
    return {"topics": len(topics), "stations": len(nodes)}


def _validate_ops(data: MutableMapping[str, Any]) -> dict[str, Any]:
    if str(data.get("diagram_type", "")).strip() != "observability":
        _fail("OPS_DIAGRAM_TYPE", "diagram_type must be 'observability'")
    observation_window = _require_text(data, "observation_window", "diagram")
    nodes = _node_map(data)
    edges = _require_graph_endpoints(data, nodes)
    services = [node for node in nodes.values() if node.get("ops_role") == "service"]
    if not services or len(services) > 12:
        _fail("OPS_SERVICE_LIMIT", "an Ops Pulse view requires 1-12 service nodes")
    expected_signals = {"latency", "traffic", "errors", "saturation"}
    allowed_status = {"ok", "warn", "critical", "unknown"}
    for node in services:
        node_id = str(node.get("id"))
        status = _require_text(node, "status", f"nodes[{node_id}]")
        if status not in allowed_status:
            _fail("OPS_SERVICE_STATUS", f"unsupported status {status!r} on {node_id}")
        metrics = node.get("signals")
        if not isinstance(metrics, Mapping) or set(metrics) != expected_signals:
            _fail("OPS_GOLDEN_SIGNALS", f"{node_id} must define exactly latency, traffic, errors, saturation")
        _require_text(node, "status_label", f"nodes[{node_id}]")
        bounds = _bounds(node, f"nodes[{node_id}]")
        if bounds[2] - bounds[0] < 180 or bounds[3] - bounds[1] < 108:
            _fail("OPS_CARD_SIZE", f"{node_id} must be at least 180x108")
        normalized_signals: list[dict[str, str]] = []
        for signal_name in ("latency", "traffic", "errors", "saturation"):
            signal = metrics[signal_name]
            if not isinstance(signal, Mapping):
                _fail("OPS_SIGNAL", f"{node_id}.{signal_name} must be an object")
            value = _require_text(signal, "value", f"nodes[{node_id}].signals.{signal_name}")
            unit = _require_text(signal, "unit", f"nodes[{node_id}].signals.{signal_name}")
            window = _require_text(signal, "window", f"nodes[{node_id}].signals.{signal_name}")
            if window != observation_window:
                _fail(
                    "OPS_OBSERVATION_WINDOW",
                    f"{node_id}.{signal_name} window {window!r} must match diagram observation_window {observation_window!r}",
                )
            status = _require_text(signal, "status", f"nodes[{node_id}].signals.{signal_name}")
            if status not in allowed_status:
                _fail("OPS_SIGNAL_STATUS", f"unsupported status {status!r} on {node_id}.{signal_name}")
            normalized_signals.append(
                {"name": signal_name, "value": value, "unit": unit, "window": window, "status": status}
            )
        node["metric_badges"] = normalized_signals

    critical_path = data.get("critical_path", [])
    if not isinstance(critical_path, list) or not critical_path:
        _fail("OPS_CRITICAL_PATH", "critical_path must be a non-empty ordered edge-id list")
    if len(set(map(str, critical_path))) != len(critical_path):
        _fail("OPS_CRITICAL_PATH", "critical_path cannot repeat an edge")
    previous_target = None
    visited_services: set[str] = set()
    service_ids = {str(node.get("id")) for node in services}
    for critical_index, raw_edge_id in enumerate(critical_path):
        edge_id = str(raw_edge_id)
        if edge_id not in edges:
            _fail("OPS_CRITICAL_PATH", f"unknown critical edge: {edge_id}")
        edge = edges[edge_id]
        if edge.get("edge_kind", "business") != "business":
            _fail("OPS_CRITICAL_PATH", f"critical edge {edge_id} must be a business edge")
        source_id = str(edge.get("source"))
        target_id = str(edge.get("target"))
        if source_id not in service_ids or target_id not in service_ids:
            _fail("OPS_CRITICAL_PATH", f"critical edge {edge_id} must connect service nodes")
        if previous_target is not None and str(edge.get("source")) != previous_target:
            _fail("OPS_CRITICAL_PATH", f"critical path is discontinuous before {edge_id}")
        if not visited_services:
            visited_services.add(source_id)
        if target_id in visited_services:
            _fail("OPS_CRITICAL_PATH", f"critical path repeats service {target_id}")
        visited_services.add(target_id)
        previous_target = target_id
        edge["critical"] = True
        edge["critical_path_id"] = str(data.get("critical_path_id", "critical-1"))
        edge["critical_hop"] = critical_index + 1
        edge["critical_hops"] = len(critical_path)

    business_flows = {
        str(edge.get("flow", "control"))
        for edge in edges.values()
        if edge.get("edge_kind", "business") == "business"
    }
    telemetry_flows = {
        str(edge.get("flow", "async"))
        for edge in edges.values()
        if edge.get("edge_kind") == "telemetry"
    }
    if business_flows & telemetry_flows:
        _fail("OPS_FLOW_SEMANTICS", "business and telemetry edges must use different flow tokens")

    spans = [node for node in nodes.values() if node.get("ops_role") == "trace_span"]
    if not spans:
        _fail("OPS_TRACE_REQUIRED", "an Ops Pulse view requires one correlated trace waterfall")
    span_map = {str(span.get("span_id")): span for span in spans}
    if len(span_map) != len(spans):
        _fail("OPS_SPAN_ID", "trace span ids must be unique")
    roots = 0
    root_span: Optional[Mapping[str, Any]] = None
    for span in spans:
        span_id = _require_text(span, "span_id", f"nodes[{span.get('id')}]")
        start = _number(span.get("start_ms"), f"spans[{span_id}].start_ms")
        duration = _number(span.get("duration_ms"), f"spans[{span_id}].duration_ms")
        if duration <= 0:
            _fail("OPS_SPAN_DURATION", f"{span_id} duration must be positive")
        parent_id = str(span.get("parent_span", "")).strip()
        if not parent_id:
            roots += 1
            root_span = span
            continue
        parent = span_map.get(parent_id)
        if parent is None:
            _fail("OPS_SPAN_PARENT", f"{span_id} references unknown parent span {parent_id}")
        parent_start = _number(parent.get("start_ms"), f"spans[{parent_id}].start_ms")
        parent_duration = _number(parent.get("duration_ms"), f"spans[{parent_id}].duration_ms")
        if start < parent_start or start + duration > parent_start + parent_duration:
            _fail("OPS_SPAN_COVERAGE", f"{span_id} must be contained by {parent_id}")
    if spans and roots != 1:
        _fail("OPS_SPAN_ROOT", "trace waterfall must contain exactly one root span")
    for span_id, span in span_map.items():
        seen: set[str] = set()
        current_id = span_id
        current = span
        while current.get("parent_span"):
            if current_id in seen:
                _fail("OPS_SPAN_CYCLE", f"trace parent cycle contains {current_id}")
            seen.add(current_id)
            current_id = str(current.get("parent_span"))
            if current_id not in span_map:
                break
            current = span_map[current_id]
    if root_span is not None:
        root_bounds = _bounds(root_span, f"spans[{root_span.get('span_id')}]")
        root_start = _number(root_span.get("start_ms"), "root_span.start_ms")
        root_duration = _number(root_span.get("duration_ms"), "root_span.duration_ms")
        pixels_per_ms = (root_bounds[2] - root_bounds[0]) / root_duration
        origin_x = root_bounds[0] - root_start * pixels_per_ms
        for span in spans:
            span_id = str(span.get("span_id"))
            span_bounds = _bounds(span, f"spans[{span_id}]")
            start = _number(span.get("start_ms"), f"spans[{span_id}].start_ms")
            duration = _number(span.get("duration_ms"), f"spans[{span_id}].duration_ms")
            expected_x = origin_x + start * pixels_per_ms
            expected_width = duration * pixels_per_ms
            if abs(span_bounds[0] - expected_x) > 1.5 or abs((span_bounds[2] - span_bounds[0]) - expected_width) > 1.5:
                _fail("OPS_SPAN_SCALE", f"{span_id} x/width must encode start_ms/duration_ms on the root time scale")
    return {
        "services": len(services),
        "spans": len(spans),
        "critical_edges": len(critical_path),
        "observation_window": observation_window,
    }


def validate_semantic_contract(data: MutableMapping[str, Any]) -> dict[str, Any]:
    """Validate and enrich a normalized diagram payload."""

    style_index = resolve_style_index(data)
    raw_profile = data.get("semantic_profile")
    if raw_profile is None:
        profile = STYLE_DEFAULT_PROFILES.get(style_index, "generic")
    else:
        normalized = _token(raw_profile)
        if normalized not in PROFILE_ALIASES:
            _fail("SEMANTIC_PROFILE", f"unsupported semantic_profile: {raw_profile}")
        profile = PROFILE_ALIASES[normalized]
    data["semantic_profile"] = profile

    validators = {
        "c4-review": _validate_c4,
        "cloud-fabric": _validate_cloud,
        "event-transit": _validate_event,
        "ops-pulse": _validate_ops,
    }
    details = validators[profile](data) if profile in validators else {}
    return {
        "ok": True,
        "style": style_index,
        "visual_theme": STYLE_NAMES[style_index],
        "profile": profile,
        "details": details,
    }


__all__ = [
    "PROFILE_ALIASES",
    "STYLE_ALIASES",
    "STYLE_DEFAULT_PROFILES",
    "STYLE_NAMES",
    "SemanticContractError",
    "resolve_style_index",
    "validate_semantic_contract",
]
skills/fireworks-tech-graph/tests/test_validate_svg.py
from __future__ import annotations

import importlib.util
import sys
import tempfile
import unittest
from pathlib import Path


SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "validate_svg.py"
SPEC = importlib.util.spec_from_file_location("validate_svg", SCRIPT)
assert SPEC and SPEC.loader
validate_svg = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = validate_svg
SPEC.loader.exec_module(validate_svg)


class ValidateSvgTest(unittest.TestCase):
    def write_svg(self, body: str, root_attrs: str = "") -> Path:
        tempdir = tempfile.TemporaryDirectory()
        self.addCleanup(tempdir.cleanup)
        path = Path(tempdir.name) / "diagram.svg"
        path.write_text(
            f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 240" {root_attrs}>'
            '<defs><marker id="arrow-main"><path d="M0 0 L8 4 L0 8 Z"/></marker></defs>'
            f"{body}</svg>",
            encoding="utf-8",
        )
        return path

    def test_text_with_equals_is_valid_xml(self) -> None:
        path = self.write_svg('<text x="10" y="20">retrieve(top_k=5)</text>')
        ok, details = validate_svg.run_check(path, "xml")
        self.assertTrue(ok, details)

    def test_marker_start_and_end_are_resolved_structurally(self) -> None:
        path = self.write_svg(
            '<path d="M 20 20 H 100" marker-start="url(#arrow-main)" marker-end="url(#arrow-main)"/>'
        )
        ok, details = validate_svg.run_check(path, "markers")
        self.assertTrue(ok, details)

    def test_missing_marker_start_is_reported(self) -> None:
        path = self.write_svg('<path d="M 20 20 H 100" marker-start="url(#missing)"/>')
        ok, details = validate_svg.run_check(path, "markers")
        self.assertFalse(ok)
        self.assertEqual(details, ["missing marker: missing"])

    def test_absolute_hv_path_collision_is_reported(self) -> None:
        path = self.write_svg(
            '<rect id="blocker" x="160" y="80" width="80" height="60"/>'
            '<path id="edge" d="M 20 110 H 280 V 180" marker-end="url(#arrow-main)"/>'
        )
        ok, details = validate_svg.run_check(path, "collisions")
        self.assertFalse(ok)
        self.assertIn("path#edge intersects rect#blocker", details)

    def test_relative_hv_path_collision_is_reported(self) -> None:
        path = self.write_svg(
            '<rect id="blocker" x="160" y="80" width="80" height="60"/>'
            '<path id="edge" d="m 20 110 h 260 v 70" marker-end="url(#arrow-main)"/>'
        )
        ok, _ = validate_svg.run_check(path, "collisions")
        self.assertFalse(ok)

    def test_quadratic_and_cubic_curves_are_sampled(self) -> None:
        quadratic = self.write_svg(
            '<rect id="blocker" x="160" y="80" width="80" height="60"/>'
            '<path id="edge" d="M 20 180 Q 200 20 380 180" marker-end="url(#arrow-main)"/>'
        )
        cubic = self.write_svg(
            '<rect id="blocker" x="160" y="80" width="80" height="60"/>'
            '<path id="edge" d="M 20 180 C 100 60 300 60 380 180" marker-end="url(#arrow-main)"/>'
        )
        self.assertFalse(validate_svg.run_check(quadratic, "collisions")[0])
        self.assertFalse(validate_svg.run_check(cubic, "collisions")[0])

    def test_elliptical_arc_is_sampled_instead_of_reduced_to_its_chord(self) -> None:
        path = self.write_svg(
            '<rect id="blocker" x="160" y="15" width="80" height="40"/>'
            '<path id="edge" d="M 20 180 A 180 160 0 0 1 380 180" marker-end="url(#arrow-main)"/>'
        )
        ok, details = validate_svg.run_check(path, "collisions")
        self.assertFalse(ok)
        self.assertIn("path#edge intersects rect#blocker", details)

    def test_boundary_to_boundary_connection_is_not_a_collision(self) -> None:
        path = self.write_svg(
            '<rect id="source" x="20" y="80" width="80" height="60"/>'
            '<rect id="target" x="280" y="80" width="80" height="60"/>'
            '<path id="edge" d="M 100 110 H 280" marker-end="url(#arrow-main)"/>'
        )
        ok, details = validate_svg.run_check(path, "collisions")
        self.assertTrue(ok, details)

    def test_small_dashed_node_is_an_obstacle_but_large_container_is_not(self) -> None:
        path = self.write_svg(
            '<rect id="container" x="10" y="20" width="380" height="200" fill="none" stroke-dasharray="6 4"/>'
            '<rect id="node" x="160" y="80" width="80" height="60" fill="none" stroke-dasharray="4 3"/>'
            '<path id="edge" d="M 20 110 H 280" marker-end="url(#arrow-main)"/>'
        )
        ok, details = validate_svg.run_check(path, "collisions")
        self.assertFalse(ok)
        self.assertEqual(details, ["path#edge intersects rect#node"])

    def test_legend_sample_arrows_do_not_collide_with_their_background(self) -> None:
        path = self.write_svg(
            '<rect id="legend" x="20" y="150" width="200" height="70"/>'
            '<path id="sample-a" d="M 40 170 H 80" marker-end="url(#arrow-main)"/>'
            '<path id="sample-b" d="M 40 195 H 80" marker-end="url(#arrow-main)"/>'
        )
        ok, details = validate_svg.run_check(path, "collisions")
        self.assertTrue(ok, details)

    def test_legacy_legend_remains_an_obstacle_for_business_edges(self) -> None:
        path = self.write_svg(
            '<rect id="legend" x="200" y="150" width="180" height="70"/>'
            '<path id="sample-a" d="M 220 170 H 260" marker-end="url(#arrow-main)"/>'
            '<path id="sample-b" d="M 220 195 H 260" marker-end="url(#arrow-main)"/>'
            '<path id="business" d="M 300 20 V 190 H 100" marker-end="url(#arrow-main)"/>'
        )
        ok, details = validate_svg.run_check(path, "collisions")
        self.assertFalse(ok)
        self.assertIn("path#business intersects rect#legend", details)

    def test_group_transform_is_applied_to_paths_and_obstacles(self) -> None:
        path = self.write_svg(
            '<g transform="translate(80 20)">'
            '<rect id="blocker" x="80" y="60" width="80" height="60"/>'
            '<path id="edge" d="M 20 90 H 200" marker-end="url(#arrow-main)"/>'
            '</g>'
        )
        ok, details = validate_svg.run_check(path, "collisions")
        self.assertFalse(ok)
        self.assertEqual(details, ["path#edge intersects rect#blocker"])

    def test_multi_subpath_checks_drawn_segments_without_connecting_moves(self) -> None:
        clear = self.write_svg(
            '<rect id="blocker" x="180" y="90" width="40" height="60"/>'
            '<path id="edge" data-graph-role="edge" d="M 20 40 H 100 M 300 180 H 380" '
            'marker-end="url(#arrow-main)"/>'
        )
        colliding = self.write_svg(
            '<rect id="blocker" x="180" y="90" width="40" height="60"/>'
            '<path id="edge" data-graph-role="edge" d="M 20 40 H 100 M 160 120 H 240" '
            'marker-end="url(#arrow-main)"/>'
        )

        self.assertTrue(validate_svg.run_check(clear, "collisions")[0])
        self.assertTrue(validate_svg.run_check(clear, "geometry")[0])
        self.assertTrue(validate_svg.run_check(clear, "composition")[0])
        ok, details = validate_svg.run_check(colliding, "collisions")
        self.assertFalse(ok)
        self.assertEqual(details, ["path#edge intersects rect#blocker"])

    def test_skew_transforms_are_applied_to_collision_geometry(self) -> None:
        skew_x = self.write_svg(
            '<rect id="blocker" x="160" y="90" width="60" height="50"/>'
            '<g transform="skewX(45)">'
            '<path id="edge" d="M 20 110 H 100" marker-end="url(#arrow-main)"/>'
            '</g>'
        )
        skew_y = self.write_svg(
            '<rect id="blocker" x="130" y="140" width="40" height="50"/>'
            '<g transform="skewY(45)">'
            '<path id="edge" d="M 110 20 H 180" marker-end="url(#arrow-main)"/>'
            '</g>'
        )

        self.assertFalse(validate_svg.run_check(skew_x, "collisions")[0])
        self.assertFalse(validate_svg.run_check(skew_y, "collisions")[0])

    def test_negative_skew_uses_all_corners_for_strict_node_and_label_bounds(self) -> None:
        skewed_node = self.write_svg(
            '<g transform="skewX(-45)">'
            '<rect id="node" data-graph-role="node" x="150" y="50" width="100" height="50"/>'
            '</g>'
            '<path id="edge" data-graph-role="edge" d="M 60 75 H 90" '
            'marker-end="url(#arrow-main)"/>'
        )
        skewed_label = self.write_svg(
            '<g transform="skewX(-45)">'
            '<text id="label" data-graph-role="label" data-owner="other" '
            'x="150" y="100" font-size="20">LABEL</text>'
            '</g>'
            '<path id="edge" data-graph-role="edge" d="M 50 94 H 65" '
            'marker-end="url(#arrow-main)"/>'
        )

        node_ok, node_details = validate_svg.run_check(skewed_node, "geometry")
        label_ok, label_details = validate_svg.run_check(skewed_label, "geometry")

        self.assertFalse(node_ok)
        self.assertIn("edge_node: path#edge intersects rect#node", node_details)
        self.assertFalse(label_ok)
        self.assertIn("label_edge: text#label intersects path#edge", label_details)

    def test_multi_subpath_bridge_budget_counts_each_declared_bridge_once(self) -> None:
        path = self.write_svg(
            '<path id="edge" data-graph-role="edge" data-bridges="200,120" '
            'd="M 20 40 H 100 M 300 180 H 380" marker-end="url(#arrow-main)"/>',
            'data-quality-profile="standard" data-max-bridged-crossings="1"',
        )

        ok, details = validate_svg.run_check(path, "composition")

        self.assertTrue(ok, details)

    def test_showcase_composition_rejects_more_than_two_bends(self) -> None:
        path = self.write_svg(
            '<path id="zigzag" data-graph-role="edge" d="M 20 20 H 80 V 60 H 140 V 120" '
            'marker-end="url(#arrow-main)"/>',
            'data-quality-profile="showcase"',
        )
        ok, details = validate_svg.run_check(path, "composition")
        self.assertFalse(ok)
        self.assertTrue(any("edge_bend_budget" in detail for detail in details), details)

    def test_showcase_composition_rejects_tight_node_spacing(self) -> None:
        path = self.write_svg(
            '<rect id="lane" data-graph-role="container" x="10" y="10" width="380" height="210"/>'
            '<rect id="first" data-graph-role="node" x="40" y="70" width="80" height="60"/>'
            '<rect id="second" data-graph-role="node" x="140" y="70" width="80" height="60"/>',
            'data-quality-profile="showcase"',
        )
        ok, details = validate_svg.run_check(path, "composition")
        self.assertFalse(ok)
        self.assertTrue(any("node_gap" in detail for detail in details), details)

    def test_composition_detects_near_miss_label_clearance(self) -> None:
        path = self.write_svg(
            '<path id="flow" data-graph-role="edge" d="M 20 100 H 380" marker-end="url(#arrow-main)"/>'
            '<text id="near" data-graph-role="label" data-owner="other" x="200" y="94" '
            'text-anchor="middle" font-size="12">near miss</text>',
            'data-quality-profile="standard" data-min-label-clearance="4"',
        )
        geometry_ok, geometry_details = validate_svg.run_check(path, "geometry")
        self.assertTrue(geometry_ok, geometry_details)
        composition_ok, composition_details = validate_svg.run_check(path, "composition")
        self.assertFalse(composition_ok)
        self.assertTrue(any("label_clearance" in detail for detail in composition_details), composition_details)

    def test_dark_luxury_fixture_passes_the_same_composition_gate(self) -> None:
        fixture = SCRIPT.parents[1] / "fixtures" / "dark-luxury-style8.svg"
        ok, details = validate_svg.run_check(fixture, "composition")
        self.assertTrue(ok, details)


if __name__ == "__main__":
    unittest.main()
tests/test_diagram_ir.py
from __future__ import annotations

import copy
import importlib.util
import math
import sys
import unittest
from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]
SPEC = importlib.util.spec_from_file_location("diagram_ir", ROOT / "scripts" / "diagram_ir.py")
assert SPEC and SPEC.loader
diagram_ir = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = diagram_ir
SPEC.loader.exec_module(diagram_ir)


class DiagramIRTest(unittest.TestCase):
    def base(self) -> dict[str, object]:
        return {
            "style": 1,
            "nodes": [
                {"id": "a", "x": 10, "y": 20, "width": 100, "height": 50},
                {"id": "b", "x": 220, "y": 20, "width": 100, "height": 50},
            ],
            "arrows": [{"source": "a", "target": "b", "route_points": [[160, 45]]}],
        }

    def test_legacy_and_v1_normalize_to_equivalent_payloads(self) -> None:
        legacy = self.base()
        versioned = copy.deepcopy(legacy)
        versioned.update({"schema_version": 1, "mode": "architecture"})
        legacy_ir = diagram_ir.normalize_diagram(legacy, "architecture")
        versioned_ir = diagram_ir.normalize_diagram(versioned, "architecture")
        self.assertEqual(legacy_ir.as_dict(), versioned_ir.as_dict())
        self.assertEqual(legacy_ir.input_schema, "legacy")
        self.assertEqual(versioned_ir.input_schema, "v1")

    def test_normalization_never_mutates_the_caller(self) -> None:
        data = self.base()
        before = copy.deepcopy(data)
        diagram_ir.normalize_diagram(data, "architecture")
        self.assertEqual(data, before)

    def test_duplicate_and_dangling_ids_are_rejected(self) -> None:
        duplicate = self.base()
        duplicate["nodes"][1]["id"] = "a"  # type: ignore[index]
        with self.assertRaisesRegex(ValueError, "duplicate node id"):
            diagram_ir.normalize_diagram(duplicate, "architecture")
        dangling = self.base()
        dangling["arrows"][0]["target"] = "missing"  # type: ignore[index]
        with self.assertRaisesRegex(ValueError, "unknown node"):
            diagram_ir.normalize_diagram(dangling, "architecture")

    def test_container_ids_are_non_empty_and_globally_unique(self) -> None:
        empty = self.base()
        empty["containers"] = [{"id": "", "x": 0, "y": 0, "width": 400, "height": 200}]
        with self.assertRaisesRegex(ValueError, r"containers\[0\]\.id.*non-empty"):
            diagram_ir.normalize_diagram(empty, "architecture")

        duplicate = self.base()
        duplicate["containers"] = [
            {"id": "scope", "x": 0, "y": 0, "width": 400, "height": 200},
            {"id": "scope", "x": 10, "y": 10, "width": 380, "height": 180},
        ]
        with self.assertRaisesRegex(ValueError, "duplicate container id: scope"):
            diagram_ir.normalize_diagram(duplicate, "architecture")

        cross_kind = self.base()
        cross_kind["containers"] = [{"id": "a", "x": 0, "y": 0, "width": 400, "height": 200}]
        with self.assertRaisesRegex(ValueError, "duplicate diagram id: a"):
            diagram_ir.normalize_diagram(cross_kind, "architecture")

    def test_schema_mode_and_non_finite_values_are_rejected(self) -> None:
        for update, message in (
            ({"schema_version": 2}, "unsupported schema_version"),
            ({"schema_version": 1, "mode": "sequence"}, "conflicts"),
            ({"width": math.inf}, "finite"),
        ):
            data = self.base()
            data.update(update)
            with self.assertRaisesRegex(ValueError, message):
                diagram_ir.normalize_diagram(data, "architecture")

    def test_malformed_waypoint_is_rejected(self) -> None:
        data = self.base()
        data["arrows"][0]["route_points"] = [[10]]  # type: ignore[index]
        with self.assertRaisesRegex(ValueError, r"must be \[x, y\]"):
            diagram_ir.normalize_diagram(data, "architecture")


if __name__ == "__main__":
    unittest.main()
skills/fireworks-tech-graph/tests/test_diagram_ir.py
from __future__ import annotations

import copy
import importlib.util
import math
import sys
import unittest
from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]
SPEC = importlib.util.spec_from_file_location("diagram_ir", ROOT / "scripts" / "diagram_ir.py")
assert SPEC and SPEC.loader
diagram_ir = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = diagram_ir
SPEC.loader.exec_module(diagram_ir)


class DiagramIRTest(unittest.TestCase):
    def base(self) -> dict[str, object]:
        return {
            "style": 1,
            "nodes": [
                {"id": "a", "x": 10, "y": 20, "width": 100, "height": 50},
                {"id": "b", "x": 220, "y": 20, "width": 100, "height": 50},
            ],
            "arrows": [{"source": "a", "target": "b", "route_points": [[160, 45]]}],
        }

    def test_legacy_and_v1_normalize_to_equivalent_payloads(self) -> None:
        legacy = self.base()
        versioned = copy.deepcopy(legacy)
        versioned.update({"schema_version": 1, "mode": "architecture"})
        legacy_ir = diagram_ir.normalize_diagram(legacy, "architecture")
        versioned_ir = diagram_ir.normalize_diagram(versioned, "architecture")
        self.assertEqual(legacy_ir.as_dict(), versioned_ir.as_dict())
        self.assertEqual(legacy_ir.input_schema, "legacy")
        self.assertEqual(versioned_ir.input_schema, "v1")

    def test_normalization_never_mutates_the_caller(self) -> None:
        data = self.base()
        before = copy.deepcopy(data)
        diagram_ir.normalize_diagram(data, "architecture")
        self.assertEqual(data, before)

    def test_duplicate_and_dangling_ids_are_rejected(self) -> None:
        duplicate = self.base()
        duplicate["nodes"][1]["id"] = "a"  # type: ignore[index]
        with self.assertRaisesRegex(ValueError, "duplicate node id"):
            diagram_ir.normalize_diagram(duplicate, "architecture")
        dangling = self.base()
        dangling["arrows"][0]["target"] = "missing"  # type: ignore[index]
        with self.assertRaisesRegex(ValueError, "unknown node"):
            diagram_ir.normalize_diagram(dangling, "architecture")

    def test_container_ids_are_non_empty_and_globally_unique(self) -> None:
        empty = self.base()
        empty["containers"] = [{"id": "", "x": 0, "y": 0, "width": 400, "height": 200}]
        with self.assertRaisesRegex(ValueError, r"containers\[0\]\.id.*non-empty"):
            diagram_ir.normalize_diagram(empty, "architecture")

        duplicate = self.base()
        duplicate["containers"] = [
            {"id": "scope", "x": 0, "y": 0, "width": 400, "height": 200},
            {"id": "scope", "x": 10, "y": 10, "width": 380, "height": 180},
        ]
        with self.assertRaisesRegex(ValueError, "duplicate container id: scope"):
            diagram_ir.normalize_diagram(duplicate, "architecture")

        cross_kind = self.base()
        cross_kind["containers"] = [{"id": "a", "x": 0, "y": 0, "width": 400, "height": 200}]
        with self.assertRaisesRegex(ValueError, "duplicate diagram id: a"):
            diagram_ir.normalize_diagram(cross_kind, "architecture")

    def test_schema_mode_and_non_finite_values_are_rejected(self) -> None:
        for update, message in (
            ({"schema_version": 2}, "unsupported schema_version"),
            ({"schema_version": 1, "mode": "sequence"}, "conflicts"),
            ({"width": math.inf}, "finite"),
        ):
            data = self.base()
            data.update(update)
            with self.assertRaisesRegex(ValueError, message):
                diagram_ir.normalize_diagram(data, "architecture")

    def test_malformed_waypoint_is_rejected(self) -> None:
        data = self.base()
        data["arrows"][0]["route_points"] = [[10]]  # type: ignore[index]
        with self.assertRaisesRegex(ValueError, r"must be \[x, y\]"):
            diagram_ir.normalize_diagram(data, "architecture")


if __name__ == "__main__":
    unittest.main()
skills/fireworks-tech-graph/tests/test_cli.py
from __future__ import annotations

import json
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]
CLI = ROOT / "scripts" / "fireworks.py"

class UnifiedCLITest(unittest.TestCase):
    def run_cli(self, *arguments: str) -> subprocess.CompletedProcess[str]:
        return subprocess.run(
            [sys.executable, str(CLI), *arguments],
            cwd=ROOT,
            text=True,
            capture_output=True,
            check=False,
        )

    def test_examples_and_validate_are_machine_readable(self) -> None:
        examples = self.run_cli("examples")
        self.assertEqual(examples.returncode, 0, examples.stderr)
        self.assertIn("fixtures/mem0-style1.json", json.loads(examples.stdout)["examples"])
        validated = self.run_cli("validate", "memory", "fixtures/mem0-style1.json")
        self.assertEqual(validated.returncode, 0, validated.stderr)
        validation = json.loads(validated.stdout)
        self.assertTrue(validation["ok"])
        self.assertEqual(validation["style"]["id"], 1)
        self.assertEqual(validation["semantics"]["profile"], "generic")

    def test_render_check_inspect_and_export_html(self) -> None:
        with tempfile.TemporaryDirectory() as directory:
            root = Path(directory)
            svg = root / "diagram.svg"
            report = root / "layout.json"
            page = root / "diagram.html"
            rendered = self.run_cli(
                "render", "architecture", "fixtures/api-flow-style7.json", str(svg), "--report", str(report)
            )
            self.assertEqual(rendered.returncode, 0, rendered.stderr)
            self.assertTrue(svg.is_file())
            self.assertTrue(json.loads(report.read_text(encoding="utf-8"))["ok"])
            checked = self.run_cli("check", str(svg))
            self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
            inspected = self.run_cli("inspect", str(svg))
            inspection = json.loads(inspected.stdout)
            self.assertEqual(inspection["generator"], "fireworks-tech-graph")
            self.assertEqual(inspection["style_id"], "7")
            self.assertEqual(inspection["semantic_profile"], "generic")
            exported = self.run_cli("export-html", str(svg), str(page))
            self.assertEqual(exported.returncode, 0, exported.stderr)
            self.assertIn('data-action="zoom-in"', page.read_text(encoding="utf-8"))

    def test_checked_in_interactive_example_matches_current_cli_output(self) -> None:
        with tempfile.TemporaryDirectory() as directory:
            root = Path(directory)
            svg = root / "api-flow.svg"
            page = root / "interactive-architecture.html"
            rendered = self.run_cli("render", "architecture", "fixtures/api-flow-style7.json", str(svg))
            self.assertEqual(rendered.returncode, 0, rendered.stderr)
            exported = self.run_cli(
                "export-html",
                str(svg),
                str(page),
                "--title",
                "API Integration Flow",
                "--slug",
                "api-integration-flow",
            )
            self.assertEqual(exported.returncode, 0, exported.stderr)
            expected = ROOT / "examples" / "interactive-architecture.html"
            self.assertEqual(page.read_bytes(), expected.read_bytes())

    def test_animate_dry_run_resolves_semantic_preset_without_writing(self) -> None:
        with tempfile.TemporaryDirectory() as directory:
            root = Path(directory)
            svg = root / "memory-weave.svg"
            gif = root / "memory-weave.gif"
            rendered = self.run_cli("render", "memory", "fixtures/mem0-style1.json", str(svg))
            self.assertEqual(rendered.returncode, 0, rendered.stderr)
            animated = self.run_cli("animate", str(svg), str(gif), "--dry-run")
            self.assertEqual(animated.returncode, 0, animated.stderr)
            plan = json.loads(animated.stdout)
            self.assertTrue(plan["dry_run"])
            self.assertEqual(plan["style_id"], 1)
            self.assertEqual(plan["preset"], "memory-weave")
            self.assertEqual(plan["duration_seconds"], 5.75)
            self.assertEqual(plan["fps"], 20)
            self.assertEqual(plan["frame_count"], 115)
            self.assertEqual(plan["width"], 960)
            self.assertEqual(plan["output_format"], "gif")
            self.assertEqual(plan["motion_grammar_version"], "3.4")
            self.assertEqual(plan["review_status"], "user-approved")
            self.assertEqual(plan["style_contract_status"], "user-approved")
            self.assertEqual(plan["timing_revision"]["id"], "+2s-settled-flow")
            self.assertEqual(plan["timing_revision"]["status"], "user-approved")
            self.assertFalse(plan["timing_revision"]["only_pending_item"])
            self.assertEqual(plan["timing_revision"]["approved_at"], "2026-07-17")
            self.assertEqual(
                plan["animation_contract"]["primitive"],
                "connector-draw-on-with-persistent-data-flow",
            )
            self.assertEqual(plan["animation_contract"]["empty_opening_frame"], 0)
            self.assertEqual(plan["animation_contract"]["draw_schedule"][0]["frames"], [1, 8])
            stream = plan["animation_contract"]["persistent_data_flow"]
            self.assertEqual(stream["stream_count"], 8)
            self.assertEqual(stream["packet_head_count"], 8)
            self.assertEqual(stream["rendered_frames"], [36, 114])
            self.assertEqual(stream["full_opacity_frames"], [38, 109])
            self.assertEqual(stream["body"]["dash_pattern"], [16, 25])
            self.assertEqual(stream["body"]["resolved_style_1_stroke_width"], 3.84)
            self.assertEqual(stream["packet_head"]["dash_pattern"], [6, 35])
            self.assertEqual(stream["packet_head"]["stroke_width"], 2.20)
            self.assertEqual(stream["packet_head"]["dash_offset_from_body"], -10)
            self.assertEqual(stream["dash_offset_per_rendered_frame"], -6.0)
            self.assertEqual(stream["expected_initial_phases"], [7, 14, 21, 28, 31, 35, 1, 8])
            self.assertEqual(plan["animation_contract"]["reset_range"], [110, 114])
            self.assertEqual(
                plan["animation_contract"]["reset_opacity_samples"],
                [1.0, 0.7575, 0.515, 0.2725, 0.03],
            )
            self.assertFalse(gif.exists())
            self.assertFalse(gif.with_suffix(".motion.json").exists())

    def test_animate_rejects_raster_inputs(self) -> None:
        with tempfile.TemporaryDirectory() as directory:
            root = Path(directory)
            for suffix, payload in ((".png", b"\x89PNG\r\n\x1a\n"), (".jpg", b"\xff\xd8\xff\xd9")):
                with self.subTest(suffix=suffix):
                    image = root / f"diagram{suffix}"
                    image.write_bytes(payload)
                    animated = self.run_cli("animate", str(image), str(root / "diagram.gif"), "--dry-run")
                    self.assertNotEqual(animated.returncode, 0)
                    self.assertIn("generated .svg", animated.stderr)

    def test_animate_rejects_non_gif_outputs_and_removed_options(self) -> None:
        with tempfile.TemporaryDirectory() as directory:
            root = Path(directory)
            svg = root / "memory.svg"
            rendered = self.run_cli("render", "memory", "fixtures/mem0-style1.json", str(svg))
            self.assertEqual(rendered.returncode, 0, rendered.stderr)
            for suffix in (".webp", ".mp4", ".webm", ".png"):
                with self.subTest(suffix=suffix):
                    animated = self.run_cli("animate", str(svg), str(root / f"memory{suffix}"), "--dry-run")
                    self.assertNotEqual(animated.returncode, 0)
                    self.assertIn("must end in .gif", animated.stderr)
            removed_option = self.run_cli(
                "animate", str(svg), str(root / "memory.gif"), "--poster", str(root / "poster.png"), "--dry-run"
            )
            self.assertNotEqual(removed_option.returncode, 0)
            self.assertIn("unrecognized arguments: --poster", removed_option.stderr)

    def test_animate_enables_styles_two_through_five(self) -> None:
        with tempfile.TemporaryDirectory() as directory:
            root = Path(directory)
            svg = root / "style2.svg"
            rendered = self.run_cli("render", "agent", "fixtures/tool-call-style2.json", str(svg))
            self.assertEqual(rendered.returncode, 0, rendered.stderr)
            animated = self.run_cli("animate", str(svg), str(root / "style2.gif"), "--dry-run")
            self.assertEqual(animated.returncode, 0, animated.stderr)
            plan = json.loads(animated.stdout)
            self.assertEqual(plan["style_id"], 2)
            self.assertEqual(plan["preset"], "tool-grounding")
            self.assertEqual(plan["height"], 720)
            self.assertEqual(plan["animation_contract"]["draw_schedule"][5]["role"], "grounding")
            self.assertEqual(plan["animation_contract"]["draw_schedule"][6]["order"], 1)
            stream = plan["animation_contract"]["persistent_data_flow"]
            self.assertEqual(stream["primitive"], "terminal-evidence-stream")
            self.assertEqual(stream["packet_head_primitive"], "terminal-command-head")
            self.assertEqual(stream["body"]["resolved_style_2_stroke_width"], 3.45)
            self.assertEqual(stream["packet_head"]["stroke_width"], 2.0)
            self.assertEqual(stream["expected_initial_phases"], [6, 12, 18, 24, 30, 36, 39, 1])
            self.assertEqual(
                plan["animation_contract"]["terminal_signature"]["primitive"],
                "terminal-prompt-cursor",
            )

            style_three = root / "style3.svg"
            rendered = self.run_cli(
                "render",
                "architecture",
                "fixtures/microservices-style3.json",
                str(style_three),
            )
            self.assertEqual(rendered.returncode, 0, rendered.stderr)
            style_three_animation = self.run_cli(
                "animate", str(style_three), str(root / "style3.gif"), "--dry-run"
            )
            self.assertEqual(style_three_animation.returncode, 0, style_three_animation.stderr)
            style_three_plan = json.loads(style_three_animation.stdout)
            self.assertEqual(style_three_plan["style_id"], 3)
            self.assertEqual(style_three_plan["preset"], "service-blueprint")
            style_three_stream = style_three_plan["animation_contract"]["persistent_data_flow"]
            self.assertEqual(style_three_stream["primitive"], "blueprint-distribution-wave")
            self.assertEqual(style_three_stream["registration_bead_count"], 10)
            self.assertEqual(style_three_stream["expected_initial_phases"], [7, 14, 21, 21, 21, 28, 28, 28, 35, 42])

            style_four = root / "style4.svg"
            rendered = self.run_cli(
                "render",
                "memory",
                "fixtures/agent-memory-types-style4.json",
                str(style_four),
            )
            self.assertEqual(rendered.returncode, 0, rendered.stderr)
            style_four_animation = self.run_cli(
                "animate", str(style_four), str(root / "style4.gif"), "--dry-run"
            )
            self.assertEqual(style_four_animation.returncode, 0, style_four_animation.stderr)
            style_four_plan = json.loads(style_four_animation.stdout)
            self.assertEqual(style_four_plan["style_id"], 4)
            self.assertEqual(style_four_plan["preset"], "memory-lifecycle")
            self.assertEqual(style_four_plan["height"], 620)
            style_four_stream = style_four_plan["animation_contract"]["persistent_data_flow"]
            self.assertEqual(style_four_stream["primitive"], "notion-memory-rail")
            self.assertEqual(style_four_stream["memory_card_count"], 6)
            self.assertEqual(style_four_stream["expected_initial_phases"], [7, 14, 21, 28, 35, 42])
            self.assertEqual(
                style_four_stream["memory_card"]["initial_normalized_progress_by_stage"],
                [0.08, 0.22, 0.36, 0.50, 0.64, 0.78],
            )

            style_five = root / "style5.svg"
            rendered = self.run_cli(
                "render",
                "agent",
                "fixtures/multi-agent-style5.json",
                str(style_five),
            )
            self.assertEqual(rendered.returncode, 0, rendered.stderr)
            style_five_animation = self.run_cli(
                "animate", str(style_five), str(root / "style5.gif"), "--dry-run"
            )
            self.assertEqual(style_five_animation.returncode, 0, style_five_animation.stderr)
            style_five_plan = json.loads(style_five_animation.stdout)
            self.assertEqual(style_five_plan["style_id"], 5)
            self.assertEqual(style_five_plan["preset"], "agent-orchestration")
            self.assertEqual(style_five_plan["review_status"], "user-approved")
            style_five_stream = style_five_plan["animation_contract"]["persistent_data_flow"]
            self.assertEqual(style_five_stream["primitive"], "glass-handoff-rail")
            self.assertEqual(style_five_stream["signature_primitive"], "glass-task-capsule")

    def test_motion_review_command_is_removed(self) -> None:
        removed = self.run_cli("motion-review", "review.json", "review.html")
        self.assertNotEqual(removed.returncode, 0)
        self.assertIn("invalid choice", removed.stderr)

    def test_legacy_generator_writes_a_failure_report_without_svg(self) -> None:
        with tempfile.TemporaryDirectory() as directory:
            root = Path(directory)
            svg = root / "unsafe.svg"
            report = root / "layout.json"
            invalid = json.dumps({"schema_version": 2, "nodes": [], "arrows": []})
            process = subprocess.run(
                [
                    sys.executable,
                    str(ROOT / "scripts" / "generate-from-template.py"),
                    "architecture",
                    str(svg),
                    invalid,
                    "--layout-report",
                    str(report),
                ],
                cwd=ROOT,
                text=True,
                capture_output=True,
                check=False,
            )
            self.assertNotEqual(process.returncode, 0)
            self.assertFalse(svg.exists())
            failure = json.loads(report.read_text(encoding="utf-8"))
            self.assertFalse(failure["ok"])
            self.assertEqual(failure["issues"][0]["code"], "LAYOUT_ERROR")


if __name__ == "__main__":
    unittest.main()
skills/fireworks-tech-graph/tests/test_python_compatibility.py
from __future__ import annotations

import ast
import unittest
from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]


class PythonCompatibilityTest(unittest.TestCase):
    def test_python_sources_parse_with_the_supported_3_9_grammar(self) -> None:
        for directory in ("scripts", "tools", "tests"):
            for path in sorted((ROOT / directory).glob("*.py")):
                with self.subTest(path=path.relative_to(ROOT)):
                    ast.parse(
                        path.read_text(encoding="utf-8"),
                        filename=str(path),
                        feature_version=9,
                    )


if __name__ == "__main__":
    unittest.main()
tests/test_python_compatibility.py
from __future__ import annotations

import ast
import unittest
from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]


class PythonCompatibilityTest(unittest.TestCase):
    def test_python_sources_parse_with_the_supported_3_9_grammar(self) -> None:
        for directory in ("scripts", "tools", "tests"):
            for path in sorted((ROOT / directory).glob("*.py")):
                with self.subTest(path=path.relative_to(ROOT)):
                    ast.parse(
                        path.read_text(encoding="utf-8"),
                        filename=str(path),
                        feature_version=9,
                    )


if __name__ == "__main__":
    unittest.main()
tools/distribution.py
#!/usr/bin/env python3
"""Build and verify every Fireworks Tech Graph distribution from one payload."""

from __future__ import annotations

import argparse
import hashlib
import json
import shutil
import stat
import subprocess
import tarfile
import zipfile
from pathlib import Path, PurePosixPath
from typing import Optional, Sequence


ROOT = Path(__file__).resolve().parents[1]
MIRROR = ROOT / "skills" / "fireworks-tech-graph"


def _npm_pack(*arguments: str) -> list[dict[str, object]]:
    process = subprocess.run(
        ["npm", "pack", *arguments, "--json"],
        cwd=ROOT,
        text=True,
        capture_output=True,
        check=False,
    )
    if process.returncode:
        raise RuntimeError(process.stderr.strip() or process.stdout.strip() or "npm pack failed")
    try:
        payload = json.loads(process.stdout)
    except json.JSONDecodeError as error:
        raise RuntimeError(f"npm pack did not return JSON: {process.stdout[-400:]}") from error
    if not isinstance(payload, list) or not payload:
        raise RuntimeError("npm pack returned no package metadata")
    return payload


def package_file_list() -> list[Path]:
    package = _npm_pack("--dry-run")[0]
    files = package.get("files", [])
    paths = sorted(Path(str(item["path"])) for item in files if isinstance(item, dict) and item.get("path"))
    if not paths or Path("SKILL.md") not in paths or Path("package.json") not in paths:
        raise RuntimeError("npm payload is missing required skill files")
    if any(path.parts and path.parts[0] == "skills" for path in paths):
        raise RuntimeError("package payload must not recursively include skills/")
    return paths


def _file_mode(path: Path) -> int:
    return stat.S_IMODE(path.stat().st_mode) & 0o777


def sync_nested_skill(check: bool = False) -> list[str]:
    paths = package_file_list()
    problems: list[str] = []
    expected = {path.as_posix() for path in paths}
    if check:
        if not MIRROR.is_dir():
            return [f"missing nested skill mirror: {MIRROR.relative_to(ROOT)}"]
        actual = {
            path.relative_to(MIRROR).as_posix()
            for path in MIRROR.rglob("*")
            if path.is_file() and "__pycache__" not in path.parts and path.suffix != ".pyc"
        }
        for relative in sorted(expected - actual):
            problems.append(f"mirror missing: {relative}")
        for relative in sorted(actual - expected):
            problems.append(f"mirror has unexpected file: {relative}")
        for relative in sorted(expected & actual):
            source = ROOT / relative
            target = MIRROR / relative
            if source.read_bytes() != target.read_bytes():
                problems.append(f"mirror content differs: {relative}")
            if bool(_file_mode(source) & stat.S_IXUSR) != bool(_file_mode(target) & stat.S_IXUSR):
                problems.append(f"mirror executable mode differs: {relative}")
        return problems

    if MIRROR.exists():
        shutil.rmtree(MIRROR)
    for relative in paths:
        source = ROOT / relative
        target = MIRROR / relative
        target.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy2(source, target)
    return []


def _tar_payload(tgz: Path) -> dict[str, tuple[bytes, int]]:
    payload: dict[str, tuple[bytes, int]] = {}
    with tarfile.open(tgz, "r:gz") as archive:
        for member in archive.getmembers():
            if not member.isfile() or not member.name.startswith("package/"):
                continue
            relative = PurePosixPath(member.name).relative_to("package").as_posix()
            extracted = archive.extractfile(member)
            if extracted is None:
                raise RuntimeError(f"cannot read tar member: {member.name}")
            payload[relative] = (extracted.read(), member.mode & 0o777)
    return payload


def build_release_zip(tgz: Path, output: Path) -> Path:
    payload = _tar_payload(tgz)
    output.parent.mkdir(parents=True, exist_ok=True)
    with zipfile.ZipFile(output, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive:
        for relative, (content, mode) in sorted(payload.items()):
            info = zipfile.ZipInfo(f"fireworks-tech-graph/{relative}")
            info.date_time = (2026, 1, 1, 0, 0, 0)
            info.compress_type = zipfile.ZIP_DEFLATED
            info.external_attr = (stat.S_IFREG | mode) << 16
            archive.writestr(info, content)
    return output


def _zip_payload(path: Path) -> dict[str, tuple[bytes, int]]:
    payload: dict[str, tuple[bytes, int]] = {}
    with zipfile.ZipFile(path) as archive:
        for info in archive.infolist():
            if info.is_dir() or not info.filename.startswith("fireworks-tech-graph/"):
                continue
            relative = PurePosixPath(info.filename).relative_to("fireworks-tech-graph").as_posix()
            payload[relative] = (archive.read(info), (info.external_attr >> 16) & 0o777)
    return payload


def verify_archive_parity(tgz: Path, release_zip: Path) -> list[str]:
    tar_payload = _tar_payload(tgz)
    zip_payload = _zip_payload(release_zip)
    problems: list[str] = []
    for relative in sorted(set(tar_payload) | set(zip_payload)):
        if relative not in tar_payload:
            problems.append(f"zip-only file: {relative}")
        elif relative not in zip_payload:
            problems.append(f"tgz-only file: {relative}")
        elif hashlib.sha256(tar_payload[relative][0]).digest() != hashlib.sha256(zip_payload[relative][0]).digest():
            problems.append(f"archive content differs: {relative}")
    return problems


def _sha256(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def build_release(output_dir: Path) -> list[Path]:
    package = json.loads((ROOT / "package.json").read_text(encoding="utf-8"))
    version = str(package["version"])
    output_dir.mkdir(parents=True, exist_ok=True)
    metadata = _npm_pack("--pack-destination", str(output_dir))[0]
    filename = str(metadata.get("filename", ""))
    tgz = output_dir / filename
    if not filename or not tgz.is_file():
        raise RuntimeError("npm pack did not create the expected tarball")
    release_zip = output_dir / f"fireworks-tech-graph-v{version}.zip"
    build_release_zip(tgz, release_zip)
    problems = verify_archive_parity(tgz, release_zip)
    if problems:
        raise RuntimeError("; ".join(problems))
    checksums = output_dir / "SHA256SUMS"
    checksums.write_text(
        "".join(f"{_sha256(path)}  {path.name}\n" for path in (tgz, release_zip)),
        encoding="utf-8",
    )
    return [tgz, release_zip, checksums]


def parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__)
    action = parser.add_mutually_exclusive_group(required=True)
    action.add_argument("--sync", action="store_true")
    action.add_argument("--check", action="store_true")
    action.add_argument("--build-release", action="store_true")
    parser.add_argument("--output-dir", type=Path, default=ROOT / "dist")
    return parser.parse_args(argv)


def main(argv: Optional[Sequence[str]] = None) -> int:
    args = parse_args(argv)
    try:
        if args.sync:
            sync_nested_skill(check=False)
            print(f"synced {MIRROR.relative_to(ROOT)}")
        elif args.check:
            problems = sync_nested_skill(check=True)
            if problems:
                for problem in problems:
                    print(problem)
                return 1
            print("nested skill mirror matches npm payload")
        else:
            for path in build_release(args.output_dir):
                print(path)
        return 0
    except (OSError, RuntimeError, subprocess.SubprocessError) as error:
        print(f"distribution error: {error}")
        return 1


if __name__ == "__main__":
    raise SystemExit(main())
    fireworks-tech-graph | Prompt Minder