diff --git a/.agents/skills/excalidraw-diagram b/.agents/skills/excalidraw-diagram new file mode 120000 index 0000000..93f9a55 --- /dev/null +++ b/.agents/skills/excalidraw-diagram @@ -0,0 +1 @@ +../../.claude/skills/excalidraw-diagram \ No newline at end of file diff --git a/.claude/skills/excalidraw-diagram/LICENSE.txt b/.claude/skills/excalidraw-diagram/LICENSE.txt new file mode 100644 index 0000000..0abd01b --- /dev/null +++ b/.claude/skills/excalidraw-diagram/LICENSE.txt @@ -0,0 +1,35 @@ +This skill is adapted from the "excalidraw-diagram-generator" skill in +https://github.com/github/awesome-copilot (skills/excalidraw-diagram-generator). + +Files derived from upstream: + references/excalidraw-schema.md (unchanged, openmlsys note added at top) + references/element-types.md (unchanged, openmlsys note added at top) + scripts/add-arrow.py (label font size 14 -> 16, note in docstring) + templates/*.excalidraw (fontFamily -> 5, bound text labels fixed, + font size >= 16) +New for openmlsys: SKILL.md (rewritten), references/openmlsys-conventions.md, +scripts/check_figures.py. + +The upstream license follows. + +MIT License + +Copyright GitHub, Inc. + +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. diff --git a/.claude/skills/excalidraw-diagram/SKILL.md b/.claude/skills/excalidraw-diagram/SKILL.md new file mode 100644 index 0000000..fdd29da --- /dev/null +++ b/.claude/skills/excalidraw-diagram/SKILL.md @@ -0,0 +1,69 @@ +--- +name: excalidraw-diagram +description: Draw figures for the openmlsys "Machine Learning Systems" book with Excalidraw. Use when a chapter needs a new or redrawn diagram (architecture, pipeline, flowchart, data flow, sequence, mind map), or when asked to "draw", "diagram", "make a figure" or "create an Excalidraw file" for this repo. Produces an editable .excalidraw source plus an exported SVG/PNG under v1/img/chXX/, with EN/ZH variants when the figure contains text. +--- + +# Excalidraw figures for openmlsys + +Every figure in the book has two files side by side: the editable `.excalidraw` +source and the exported `.svg` (preferred) or `.png` that the Markdown +references. Read [references/openmlsys-conventions.md](references/openmlsys-conventions.md) +before drawing. It covers paths, house style, dark mode and bilingual variants. + +## Workflow + +1. **Plan the figure.** Pick the diagram type and list the nodes, edges and + labels. Keep it to about 20 elements. If it needs more, split it into + several figures. +2. **Choose the paths.** Images live in `v1/img/ch/`, which every chapter + tree reaches through its `img` symlink. Name files in lowercase with + hyphens, e.g. `v1/img/ch03/tensor-core-dataflow.excalidraw`. v1 and v2 + share this folder, so check that the name is not taken. +3. **Write the scene.** Start from a file in [templates/](templates/) or write + the JSON yourself. The schema is in + [references/excalidraw-schema.md](references/excalidraw-schema.md) and the + element specs are in [references/element-types.md](references/element-types.md). + House rules: + - Text uses `fontFamily: 5` (Excalifont), or `3` for code. Use `fontSize` + 16 or more and the dark stroke `#1e1e1e`. + - A label inside a shape is a **separate `text` element** with + `containerId` set to the shape. The shape must list it in + `boundElements`. A `text` property on a rectangle is silently ignored. + - Use pastel fills, dark strokes and no white or light-colored text (see + the palette in the conventions file). + - To add a connector to an existing file, run + `python3 .claude/skills/excalidraw-diagram/scripts/add-arrow.py x1 y1 x2 y2 [--label T] [--style dashed]`. +4. **Make the ZH variant** if the figure contains words. Copy the scene to + `-zh.excalidraw` and translate only the text. Then widen any boxes + that the Chinese labels overflow. Figures without text use one shared file. +5. **Lint.** Run `python3 .claude/skills/excalidraw-diagram/scripts/check_figures.py `. + It needs only the standard library. Fix every ERROR. +6. **Export** each scene to `.svg` (or `.png` at 2x) next to its + source: background off, dark mode off, light theme. You can export with the + Excalidraw MCP, excalidraw.com or the VS Code Excalidraw extension (steps in + the conventions file). If you have no tool that can render, stop here. Tell + the contributor which files still need exporting. Do not hand-write an SVG. + **Never export or reference a PDF.** mdBook pages cannot display PDF images. +7. **Reference** the image from the chapter with a relative path: + + ```markdown + ![Data flow of a training step](../img/ch06/training-step-dataflow.svg) + :width:`700px` + :label:`fig-training-step-dataflow` + ``` + + Use the `-zh.svg` variant in `v2/zh_chapters/...`. Re-run `check_figures.py` + on the edited `.md` files to confirm that the paths resolve. + +## Deliverables checklist + +- [ ] `.excalidraw` + exported `.svg|png` (and `-zh` pair if the figure has text) +- [ ] `check_figures.py` reports 0 errors for the scenes and the edited chapter files +- [ ] Markdown references the SVG/PNG, never `.pdf` or `.excalidraw` +- [ ] Commit the source and the export together + +## Attribution + +Adapted from the `excalidraw-diagram-generator` skill in +[github/awesome-copilot](https://github.com/github/awesome-copilot/tree/main/skills/excalidraw-diagram-generator) +(MIT, see [LICENSE.txt](LICENSE.txt)). diff --git a/.claude/skills/excalidraw-diagram/references/element-types.md b/.claude/skills/excalidraw-diagram/references/element-types.md new file mode 100644 index 0000000..3e8adcd --- /dev/null +++ b/.claude/skills/excalidraw-diagram/references/element-types.md @@ -0,0 +1,499 @@ +# Excalidraw Element Types Guide + +> **openmlsys note:** This file comes from github/awesome-copilot (MIT, see `../LICENSE.txt`) and is kept unchanged. Where it shows `fontFamily` 1 to 3, use `5` (Excalifont, or `3` for code) instead. House style and labels inside shapes are covered in [openmlsys-conventions.md](openmlsys-conventions.md). + +Detailed specifications for each Excalidraw element type with visual examples and use cases. + +## Element Type Overview + +| Type | Visual | Primary Use | Text Support | +|------|--------|-------------|--------------| +| `rectangle` | □ | Boxes, containers, process steps | ✅ Yes | +| `ellipse` | ○ | Emphasis, terminals, states | ✅ Yes | +| `diamond` | ◇ | Decision points, choices | ✅ Yes | +| `arrow` | → | Directional flow, relationships | ❌ No (use separate text) | +| `line` | — | Connections, dividers | ❌ No | +| `text` | A | Labels, annotations, titles | ✅ (Its purpose) | + +--- + +## Rectangle + +**Best for:** Process steps, entities, data stores, components + +### Properties + +```typescript +{ + type: "rectangle", + roundness: { type: 3 }, // Rounded corners + text: "Step Name", // Optional embedded text + fontSize: 20, + textAlign: "center", + verticalAlign: "middle" +} +``` + +### Use Cases + +| Scenario | Configuration | +|----------|---------------| +| **Process step** | Green background (`#b2f2bb`), centered text | +| **Entity/Object** | Blue background (`#a5d8ff`), medium size | +| **System component** | Light color, descriptive text | +| **Data store** | Gray/white, database-like label | + +### Size Guidelines + +| Content | Width | Height | +|---------|-------|--------| +| Single word | 120-150px | 60-80px | +| Short phrase (2-4 words) | 180-220px | 80-100px | +| Sentence | 250-300px | 100-120px | + +### Example + +```json +{ + "type": "rectangle", + "x": 100, + "y": 100, + "width": 200, + "height": 80, + "backgroundColor": "#b2f2bb", + "text": "Validate Input", + "fontSize": 20, + "textAlign": "center", + "verticalAlign": "middle", + "roundness": { "type": 3 } +} +``` + +--- + +## Ellipse + +**Best for:** Start/end points, states, emphasis circles + +### Properties + +```typescript +{ + type: "ellipse", + text: "Start", + fontSize: 18, + textAlign: "center", + verticalAlign: "middle" +} +``` + +### Use Cases + +| Scenario | Configuration | +|----------|---------------| +| **Flow start** | Light green, "Start" text | +| **Flow end** | Light red, "End" text | +| **State** | Soft color, state name | +| **Highlight** | Bright color, emphasis text | + +### Size Guidelines + +For circular shapes, use `width === height`: + +| Content | Diameter | +|---------|----------| +| Icon/Symbol | 60-80px | +| Short text | 100-120px | +| Longer text | 150-180px | + +### Example + +```json +{ + "type": "ellipse", + "x": 100, + "y": 100, + "width": 120, + "height": 120, + "backgroundColor": "#d0f0c0", + "text": "Start", + "fontSize": 18, + "textAlign": "center", + "verticalAlign": "middle" +} +``` + +--- + +## Diamond + +**Best for:** Decision points, conditional branches + +### Properties + +```typescript +{ + type: "diamond", + text: "Valid?", + fontSize: 18, + textAlign: "center", + verticalAlign": "middle" +} +``` + +### Use Cases + +| Scenario | Text Example | +|----------|--------------| +| **Yes/No decision** | "Is Valid?", "Exists?" | +| **Multiple choice** | "Type?", "Status?" | +| **Conditional** | "Score > 50?" | + +### Size Guidelines + +Diamonds need more space than rectangles for the same text: + +| Content | Width | Height | +|---------|-------|--------| +| Yes/No | 120-140px | 120-140px | +| Short question | 160-180px | 160-180px | +| Longer question | 200-220px | 200-220px | + +### Example + +```json +{ + "type": "diamond", + "x": 100, + "y": 100, + "width": 150, + "height": 150, + "backgroundColor": "#ffe4a3", + "text": "Valid?", + "fontSize": 18, + "textAlign": "center", + "verticalAlign": "middle" +} +``` + +--- + +## Arrow + +**Best for:** Flow direction, relationships, dependencies + +### Properties + +```typescript +{ + type: "arrow", + points: [[0, 0], [endX, endY]], // Relative coordinates + roundness: { type: 2 }, // Curved + startBinding: null, // Or { elementId, focus, gap } + endBinding: null +} +``` + +### Arrow Directions + +#### Horizontal (Left to Right) + +```json +{ + "x": 100, + "y": 150, + "width": 200, + "height": 0, + "points": [[0, 0], [200, 0]] +} +``` + +#### Vertical (Top to Bottom) + +```json +{ + "x": 200, + "y": 100, + "width": 0, + "height": 150, + "points": [[0, 0], [0, 150]] +} +``` + +#### Diagonal + +```json +{ + "x": 100, + "y": 100, + "width": 200, + "height": 150, + "points": [[0, 0], [200, 150]] +} +``` + +### Arrow Styles + +| Style | `strokeStyle` | `strokeWidth` | Use Case | +|-------|---------------|---------------|----------| +| **Normal flow** | `"solid"` | 2 | Standard connections | +| **Optional/Weak** | `"dashed"` | 2 | Optional paths | +| **Important** | `"solid"` | 3-4 | Emphasized flow | +| **Dotted** | `"dotted"` | 2 | Indirect relationships | + +### Adding Arrow Labels + +Use separate text elements positioned near arrow midpoint: + +```json +[ + { + "type": "arrow", + "id": "arrow1", + "x": 100, + "y": 150, + "points": [[0, 0], [200, 0]] + }, + { + "type": "text", + "x": 180, // Near midpoint + "y": 130, // Above arrow + "text": "sends", + "fontSize": 14 + } +] +``` + +--- + +## Line + +**Best for:** Non-directional connections, dividers, borders + +### Properties + +```typescript +{ + type: "line", + points: [[0, 0], [x2, y2], [x3, y3], ...], + roundness: null // Or { type: 2 } for curved +} +``` + +### Use Cases + +| Scenario | Configuration | +|----------|---------------| +| **Divider** | Horizontal, thin stroke | +| **Border** | Closed path (polygon) | +| **Connection** | Multi-point path | +| **Underline** | Short horizontal line | + +### Multi-Point Line Example + +```json +{ + "type": "line", + "x": 100, + "y": 100, + "points": [ + [0, 0], + [100, 50], + [200, 0] + ] +} +``` + +--- + +## Text + +**Best for:** Labels, titles, annotations, standalone text + +### Properties + +```typescript +{ + type: "text", + text: "Label text", + fontSize: 20, + fontFamily: 1, // 1=Virgil, 2=Helvetica, 3=Cascadia + textAlign: "left", + verticalAlign: "top" +} +``` + +### Font Sizes by Purpose + +| Purpose | Font Size | +|---------|-----------| +| **Main title** | 28-36 | +| **Section header** | 24-28 | +| **Element label** | 18-22 | +| **Annotation** | 14-16 | +| **Small note** | 12-14 | + +### Width/Height Calculation + +```javascript +// Approximate width +const width = text.length * fontSize * 0.6; + +// Approximate height (single line) +const height = fontSize * 1.2; + +// Multi-line +const lines = text.split('\n').length; +const height = fontSize * 1.2 * lines; +``` + +### Text Positioning + +| Position | textAlign | verticalAlign | Use Case | +|----------|-----------|---------------|----------| +| **Top-left** | `"left"` | `"top"` | Default labels | +| **Centered** | `"center"` | `"middle"` | Titles | +| **Bottom-right** | `"right"` | `"bottom"` | Footnotes | + +### Example: Title + +```json +{ + "type": "text", + "x": 100, + "y": 50, + "width": 400, + "height": 40, + "text": "System Architecture", + "fontSize": 32, + "fontFamily": 2, + "textAlign": "center", + "verticalAlign": "top" +} +``` + +### Example: Annotation + +```json +{ + "type": "text", + "x": 150, + "y": 200, + "width": 100, + "height": 20, + "text": "User input", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top" +} +``` + +--- + +## Combining Elements + +### Pattern: Labeled Box + +```json +[ + { + "type": "rectangle", + "id": "box1", + "x": 100, + "y": 100, + "width": 200, + "height": 100, + "text": "Component", + "textAlign": "center", + "verticalAlign": "middle" + } +] +``` + +### Pattern: Connected Boxes + +```json +[ + { + "type": "rectangle", + "id": "box1", + "x": 100, + "y": 100, + "width": 150, + "height": 80, + "text": "Step 1" + }, + { + "type": "arrow", + "id": "arrow1", + "x": 250, + "y": 140, + "points": [[0, 0], [100, 0]] + }, + { + "type": "rectangle", + "id": "box2", + "x": 350, + "y": 100, + "width": 150, + "height": 80, + "text": "Step 2" + } +] +``` + +### Pattern: Decision Tree + +```json +[ + { + "type": "diamond", + "id": "decision", + "x": 100, + "y": 100, + "width": 140, + "height": 140, + "text": "Valid?" + }, + { + "type": "arrow", + "id": "yes-arrow", + "x": 240, + "y": 170, + "points": [[0, 0], [60, 0]] + }, + { + "type": "text", + "id": "yes-label", + "x": 250, + "y": 150, + "text": "Yes", + "fontSize": 14 + }, + { + "type": "rectangle", + "id": "yes-box", + "x": 300, + "y": 140, + "width": 120, + "height": 60, + "text": "Process" + } +] +``` + +--- + +## Summary + +| When you need... | Use this element | +|------------------|------------------| +| Process box | `rectangle` with text | +| Decision point | `diamond` with question | +| Flow direction | `arrow` | +| Start/End | `ellipse` | +| Title/Header | `text` (large font) | +| Annotation | `text` (small font) | +| Non-directional link | `line` | +| Divider | `line` (horizontal) | diff --git a/.claude/skills/excalidraw-diagram/references/excalidraw-schema.md b/.claude/skills/excalidraw-diagram/references/excalidraw-schema.md new file mode 100644 index 0000000..900ff23 --- /dev/null +++ b/.claude/skills/excalidraw-diagram/references/excalidraw-schema.md @@ -0,0 +1,352 @@ +# Excalidraw JSON Schema Reference + +> **openmlsys note:** This file comes from github/awesome-copilot (MIT, see `../LICENSE.txt`) and is kept unchanged. Where it shows `fontFamily` 1 to 3, use `5` (Excalifont, or `3` for code) instead. House style and labels inside shapes are covered in [openmlsys-conventions.md](openmlsys-conventions.md). + +This document describes the structure of Excalidraw `.excalidraw` files for diagram generation. + +## Top-Level Structure + +```typescript +interface ExcalidrawFile { + type: "excalidraw"; + version: number; // Always 2 + source: string; // "https://excalidraw.com" + elements: ExcalidrawElement[]; + appState: AppState; + files: Record; // Usually empty {} +} +``` + +## AppState + +```typescript +interface AppState { + viewBackgroundColor: string; // Hex color, e.g., "#ffffff" + gridSize: number; // Typically 20 +} +``` + +## ExcalidrawElement Base Properties + +All elements share these common properties: + +```typescript +interface BaseElement { + id: string; // Unique identifier + type: ElementType; // See Element Types below + x: number; // X coordinate (pixels from top-left) + y: number; // Y coordinate (pixels from top-left) + width: number; // Width in pixels + height: number; // Height in pixels + angle: number; // Rotation angle in radians (usually 0) + strokeColor: string; // Hex color, e.g., "#1e1e1e" + backgroundColor: string; // Hex color or "transparent" + fillStyle: "solid" | "hachure" | "cross-hatch"; + strokeWidth: number; // 1-4 typically + strokeStyle: "solid" | "dashed" | "dotted"; + roughness: number; // 0-2, controls hand-drawn effect (1 = default) + opacity: number; // 0-100 + groupIds: string[]; // IDs of groups this element belongs to + frameId: null; // Usually null + index: string; // Stacking order identifier + roundness: Roundness | null; + seed: number; // Random seed for deterministic rendering + version: number; // Element version (increment on edit) + versionNonce: number; // Random number changed on edit + isDeleted: boolean; // Should be false + boundElements: any; // Usually null + updated: number; // Timestamp in milliseconds + link: null; // External link (usually null) + locked: boolean; // Whether element is locked +} +``` + +## Element Types + +### Rectangle + +```typescript +interface RectangleElement extends BaseElement { + type: "rectangle"; + roundness: { type: 3 }; // 3 = rounded corners + text?: string; // Optional text inside + fontSize?: number; // Font size (16-32 typical) + fontFamily?: number; // 1 = Virgil, 2 = Helvetica, 3 = Cascadia + textAlign?: "left" | "center" | "right"; + verticalAlign?: "top" | "middle" | "bottom"; +} +``` + +**Example:** +```json +{ + "id": "rect1", + "type": "rectangle", + "x": 100, + "y": 100, + "width": 200, + "height": 100, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "text": "My Box", + "fontSize": 20, + "textAlign": "center", + "verticalAlign": "middle", + "roundness": { "type": 3 } +} +``` + +### Ellipse + +```typescript +interface EllipseElement extends BaseElement { + type: "ellipse"; + text?: string; + fontSize?: number; + fontFamily?: number; + textAlign?: "left" | "center" | "right"; + verticalAlign?: "top" | "middle" | "bottom"; +} +``` + +### Diamond + +```typescript +interface DiamondElement extends BaseElement { + type: "diamond"; + text?: string; + fontSize?: number; + fontFamily?: number; + textAlign?: "left" | "center" | "right"; + verticalAlign?: "top" | "middle" | "bottom"; +} +``` + +### Arrow + +```typescript +interface ArrowElement extends BaseElement { + type: "arrow"; + points: [number, number][]; // Array of [x, y] coordinates relative to element + startBinding: Binding | null; + endBinding: Binding | null; + roundness: { type: 2 }; // 2 = curved arrow +} +``` + +**Example:** +```json +{ + "id": "arrow1", + "type": "arrow", + "x": 100, + "y": 100, + "width": 200, + "height": 0, + "points": [ + [0, 0], + [200, 0] + ], + "roundness": { "type": 2 }, + "startBinding": null, + "endBinding": null +} +``` + +**Points explanation:** +- First point `[0, 0]` is relative to `(x, y)` +- Subsequent points are relative to the first point +- For straight horizontal arrow: `[[0, 0], [width, 0]]` +- For straight vertical arrow: `[[0, 0], [0, height]]` + +### Line + +```typescript +interface LineElement extends BaseElement { + type: "line"; + points: [number, number][]; + startBinding: Binding | null; + endBinding: Binding | null; + roundness: { type: 2 } | null; +} +``` + +### Text + +```typescript +interface TextElement extends BaseElement { + type: "text"; + text: string; + fontSize: number; + fontFamily: number; // 1-3 + textAlign: "left" | "center" | "right"; + verticalAlign: "top" | "middle" | "bottom"; + roundness: null; // Text has no roundness +} +``` + +**Example:** +```json +{ + "id": "text1", + "type": "text", + "x": 100, + "y": 100, + "width": 150, + "height": 25, + "text": "Hello World", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "roundness": null +} +``` + +**Width/Height calculation:** +- Width ≈ `text.length * fontSize * 0.6` +- Height ≈ `fontSize * 1.2 * numberOfLines` + +## Bindings + +Bindings connect arrows to shapes: + +```typescript +interface Binding { + elementId: string; // ID of bound element + focus: number; // -1 to 1, position along edge + gap: number; // Distance from element edge +} +``` + +## Common Colors + +| Color Name | Hex Code | Use Case | +|------------|----------|----------| +| Black | `#1e1e1e` | Default stroke | +| Light Blue | `#a5d8ff` | Primary entities | +| Light Green | `#b2f2bb` | Process steps | +| Yellow | `#ffd43b` | Important/Central | +| Light Red | `#ffc9c9` | Warnings/Errors | +| Cyan | `#96f2d7` | Secondary items | +| Transparent | `transparent` | No fill | +| White | `#ffffff` | Background | + +## ID Generation + +IDs should be unique strings. Common patterns: + +```javascript +// Timestamp-based +const id = Date.now().toString(36) + Math.random().toString(36).substr(2); + +// Sequential +const id = "element-" + counter++; + +// Descriptive +const id = "step-1", "entity-user", "arrow-1-to-2"; +``` + +## Seed Generation + +Seeds are used for deterministic randomness in hand-drawn effect: + +```javascript +const seed = Math.floor(Math.random() * 2147483647); +``` + +## Version and VersionNonce + +```javascript +const version = 1; // Increment when element is edited +const versionNonce = Math.floor(Math.random() * 2147483647); +``` + +## Coordinate System + +- Origin `(0, 0)` is top-left corner +- X increases to the right +- Y increases downward +- All units are in pixels + +## Recommended Spacing + +| Context | Spacing | +|---------|---------| +| Horizontal gap between elements | 200-300px | +| Vertical gap between rows | 100-150px | +| Minimum margin from edge | 50px | +| Arrow-to-box clearance | 20-30px | + +## Font Families + +| ID | Name | Description | +|----|------|-------------| +| 1 | Virgil | Hand-drawn style (default) | +| 2 | Helvetica | Clean sans-serif | +| 3 | Cascadia | Monospace | + +## Validation Rules + +✅ **Required:** +- All IDs must be unique +- `type` must match actual element type +- `version` must be an integer ≥ 1 +- `opacity` must be 0-100 + +⚠️ **Recommended:** +- Keep `roughness` at 1 for consistency +- Use `strokeWidth` of 2 for clarity +- Set `isDeleted` to `false` +- Set `locked` to `false` +- Keep `frameId`, `boundElements`, `link` as `null` + +## Complete Minimal Example + +```json +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "box1", + "type": "rectangle", + "x": 100, + "y": 100, + "width": 200, + "height": 100, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a0", + "roundness": { "type": 3 }, + "seed": 1234567890, + "version": 1, + "versionNonce": 987654321, + "isDeleted": false, + "boundElements": null, + "updated": 1706659200000, + "link": null, + "locked": false, + "text": "Hello", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "middle" + } + ], + "appState": { + "viewBackgroundColor": "#ffffff", + "gridSize": 20 + }, + "files": {} +} +``` diff --git a/.claude/skills/excalidraw-diagram/references/openmlsys-conventions.md b/.claude/skills/excalidraw-diagram/references/openmlsys-conventions.md new file mode 100644 index 0000000..6a1b82c --- /dev/null +++ b/.claude/skills/excalidraw-diagram/references/openmlsys-conventions.md @@ -0,0 +1,143 @@ +# openmlsys figure conventions + +These rules apply to every Excalidraw figure in the book. They extend +`CONTRIBUTING/style_en.md`, section "Figure Guidelines". + +## Where files go + +| What | Path | +|------|------| +| Image folder (physical) | `v1/img/ch/` | +| Same folder seen from a chapter | `v1/{en,zh}_chapters/img` → `../img`, `v2/{en,zh}_chapters/img` → `../../v1/img` (symlinks) | +| Chapter file (v2) | `v2/en_chapters/_chapter_/
.md` | +| Reference from a chapter | `../img/ch/.svg` | + +- `NN` is the chapter number of the chapter that uses the figure, for example + `v2/en_chapters/03_chapter_accelerator/` → `img/ch03/`. v1 and v2 share this + folder, so run `ls v1/img/ch/` first to avoid name clashes. +- Use lowercase names with hyphens: `pipeline-parallel-schedule`. +- The source and the export share a stem. Commit both: + + ``` + v1/img/ch06/pipeline-parallel-schedule.excalidraw # editable source (EN) + v1/img/ch06/pipeline-parallel-schedule.svg # exported, used by en_chapters + v1/img/ch06/pipeline-parallel-schedule-zh.excalidraw # editable source (ZH) + v1/img/ch06/pipeline-parallel-schedule-zh.svg # exported, used by zh_chapters + ``` +- Do not save a source as `.json`. The repo `.gitignore` ignores `*.json`. + +## Export formats + +- **SVG** is the default for diagrams. Use **PNG** at 2x scale, at most + 1200 px wide, only if the SVG renders badly (very large scenes, embedded + bitmaps). +- **Never use PDF.** mdBook output is HTML, and browsers do not render + `![](x.pdf)` as an image. Some old v1 pages still point at `.pdf` files. + Replace those references when you redraw the figures. Do not copy the pattern. +- Do not reference the `.excalidraw` file from Markdown. + +### How to export + +Use one of the following: + +1. **Excalidraw MCP.** The style guide recommends the official server at + `https://mcp.excalidraw.com` or the self-hosted `interactive-drawer`. Load + the scene and export it to SVG or PNG with the settings below. +2. **excalidraw.com** (no install). Open the `.excalidraw` file, then + *Menu → Export image*. Set *Background* off, *Dark mode* off and scale 2x + for PNG, then save as SVG or PNG. Use *Menu → Save to…* to write the + `.excalidraw` file back after you edit it. +3. **VS Code "Excalidraw" extension** (`pomdtr.excalidraw-editor`). It edits + `.excalidraw` files in the editor and exports through the same dialog. + +Export settings are the same for every method: light theme, no background +(the style guide asks for figures without a white background), and +`appState.exportWithDarkMode` false. + +## Dark mode + +The book's themes include `theme/dark-mode-images.css` (see +`tests/test_dark_mode_images_css.py`). In the dark themes `navy`, `coal` and +`ayu`, every body image gets a light gray `#e8e8e8` backdrop with 8 px +padding. Images are never inverted. A transparent figure therefore sits on +white in light mode and on `#e8e8e8` in dark mode. Consequences: + +- Text and strokes must be dark. Use `#1e1e1e` by default. White or pastel + text disappears, and `check_figures.py` reports it as an error. +- Every shape needs a visible dark stroke. A very light fill (`#e9ecef`, + `#f8f9fa`) blends into the dark-mode backdrop, so do not rely on fill alone. +- Do not export with Excalidraw's "dark mode" option. +- Do not encode meaning by color alone. Add a label or a dashed/solid style + difference so the figure also works in grayscale print. + +## Palette + +These are Excalidraw's built-in swatches, so the figures stay editable from +the stock UI. + +| Role | Fill (`backgroundColor`) | Stroke / text | +|------|--------------------------|---------------| +| Default component | `#a5d8ff` (blue) | `#1e1e1e` | +| Data / tensors | `#b2f2bb` (green) | `#1e1e1e` | +| Highlight / focus | `#ffec99` (yellow) | `#1e1e1e` | +| Hardware / device | `#d0bfff` (violet) | `#1e1e1e` | +| Error / bottleneck | `#ffc9c9` (red) | `#1e1e1e` | +| Group / region frame | `transparent`, dashed stroke | `#495057` | +| Accent arrows (optional) | – | `#1971c2`, `#2f9e44`, `#e03131`, `#f08c00` | + +Keep `fillStyle: "solid"`, `strokeWidth: 2`, `roughness: 1` and +`roundness: {"type": 3}` for boxes. Use the same values across one chapter. + +## Text and fonts + +- Use `fontFamily: 5` (Excalifont) for all labels and `3` (Cascadia) for code + or identifiers. `1` (Virgil) is the legacy hand-drawn font and `2` + (Helvetica) depends on the reader's system fonts. Do not use either. +- Excalidraw falls back to its bundled CJK hand-drawn font (Xiaolai) for + Chinese automatically, and SVG export embeds the glyphs it uses. You do not + need a different `fontFamily` for ZH variants. +- Use `fontSize` 20 for box labels, 16 for edge labels and annotations, and + 28 for an optional in-figure title. Most figures should have no title, + because the Markdown caption serves that purpose. +- For math, write plain Unicode such as `W·x + b` or `∇L`. LaTeX is not + rendered inside Excalidraw. + +### Labels inside shapes (bound text) + +```json +{ "id": "box1", "type": "rectangle", "...": "...", + "boundElements": [{ "type": "text", "id": "box1-label" }] }, +{ "id": "box1-label", "type": "text", "containerId": "box1", + "text": "Parameter Server", "originalText": "Parameter Server", + "fontSize": 20, "fontFamily": 5, "textAlign": "center", + "verticalAlign": "middle", "lineHeight": 1.25, "autoResize": true, + "strokeColor": "#1e1e1e", "...": "..." } +``` + +Center the text element in its container. Estimate the width as about +0.6 × fontSize per Latin character and 1.0 × fontSize per CJK character. +Excalidraw re-measures the text when the file is opened. Arrows that connect +shapes should set `startBinding`/`endBinding` (`{"elementId": ..., "focus": 0, "gap": 4}`). +The bound shapes must list the arrow in their `boundElements`. + +## Bilingual figures + +- If a figure contains words, make two variants: `` (English, used by + `en_chapters`) and `-zh` (Chinese, used by `zh_chapters`). +- Build the ZH scene from the EN scene. Keep the ids and the geometry, and + translate only `text`/`originalText`. Chinese labels are often wider, so + widen the boxes and move neighbors symmetrically if needed. +- Keep established English acronyms (GPU, NCCL, AllReduce, KV cache) in the ZH + variant. Follow `CONTRIBUTING/terminology.md` for the translated terms. +- A figure with no words, or only symbols and math, uses one file shared by + both languages. +- Captions belong in the Markdown in each language, not in the image. + +## Layout tips + +- Draw main flows left to right, or top to bottom for stacks and layers. +- Leave a 40–80 px gap between boxes and use one box size per role. +- Use about 20 elements per figure at most. Split a dense figure and refer + to both parts in the text. +- Leave a margin of about 20 px around the content so the exported image + is not cropped tight. diff --git a/.claude/skills/excalidraw-diagram/scripts/add-arrow.py b/.claude/skills/excalidraw-diagram/scripts/add-arrow.py new file mode 100755 index 0000000..b23bc83 --- /dev/null +++ b/.claude/skills/excalidraw-diagram/scripts/add-arrow.py @@ -0,0 +1,317 @@ +#!/usr/bin/env python3 +""" +Add arrows (connections) between elements in Excalidraw diagrams. + +From github/awesome-copilot (MIT, see ../LICENSE.txt); label font size raised to 16 for openmlsys. + +Usage: + python add-arrow.py [OPTIONS] + +Options: + --style {solid|dashed|dotted} Arrow line style (default: solid) + --color HEX Arrow color (default: #1e1e1e) + --label TEXT Add text label on the arrow + --use-edit-suffix Edit via .excalidraw.edit to avoid editor overwrite issues (enabled by default; use --no-use-edit-suffix to disable) + +Examples: + python add-arrow.py diagram.excalidraw 300 200 500 300 + python add-arrow.py diagram.excalidraw 300 200 500 300 --label "HTTP" + python add-arrow.py diagram.excalidraw 300 200 500 300 --style dashed --color "#7950f2" + python add-arrow.py diagram.excalidraw 300 200 500 300 --use-edit-suffix +""" + +import json +import sys +import uuid +from pathlib import Path +from typing import Dict, Any + + +def generate_unique_id() -> str: + """Generate a unique ID for Excalidraw elements.""" + return str(uuid.uuid4()).replace('-', '')[:16] + + +def prepare_edit_path(diagram_path: Path, use_edit_suffix: bool) -> tuple[Path, Path | None]: + """ + Prepare a safe edit path to avoid editor overwrite issues. + + Returns: + (work_path, final_path) + - work_path: file path to read/write during edit + - final_path: file path to rename back to (or None if not used) + """ + if not use_edit_suffix: + return diagram_path, None + + if diagram_path.suffix != ".excalidraw": + return diagram_path, None + + edit_path = diagram_path.with_suffix(diagram_path.suffix + ".edit") + + if diagram_path.exists(): + if edit_path.exists(): + raise FileExistsError(f"Edit file already exists: {edit_path}") + diagram_path.rename(edit_path) + + return edit_path, diagram_path + + +def finalize_edit_path(work_path: Path, final_path: Path | None) -> None: + """Finalize edit by renaming .edit back to .excalidraw if needed.""" + if final_path is None: + return + + if final_path.exists(): + final_path.unlink() + + work_path.rename(final_path) + + +def create_arrow( + from_x: float, + from_y: float, + to_x: float, + to_y: float, + style: str = "solid", + color: str = "#1e1e1e", + label: str = None +) -> list: + """ + Create an arrow element. + + Args: + from_x: Starting X coordinate + from_y: Starting Y coordinate + to_x: Ending X coordinate + to_y: Ending Y coordinate + style: Line style (solid, dashed, dotted) + color: Arrow color + label: Optional text label on the arrow + + Returns: + List of elements (arrow and optional label) + """ + elements = [] + + # Arrow element + arrow = { + "id": generate_unique_id(), + "type": "arrow", + "x": from_x, + "y": from_y, + "width": to_x - from_x, + "height": to_y - from_y, + "angle": 0, + "strokeColor": color, + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": style, + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": None, + "index": "a0", + "roundness": { + "type": 2 + }, + "seed": 1000000000 + hash(f"{from_x}{from_y}{to_x}{to_y}") % 1000000000, + "version": 1, + "versionNonce": 2000000000 + hash(f"{from_x}{from_y}{to_x}{to_y}") % 1000000000, + "isDeleted": False, + "boundElements": [], + "updated": 1738195200000, + "link": None, + "locked": False, + "points": [ + [0, 0], + [to_x - from_x, to_y - from_y] + ], + "startBinding": None, + "endBinding": None, + "startArrowhead": None, + "endArrowhead": "arrow", + "lastCommittedPoint": None + } + elements.append(arrow) + + # Optional label + if label: + mid_x = (from_x + to_x) / 2 - (len(label) * 5) + mid_y = (from_y + to_y) / 2 - 10 + + label_element = { + "id": generate_unique_id(), + "type": "text", + "x": mid_x, + "y": mid_y, + "width": len(label) * 10, + "height": 20, + "angle": 0, + "strokeColor": color, + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": None, + "index": "a0", + "roundness": None, + "seed": 1000000000 + hash(label) % 1000000000, + "version": 1, + "versionNonce": 2000000000 + hash(label) % 1000000000, + "isDeleted": False, + "boundElements": [], + "updated": 1738195200000, + "link": None, + "locked": False, + "text": label, + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "top", + "containerId": None, + "originalText": label, + "autoResize": True, + "lineHeight": 1.25 + } + elements.append(label_element) + + return elements + + +def add_arrow_to_diagram( + diagram_path: Path, + from_x: float, + from_y: float, + to_x: float, + to_y: float, + style: str = "solid", + color: str = "#1e1e1e", + label: str = None +) -> None: + """ + Add an arrow to an Excalidraw diagram. + + Args: + diagram_path: Path to the Excalidraw diagram file + from_x: Starting X coordinate + from_y: Starting Y coordinate + to_x: Ending X coordinate + to_y: Ending Y coordinate + style: Line style (solid, dashed, dotted) + color: Arrow color + label: Optional text label + """ + print(f"Creating arrow from ({from_x}, {from_y}) to ({to_x}, {to_y})") + arrow_elements = create_arrow(from_x, from_y, to_x, to_y, style, color, label) + + if label: + print(f" With label: '{label}'") + + # Load diagram + print(f"Loading diagram: {diagram_path}") + with open(diagram_path, 'r', encoding='utf-8') as f: + diagram = json.load(f) + + # Add arrow elements + if 'elements' not in diagram: + diagram['elements'] = [] + + original_count = len(diagram['elements']) + diagram['elements'].extend(arrow_elements) + print(f" Added {len(arrow_elements)} elements (total: {original_count} -> {len(diagram['elements'])})") + + # Save diagram + print(f"Saving diagram") + with open(diagram_path, 'w', encoding='utf-8') as f: + json.dump(diagram, f, indent=2, ensure_ascii=False) + + print(f"✓ Successfully added arrow to diagram") + + +def main(): + """Main entry point.""" + if hasattr(sys.stdout, "reconfigure"): + # Ensure consistent UTF-8 output on Windows consoles. + sys.stdout.reconfigure(encoding="utf-8") + if len(sys.argv) < 6: + print("Usage: python add-arrow.py [OPTIONS]") + print("\nOptions:") + print(" --style {solid|dashed|dotted} Line style (default: solid)") + print(" --color HEX Color (default: #1e1e1e)") + print(" --label TEXT Text label on arrow") + print(" --use-edit-suffix Edit via .excalidraw.edit to avoid editor overwrite issues (enabled by default; use --no-use-edit-suffix to disable)") + print("\nExamples:") + print(" python add-arrow.py diagram.excalidraw 300 200 500 300") + print(" python add-arrow.py diagram.excalidraw 300 200 500 300 --label 'HTTP'") + sys.exit(1) + + diagram_path = Path(sys.argv[1]) + from_x = float(sys.argv[2]) + from_y = float(sys.argv[3]) + to_x = float(sys.argv[4]) + to_y = float(sys.argv[5]) + + # Parse optional arguments + style = "solid" + color = "#1e1e1e" + label = None + # Default: use edit suffix to avoid editor overwrite issues + use_edit_suffix = True + + i = 6 + while i < len(sys.argv): + if sys.argv[i] == '--style': + if i + 1 < len(sys.argv): + style = sys.argv[i + 1] + if style not in ['solid', 'dashed', 'dotted']: + print(f"Error: Invalid style '{style}'. Must be: solid, dashed, or dotted") + sys.exit(1) + i += 2 + else: + print("Error: --style requires an argument") + sys.exit(1) + elif sys.argv[i] == '--color': + if i + 1 < len(sys.argv): + color = sys.argv[i + 1] + i += 2 + else: + print("Error: --color requires an argument") + sys.exit(1) + elif sys.argv[i] == '--label': + if i + 1 < len(sys.argv): + label = sys.argv[i + 1] + i += 2 + else: + print("Error: --label requires a text argument") + sys.exit(1) + elif sys.argv[i] == '--use-edit-suffix': + use_edit_suffix = True + i += 1 + elif sys.argv[i] == '--no-use-edit-suffix': + use_edit_suffix = False + i += 1 + else: + print(f"Error: Unknown option: {sys.argv[i]}") + sys.exit(1) + + # Validate inputs + if not diagram_path.exists(): + print(f"Error: Diagram file not found: {diagram_path}") + sys.exit(1) + + try: + work_path, final_path = prepare_edit_path(diagram_path, use_edit_suffix) + add_arrow_to_diagram(work_path, from_x, from_y, to_x, to_y, style, color, label) + finalize_edit_path(work_path, final_path) + except Exception as e: + print(f"Error: {e}") + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/.claude/skills/excalidraw-diagram/scripts/check_figures.py b/.claude/skills/excalidraw-diagram/scripts/check_figures.py new file mode 100755 index 0000000..7c5fa05 --- /dev/null +++ b/.claude/skills/excalidraw-diagram/scripts/check_figures.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +"""Lint Excalidraw sources and figure references for the openmlsys book. + +Standard library only (Python 3.8+). No network access. + +Usage: + python3 check_figures.py PATH [PATH ...] + +PATH may be: + * a ``.excalidraw`` file or a directory (searched recursively) -- each scene + is checked for structural problems and house-style violations, and for an + exported ``.svg``/``.png`` sibling with the same stem; + * a ``.md`` chapter file -- every image reference is checked: it must not + point at a ``.pdf`` and the referenced file must exist. + +Exit status is 1 if any ERROR was reported, otherwise 0 (WARNINGs only). +""" + +from __future__ import annotations + +import json +import re +import sys +from pathlib import Path + +ALLOWED_FONTS = {5, 3} # 5 = Excalifont (body text), 3 = Cascadia (code) +MIN_FONT_SIZE = 16 +IMAGE_REF = re.compile(r"!\[[^\]]*\]\(([^)\s]+)(?:\s+\"[^\"]*\")?\)") + +errors = 0 +warnings = 0 + + +def report(level: str, where: Path, msg: str) -> None: + global errors, warnings + if level == "ERROR": + errors += 1 + else: + warnings += 1 + print(f"{level}: {where}: {msg}") + + +def luminance(color: str) -> float | None: + m = re.fullmatch(r"#([0-9a-fA-F]{6})", color or "") + if not m: + return None + r, g, b = (int(m.group(1)[i : i + 2], 16) / 255 for i in (0, 2, 4)) + return 0.2126 * r + 0.7152 * g + 0.0722 * b + + +def check_scene(path: Path) -> None: + try: + doc = json.loads(path.read_text(encoding="utf-8")) + except (OSError, ValueError) as exc: + report("ERROR", path, f"not valid JSON: {exc}") + return + if doc.get("type") != "excalidraw" or not isinstance(doc.get("elements"), list): + report("ERROR", path, 'expected {"type": "excalidraw", "elements": [...]}') + return + + elements = [e for e in doc["elements"] if not e.get("isDeleted")] + by_id = {} + for el in elements: + eid = el.get("id") + if not eid: + report("ERROR", path, f"element without id: {el.get('type')}") + elif eid in by_id: + report("ERROR", path, f"duplicate id {eid!r}") + by_id[eid] = el + + for el in elements: + eid, etype = el.get("id"), el.get("type") + tag = f"{etype} {eid!r}" + if etype != "text" and "text" in el: + report("ERROR", path, f"{tag} has a 'text' property; Excalidraw ignores it. " + "Use a separate text element with containerId (see references/openmlsys-conventions.md)") + if etype == "text": + if el.get("fontFamily") not in ALLOWED_FONTS: + report("WARNING", path, f"{tag} uses fontFamily {el.get('fontFamily')}; house style is 5 (or 3 for code)") + if (el.get("fontSize") or 0) < MIN_FONT_SIZE: + report("WARNING", path, f"{tag} fontSize {el.get('fontSize')} < {MIN_FONT_SIZE}; may be illegible when scaled") + lum = luminance(el.get("strokeColor", "")) + if lum is not None and lum > 0.6: + report("ERROR", path, f"{tag} text color {el['strokeColor']} is too light for the light page / #e8e8e8 dark-mode backdrop") + cid = el.get("containerId") + if cid: + container = by_id.get(cid) + if container is None: + report("ERROR", path, f"{tag} containerId {cid!r} does not exist") + elif not any(b.get("id") == eid for b in container.get("boundElements") or []): + report("ERROR", path, f"container {cid!r} does not list text {eid!r} in boundElements") + for b in el.get("boundElements") or []: + if b.get("id") not in by_id: + report("ERROR", path, f"{tag} boundElements references missing {b.get('id')!r}") + for key in ("startBinding", "endBinding"): + binding = el.get(key) + if binding and binding.get("elementId") not in by_id: + report("ERROR", path, f"{tag} {key} references missing {binding.get('elementId')!r}") + if etype in ("arrow", "line") and len(el.get("points") or []) < 2: + report("ERROR", path, f"{tag} needs at least two points") + + app = doc.get("appState") or {} + if app.get("exportWithDarkMode"): + report("WARNING", path, "exportWithDarkMode is on; export in light mode (the site handles dark mode)") + + stem = path.name[: -len(".excalidraw")] + if not any((path.parent / f"{stem}{ext}").exists() for ext in (".svg", ".png")): + report("WARNING", path, f"no exported {stem}.svg or {stem}.png next to the source") + + +def check_markdown(path: Path) -> None: + text = path.read_text(encoding="utf-8") + for lineno, line in enumerate(text.splitlines(), 1): + for target in IMAGE_REF.findall(line): + if re.match(r"[a-z]+://", target): + continue + where = f"{path}:{lineno}" + if target.lower().endswith(".pdf"): + report("ERROR", Path(where), f"{target} is a PDF; browsers cannot show it in mdBook, export SVG/PNG instead") + if target.endswith(".excalidraw"): + report("ERROR", Path(where), f"{target} is an Excalidraw source; reference the exported SVG/PNG") + if not (path.parent / target).exists(): + report("ERROR", Path(where), f"{target} does not resolve relative to the chapter file") + + +def main(argv: list[str]) -> int: + if not argv or argv[0] in ("-h", "--help"): + print(__doc__) + return 0 if argv else 2 + for arg in argv: + p = Path(arg) + if p.is_dir(): + for scene in sorted(p.rglob("*.excalidraw")): + check_scene(scene) + elif p.suffix == ".excalidraw": + check_scene(p) + elif p.suffix == ".md": + check_markdown(p) + else: + report("ERROR", p, "not a directory, .excalidraw or .md file") + print(f"{errors} error(s), {warnings} warning(s)") + return 1 if errors else 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/.claude/skills/excalidraw-diagram/templates/data-flow-diagram-template.excalidraw b/.claude/skills/excalidraw-diagram/templates/data-flow-diagram-template.excalidraw new file mode 100644 index 0000000..84c981f --- /dev/null +++ b/.claude/skills/excalidraw-diagram/templates/data-flow-diagram-template.excalidraw @@ -0,0 +1,414 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "external-entity-1", + "type": "rectangle", + "x": 100, + "y": 200, + "width": 120, + "height": 80, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a0", + "roundness": { + "type": 3 + }, + "seed": 1001001001, + "version": 1, + "versionNonce": 2002002002, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "external-entity-1-label" + } + ], + "updated": 1706659200000, + "link": null, + "locked": false + }, + { + "id": "external-entity-1-label", + "type": "text", + "x": 138.4, + "y": 228.75, + "width": 43.199999999999996, + "height": 22.5, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a0V", + "roundness": null, + "seed": 1001001008, + "version": 1, + "versionNonce": 2002002009, + "isDeleted": false, + "boundElements": null, + "updated": 1706659200000, + "link": null, + "locked": false, + "text": "User", + "fontSize": 18, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "external-entity-1", + "originalText": "User", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "data-flow-1", + "type": "arrow", + "x": 220, + "y": 240, + "width": 80, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a1", + "roundness": { + "type": 2 + }, + "seed": 1001001002, + "version": 1, + "versionNonce": 2002002003, + "isDeleted": false, + "boundElements": null, + "updated": 1706659200000, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 80, + 0 + ] + ], + "startBinding": null, + "endBinding": null + }, + { + "id": "flow-label-1", + "type": "text", + "x": 230, + "y": 220, + "width": 80, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a2", + "roundness": null, + "seed": 1001001003, + "version": 1, + "versionNonce": 2002002004, + "isDeleted": false, + "boundElements": null, + "updated": 1706659200000, + "link": null, + "locked": false, + "text": "input data", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top" + }, + { + "id": "process-1", + "type": "ellipse", + "x": 300, + "y": 200, + "width": 120, + "height": 80, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a3", + "roundness": null, + "seed": 1001001004, + "version": 1, + "versionNonce": 2002002005, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "process-1-label" + } + ], + "updated": 1706659200000, + "link": null, + "locked": false + }, + { + "id": "process-1-label", + "type": "text", + "x": 326.4, + "y": 220.0, + "width": 67.2, + "height": 40.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a3V", + "roundness": null, + "seed": 1001001011, + "version": 1, + "versionNonce": 2002002012, + "isDeleted": false, + "boundElements": null, + "updated": 1706659200000, + "link": null, + "locked": false, + "text": "Process\nData", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "process-1", + "originalText": "Process\nData", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "data-flow-2", + "type": "arrow", + "x": 420, + "y": 240, + "width": 80, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a4", + "roundness": { + "type": 2 + }, + "seed": 1001001005, + "version": 1, + "versionNonce": 2002002006, + "isDeleted": false, + "boundElements": null, + "updated": 1706659200000, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 80, + 0 + ] + ], + "startBinding": null, + "endBinding": null + }, + { + "id": "flow-label-2", + "type": "text", + "x": 425, + "y": 220, + "width": 100, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a5", + "roundness": null, + "seed": 1001001006, + "version": 1, + "versionNonce": 2002002007, + "isDeleted": false, + "boundElements": null, + "updated": 1706659200000, + "link": null, + "locked": false, + "text": "processed data", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top" + }, + { + "id": "data-store-1", + "type": "rectangle", + "x": 500, + "y": 200, + "width": 150, + "height": 80, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#96f2d7", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a6", + "roundness": null, + "seed": 1001001007, + "version": 1, + "versionNonce": 2002002008, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "data-store-1-label" + } + ], + "updated": 1706659200000, + "link": null, + "locked": false + }, + { + "id": "data-store-1-label", + "type": "text", + "x": 527.0, + "y": 220.0, + "width": 96.0, + "height": 40.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a6V", + "roundness": null, + "seed": 1001001014, + "version": 1, + "versionNonce": 2002002015, + "isDeleted": false, + "boundElements": null, + "updated": 1706659200000, + "link": null, + "locked": false, + "text": "Data Store\n(Database)", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "data-store-1", + "originalText": "Data Store\n(Database)", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "data-store-line", + "type": "line", + "x": 500, + "y": 225, + "width": 150, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a7", + "roundness": null, + "seed": 1001001008, + "version": 1, + "versionNonce": 2002002009, + "isDeleted": false, + "boundElements": null, + "updated": 1706659200000, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 150, + 0 + ] + ], + "startBinding": null, + "endBinding": null + } + ], + "appState": { + "viewBackgroundColor": "#ffffff", + "gridSize": 20 + }, + "files": {} +} diff --git a/.claude/skills/excalidraw-diagram/templates/flowchart-template.excalidraw b/.claude/skills/excalidraw-diagram/templates/flowchart-template.excalidraw new file mode 100644 index 0000000..f1c67e2 --- /dev/null +++ b/.claude/skills/excalidraw-diagram/templates/flowchart-template.excalidraw @@ -0,0 +1,312 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "step1", + "type": "rectangle", + "x": 400, + "y": 200, + "width": 200, + "height": 80, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a0", + "roundness": { + "type": 3 + }, + "seed": 1234567890, + "version": 1, + "versionNonce": 987654321, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "step1-label" + } + ], + "updated": 1706659200000, + "link": null, + "locked": false + }, + { + "id": "step1-label", + "type": "text", + "x": 464.0, + "y": 227.5, + "width": 72.0, + "height": 25.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a0V", + "roundness": null, + "seed": 1234567897, + "version": 1, + "versionNonce": 987654328, + "isDeleted": false, + "boundElements": null, + "updated": 1706659200000, + "link": null, + "locked": false, + "text": "Step 1", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "step1", + "originalText": "Step 1", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "arrow1", + "type": "arrow", + "x": 500, + "y": 280, + "width": 0, + "height": 100, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a1", + "roundness": { + "type": 2 + }, + "seed": 1234567891, + "version": 1, + "versionNonce": 987654322, + "isDeleted": false, + "boundElements": null, + "updated": 1706659200000, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 100 + ] + ], + "startBinding": null, + "endBinding": null + }, + { + "id": "step2", + "type": "rectangle", + "x": 400, + "y": 380, + "width": 200, + "height": 80, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a2", + "roundness": { + "type": 3 + }, + "seed": 1234567892, + "version": 1, + "versionNonce": 987654323, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "step2-label" + } + ], + "updated": 1706659200000, + "link": null, + "locked": false + }, + { + "id": "step2-label", + "type": "text", + "x": 464.0, + "y": 407.5, + "width": 72.0, + "height": 25.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a2V", + "roundness": null, + "seed": 1234567899, + "version": 1, + "versionNonce": 987654330, + "isDeleted": false, + "boundElements": null, + "updated": 1706659200000, + "link": null, + "locked": false, + "text": "Step 2", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "step2", + "originalText": "Step 2", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "arrow2", + "type": "arrow", + "x": 500, + "y": 460, + "width": 0, + "height": 100, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a3", + "roundness": { + "type": 2 + }, + "seed": 1234567893, + "version": 1, + "versionNonce": 987654324, + "isDeleted": false, + "boundElements": null, + "updated": 1706659200000, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 100 + ] + ], + "startBinding": null, + "endBinding": null + }, + { + "id": "step3", + "type": "rectangle", + "x": 400, + "y": 560, + "width": 200, + "height": 80, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#b2f2bb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a4", + "roundness": { + "type": 3 + }, + "seed": 1234567894, + "version": 1, + "versionNonce": 987654325, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "step3-label" + } + ], + "updated": 1706659200000, + "link": null, + "locked": false + }, + { + "id": "step3-label", + "type": "text", + "x": 464.0, + "y": 587.5, + "width": 72.0, + "height": 25.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a4V", + "roundness": null, + "seed": 1234567901, + "version": 1, + "versionNonce": 987654332, + "isDeleted": false, + "boundElements": null, + "updated": 1706659200000, + "link": null, + "locked": false, + "text": "Step 3", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "step3", + "originalText": "Step 3", + "autoResize": true, + "lineHeight": 1.25 + } + ], + "appState": { + "viewBackgroundColor": "#ffffff", + "gridSize": 20 + }, + "files": {} +} diff --git a/.claude/skills/excalidraw-diagram/templates/mindmap-template.excalidraw b/.claude/skills/excalidraw-diagram/templates/mindmap-template.excalidraw new file mode 100644 index 0000000..2a16b64 --- /dev/null +++ b/.claude/skills/excalidraw-diagram/templates/mindmap-template.excalidraw @@ -0,0 +1,352 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor", + "elements": [ + { + "id": "center", + "type": "rectangle", + "x": 500, + "y": 350, + "width": 200, + "height": 100, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffd43b", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a0", + "roundness": { + "type": 3 + }, + "seed": 3333333333, + "version": 3, + "versionNonce": 641024845, + "isDeleted": false, + "boundElements": [ + { + "id": "arrow1", + "type": "arrow" + }, + { + "id": "arrow2", + "type": "arrow" + }, + { + "type": "text", + "id": "center-label" + } + ], + "updated": 1769755916717, + "link": null, + "locked": false + }, + { + "id": "center-label", + "type": "text", + "x": 522.0, + "y": 387.5, + "width": 156.0, + "height": 25.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a0V", + "roundness": null, + "seed": 3333333340, + "version": 1, + "versionNonce": 641024852, + "isDeleted": false, + "boundElements": null, + "updated": 1769755916717, + "link": null, + "locked": false, + "text": "Central Topic", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "center", + "originalText": "Central Topic", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "branch1", + "type": "rectangle", + "x": 250, + "y": 150, + "width": 150, + "height": 80, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#96f2d7", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a1", + "roundness": { + "type": 3 + }, + "seed": 3333333334, + "version": 2, + "versionNonce": 2040232045, + "isDeleted": false, + "boundElements": [ + { + "id": "arrow1", + "type": "arrow" + }, + { + "type": "text", + "id": "branch1-label" + } + ], + "updated": 1769755912840, + "link": null, + "locked": false + }, + { + "id": "branch1-label", + "type": "text", + "x": 281.8, + "y": 178.75, + "width": 86.39999999999999, + "height": 22.5, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a1V", + "roundness": null, + "seed": 3333333341, + "version": 1, + "versionNonce": 2040232052, + "isDeleted": false, + "boundElements": null, + "updated": 1769755912840, + "link": null, + "locked": false, + "text": "Branch 1", + "fontSize": 18, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "branch1", + "originalText": "Branch 1", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "arrow1", + "type": "arrow", + "x": 600, + "y": 350, + "width": 246.39999389648438, + "height": 111.20001220703125, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a2", + "roundness": { + "type": 2 + }, + "seed": 3333333335, + "version": 23, + "versionNonce": 308894189, + "isDeleted": false, + "boundElements": [], + "updated": 1769755914127, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -246.39999389648438, + -111.20001220703125 + ] + ], + "startBinding": { + "elementId": "center", + "focus": 0.5255972360761778, + "gap": 1 + }, + "endBinding": { + "elementId": "branch1", + "focus": 0.48604063201707415, + "gap": 8.79998779296875 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "branch2", + "type": "rectangle", + "x": 750, + "y": 150, + "width": 150, + "height": 80, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#96f2d7", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a3", + "roundness": { + "type": 3 + }, + "seed": 3333333336, + "version": 2, + "versionNonce": 1459929741, + "isDeleted": false, + "boundElements": [ + { + "id": "arrow2", + "type": "arrow" + }, + { + "type": "text", + "id": "branch2-label" + } + ], + "updated": 1769755916716, + "link": null, + "locked": false + }, + { + "id": "branch2-label", + "type": "text", + "x": 781.8, + "y": 178.75, + "width": 86.39999999999999, + "height": 22.5, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a3V", + "roundness": null, + "seed": 3333333343, + "version": 1, + "versionNonce": 1459929748, + "isDeleted": false, + "boundElements": null, + "updated": 1769755916716, + "link": null, + "locked": false, + "text": "Branch 2", + "fontSize": 18, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "branch2", + "originalText": "Branch 2", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "arrow2", + "type": "arrow", + "x": 600, + "y": 350, + "width": 216, + "height": 112.80001831054688, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a4", + "roundness": { + "type": 2 + }, + "seed": 3333333337, + "version": 41, + "versionNonce": 1447859213, + "isDeleted": false, + "boundElements": [], + "updated": 1769756030188, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 216, + -112.80001831054688 + ] + ], + "startBinding": { + "elementId": "center", + "focus": -0.48913039421990545, + "gap": 1 + }, + "endBinding": { + "elementId": "branch2", + "focus": -0.5368418212214556, + "gap": 7.199981689453125 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow" + } + ], + "appState": { + "gridSize": 20, + "gridStep": 5, + "gridModeEnabled": false, + "viewBackgroundColor": "#ffffff" + }, + "files": {} +} diff --git a/.claude/skills/excalidraw-diagram/templates/relationship-template.excalidraw b/.claude/skills/excalidraw-diagram/templates/relationship-template.excalidraw new file mode 100644 index 0000000..8ae4122 --- /dev/null +++ b/.claude/skills/excalidraw-diagram/templates/relationship-template.excalidraw @@ -0,0 +1,231 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "entity1", + "type": "rectangle", + "x": 300, + "y": 300, + "width": 180, + "height": 100, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a0", + "roundness": { + "type": 3 + }, + "seed": 1111111111, + "version": 1, + "versionNonce": 2222222222, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "entity1-label" + } + ], + "updated": 1706659200000, + "link": null, + "locked": false + }, + { + "id": "entity1-label", + "type": "text", + "x": 342.0, + "y": 337.5, + "width": 96.0, + "height": 25.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a0V", + "roundness": null, + "seed": 1111111118, + "version": 1, + "versionNonce": 2222222229, + "isDeleted": false, + "boundElements": null, + "updated": 1706659200000, + "link": null, + "locked": false, + "text": "Entity A", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "entity1", + "originalText": "Entity A", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "entity2", + "type": "rectangle", + "x": 600, + "y": 300, + "width": 180, + "height": 100, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a1", + "roundness": { + "type": 3 + }, + "seed": 1111111112, + "version": 1, + "versionNonce": 2222222223, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "entity2-label" + } + ], + "updated": 1706659200000, + "link": null, + "locked": false + }, + { + "id": "entity2-label", + "type": "text", + "x": 642.0, + "y": 337.5, + "width": 96.0, + "height": 25.0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a1V", + "roundness": null, + "seed": 1111111119, + "version": 1, + "versionNonce": 2222222230, + "isDeleted": false, + "boundElements": null, + "updated": 1706659200000, + "link": null, + "locked": false, + "text": "Entity B", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "entity2", + "originalText": "Entity B", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "relationship", + "type": "arrow", + "x": 480, + "y": 350, + "width": 120, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a2", + "roundness": { + "type": 2 + }, + "seed": 1111111113, + "version": 1, + "versionNonce": 2222222224, + "isDeleted": false, + "boundElements": null, + "updated": 1706659200000, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 120, + 0 + ] + ], + "startBinding": null, + "endBinding": null + }, + { + "id": "label", + "type": "text", + "x": 510, + "y": 325, + "width": 60, + "height": 24, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a3", + "roundness": null, + "seed": 1111111114, + "version": 1, + "versionNonce": 2222222225, + "isDeleted": false, + "boundElements": null, + "updated": 1706659200000, + "link": null, + "locked": false, + "text": "relates to", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top" + } + ], + "appState": { + "viewBackgroundColor": "#ffffff", + "gridSize": 20 + }, + "files": {} +} diff --git a/.claude/skills/excalidraw-diagram/templates/sequence-diagram-template.excalidraw b/.claude/skills/excalidraw-diagram/templates/sequence-diagram-template.excalidraw new file mode 100644 index 0000000..9ca4b3b --- /dev/null +++ b/.claude/skills/excalidraw-diagram/templates/sequence-diagram-template.excalidraw @@ -0,0 +1,670 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "object-1", + "type": "rectangle", + "x": 150, + "y": 100, + "width": 120, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e7f5ff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a0", + "roundness": null, + "seed": 4001001001, + "version": 1, + "versionNonce": 5002002001, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "object-1-label" + } + ], + "updated": 1706659200000, + "link": null, + "locked": false + }, + { + "id": "object-1-label", + "type": "text", + "x": 177.6, + "y": 113.75, + "width": 64.8, + "height": 22.5, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a0V", + "roundness": null, + "seed": 4001001008, + "version": 1, + "versionNonce": 5002002008, + "isDeleted": false, + "boundElements": null, + "updated": 1706659200000, + "link": null, + "locked": false, + "text": "Client", + "fontSize": 18, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "object-1", + "originalText": "Client", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "lifeline-1", + "type": "line", + "x": 210, + "y": 150, + "width": 0, + "height": 300, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a1", + "roundness": null, + "seed": 4001001002, + "version": 1, + "versionNonce": 5002002002, + "isDeleted": false, + "boundElements": null, + "updated": 1706659200000, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 300 + ] + ], + "startBinding": null, + "endBinding": null + }, + { + "id": "object-2", + "type": "rectangle", + "x": 350, + "y": 100, + "width": 120, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#fff3bf", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a2", + "roundness": null, + "seed": 4001001003, + "version": 1, + "versionNonce": 5002002003, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "object-2-label" + } + ], + "updated": 1706659200000, + "link": null, + "locked": false + }, + { + "id": "object-2-label", + "type": "text", + "x": 377.6, + "y": 113.75, + "width": 64.8, + "height": 22.5, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a2V", + "roundness": null, + "seed": 4001001010, + "version": 1, + "versionNonce": 5002002010, + "isDeleted": false, + "boundElements": null, + "updated": 1706659200000, + "link": null, + "locked": false, + "text": "Server", + "fontSize": 18, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "object-2", + "originalText": "Server", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "lifeline-2", + "type": "line", + "x": 410, + "y": 150, + "width": 0, + "height": 300, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a3", + "roundness": null, + "seed": 4001001004, + "version": 1, + "versionNonce": 5002002004, + "isDeleted": false, + "boundElements": null, + "updated": 1706659200000, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 300 + ] + ], + "startBinding": null, + "endBinding": null + }, + { + "id": "object-3", + "type": "rectangle", + "x": 550, + "y": 100, + "width": 120, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#d0f0c0", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a4", + "roundness": null, + "seed": 4001001005, + "version": 1, + "versionNonce": 5002002005, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "object-3-label" + } + ], + "updated": 1706659200000, + "link": null, + "locked": false + }, + { + "id": "object-3-label", + "type": "text", + "x": 566.8, + "y": 113.75, + "width": 86.39999999999999, + "height": 22.5, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a4V", + "roundness": null, + "seed": 4001001012, + "version": 1, + "versionNonce": 5002002012, + "isDeleted": false, + "boundElements": null, + "updated": 1706659200000, + "link": null, + "locked": false, + "text": "Database", + "fontSize": 18, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "object-3", + "originalText": "Database", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "lifeline-3", + "type": "line", + "x": 610, + "y": 150, + "width": 0, + "height": 300, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a5", + "roundness": null, + "seed": 4001001006, + "version": 1, + "versionNonce": 5002002006, + "isDeleted": false, + "boundElements": null, + "updated": 1706659200000, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 300 + ] + ], + "startBinding": null, + "endBinding": null + }, + { + "id": "message-1", + "type": "arrow", + "x": 210, + "y": 200, + "width": 200, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a6", + "roundness": { + "type": 2 + }, + "seed": 4001001007, + "version": 1, + "versionNonce": 5002002007, + "isDeleted": false, + "boundElements": null, + "updated": 1706659200000, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 200, + 0 + ] + ], + "startBinding": null, + "endBinding": null + }, + { + "id": "message-label-1", + "type": "text", + "x": 250, + "y": 180, + "width": 120, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a7", + "roundness": null, + "seed": 4001001008, + "version": 1, + "versionNonce": 5002002008, + "isDeleted": false, + "boundElements": null, + "updated": 1706659200000, + "link": null, + "locked": false, + "text": "1: request()", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top" + }, + { + "id": "activation-1", + "type": "rectangle", + "x": 405, + "y": 200, + "width": 10, + "height": 80, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffd43b", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a8", + "roundness": null, + "seed": 4001001009, + "version": 1, + "versionNonce": 5002002009, + "isDeleted": false, + "boundElements": null, + "updated": 1706659200000, + "link": null, + "locked": false + }, + { + "id": "message-2", + "type": "arrow", + "x": 415, + "y": 230, + "width": 195, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a9", + "roundness": { + "type": 2 + }, + "seed": 4001001010, + "version": 1, + "versionNonce": 5002002010, + "isDeleted": false, + "boundElements": null, + "updated": 1706659200000, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 195, + 0 + ] + ], + "startBinding": null, + "endBinding": null + }, + { + "id": "message-label-2", + "type": "text", + "x": 450, + "y": 210, + "width": 120, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a10", + "roundness": null, + "seed": 4001001011, + "version": 1, + "versionNonce": 5002002011, + "isDeleted": false, + "boundElements": null, + "updated": 1706659200000, + "link": null, + "locked": false, + "text": "2: query()", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top" + }, + { + "id": "return-message-1", + "type": "arrow", + "x": 610, + "y": 250, + "width": 195, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a11", + "roundness": { + "type": 2 + }, + "seed": 4001001012, + "version": 1, + "versionNonce": 5002002012, + "isDeleted": false, + "boundElements": null, + "updated": 1706659200000, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -195, + 0 + ] + ], + "startBinding": null, + "endBinding": null + }, + { + "id": "return-label-1", + "type": "text", + "x": 450, + "y": 255, + "width": 120, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a12", + "roundness": null, + "seed": 4001001013, + "version": 1, + "versionNonce": 5002002013, + "isDeleted": false, + "boundElements": null, + "updated": 1706659200000, + "link": null, + "locked": false, + "text": "3: result", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top" + }, + { + "id": "return-message-2", + "type": "arrow", + "x": 410, + "y": 280, + "width": 200, + "height": 0, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a13", + "roundness": { + "type": 2 + }, + "seed": 4001001014, + "version": 1, + "versionNonce": 5002002014, + "isDeleted": false, + "boundElements": null, + "updated": 1706659200000, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -200, + 0 + ] + ], + "startBinding": null, + "endBinding": null + }, + { + "id": "return-label-2", + "type": "text", + "x": 250, + "y": 285, + "width": 120, + "height": 20, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a14", + "roundness": null, + "seed": 4001001015, + "version": 1, + "versionNonce": 5002002015, + "isDeleted": false, + "boundElements": null, + "updated": 1706659200000, + "link": null, + "locked": false, + "text": "4: response", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top" + } + ], + "appState": { + "viewBackgroundColor": "#ffffff", + "gridSize": 20 + }, + "files": {} +} diff --git a/CONTRIBUTING/CONTRIBUTING_en.md b/CONTRIBUTING/CONTRIBUTING_en.md index df84fa9..e75c7d7 100644 --- a/CONTRIBUTING/CONTRIBUTING_en.md +++ b/CONTRIBUTING/CONTRIBUTING_en.md @@ -78,6 +78,27 @@ After modifying source files under `zh_chapters/` or `en_chapters/`, the browser For detailed custom syntax (math, figure labels, citations, code blocks) and figure guidelines, refer to the **[Writing Style Guide](style_en.md)**. +### Drawing Figures with AI Agents (Excalidraw Skill) + +The repository includes a project-level agent skill, +[`.claude/skills/excalidraw-diagram/`](../.claude/skills/excalidraw-diagram/SKILL.md). +`.agents/skills/excalidraw-diagram` is a symlink to the same directory. +Claude Code, and other agents that support the Agent Skills format, load it +automatically when you ask them to draw a figure for the book. + +The skill tells the agent to: + +- keep the editable `.excalidraw` source next to the exported SVG/PNG in `v1/img/ch/` +- make separate `-zh` variants for figures that contain text +- follow a palette that stays legible in dark mode +- never reference PDF images + +Lint figures and chapter references before you commit: + +```bash +python3 .claude/skills/excalidraw-diagram/scripts/check_figures.py v1/img/ch06 v2/en_chapters/06_chapter_training_systems/index.md +``` + --- ## Submission Workflow diff --git a/CONTRIBUTING/CONTRIBUTING_zh.md b/CONTRIBUTING/CONTRIBUTING_zh.md index acee9de..306371f 100644 --- a/CONTRIBUTING/CONTRIBUTING_zh.md +++ b/CONTRIBUTING/CONTRIBUTING_zh.md @@ -78,6 +78,26 @@ cd v2/config/en_config && mdbook serve --open 详细的自定义语法(公式、图片标签、文献引用、代码块等)及图片规范,请参阅 **[写作样式规范](style_zh.md)**。 +### 使用 AI Agent 绘图(Excalidraw Skill) + +仓库内置了项目级 Agent Skill +[`.claude/skills/excalidraw-diagram/`](../.claude/skills/excalidraw-diagram/SKILL.md)。 +`.agents/skills/excalidraw-diagram` 是指向同一目录的符号链接。 +让 Claude Code 或其他支持 Agent Skills 格式的 Agent 为本书绘图时,它们会自动加载该 Skill。 + +该 Skill 要求 Agent: + +- 将可编辑的 `.excalidraw` 源文件与导出的 SVG/PNG 一起放在 `v1/img/ch/` +- 为含文字的图另做 `-zh` 中文版本 +- 使用在暗色模式下同样清晰的配色 +- 不引用 PDF 图片 + +提交前可运行以下命令检查图片源文件与章节中的引用: + +```bash +python3 .claude/skills/excalidraw-diagram/scripts/check_figures.py v1/img/ch06 v2/zh_chapters/06_chapter_training_systems/index.md +``` + --- ## 提交流程