From ffc66f40a5ae9931d6d5060174244bd370113442 Mon Sep 17 00:00:00 2001 From: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Date: Sat, 3 Oct 2026 11:09:22 -0400 Subject: [PATCH 1/2] feat(distribution): Claude Code plugin, rules bridge, Pages trim, count and cap gates - #288: remove the personal context-mode block from CLAUDE.md; CI fails if it returns - #289: add .claude-plugin/plugin.json + marketplace.json (CI-validated, version synced by release.yml), generate claude/blender-rules.md from rules/*.mdc (CI drift check), rewrite README and site.json install steps, drop placeholder author email - #295: fix stale counts, document the real 5-to-75 snippet cap and enforce it; CI scans AGENTS.md and CONTRIBUTING.md for stale aggregate counts - #307: pin Jinja2/MarkupSafe exactly, pip cache, dependabot pip entry, warn when the GitHub description drifts from the README counts - #212: Pages artifact leaves out contact/asset sheets and internal docs Closes #288, closes #289, closes #295, closes #307, closes #212 Co-Authored-By: Claude Sonnet 5.5 --- .claude-plugin/marketplace.json | 15 ++++++ .claude-plugin/plugin.json | 20 ++++++++ .cursor-plugin/plugin.json | 9 ++-- .github/dependabot.yml | 5 ++ .github/workflows/pages.yml | 13 ++++- .github/workflows/release.yml | 11 ++++ .github/workflows/validate.yml | 90 +++++++++++++++++++++++++++++++++ CLAUDE.md | 65 +----------------------- CONTRIBUTING.md | 6 +-- README.md | 17 +++++-- ROADMAP.md | 2 +- claude/blender-rules.md | 60 ++++++++++++++++++++++ scripts/build_claude_rules.py | 75 +++++++++++++++++++++++++++ scripts/site/requirements.txt | 3 +- site.json | 4 +- 15 files changed, 315 insertions(+), 80 deletions(-) create mode 100644 .claude-plugin/marketplace.json create mode 100644 .claude-plugin/plugin.json create mode 100644 claude/blender-rules.md create mode 100644 scripts/build_claude_rules.py diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 00000000..acd53685 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,15 @@ +{ + "name": "blender-developer-tools", + "owner": { + "name": "TMHSDigital", + "url": "https://github.com/TMHSDigital" + }, + "plugins": [ + { + "name": "blender-developer-tools", + "source": "./", + "description": "Cursor and Claude Code skills, rules, snippets, and templates for Blender Python add-on and scripting development", + "version": "0.131.1" + } + ] +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 00000000..0f8f5081 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,20 @@ +{ + "name": "blender-developer-tools", + "description": "Cursor and Claude Code skills, rules, snippets, and templates for Blender Python add-on and scripting development", + "version": "0.131.1", + "author": { + "name": "TMHSDigital", + "url": "https://github.com/TMHSDigital" + }, + "homepage": "https://github.com/TMHSDigital/Blender-Developer-Tools", + "repository": "https://github.com/TMHSDigital/Blender-Developer-Tools", + "license": "CC-BY-NC-ND-4.0", + "keywords": [ + "claude-code", + "blender", + "bpy", + "blender-addon", + "blender-python", + "developer-tools" + ] +} diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 9f16b6ff..019cd349 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -4,14 +4,17 @@ "description": "Cursor and Claude Code skills, rules, snippets, and templates for Blender Python add-on and scripting development", "version": "0.131.1", "author": { - "name": "TMHSDigital", - "email": "contact@users.noreply.github.com" + "name": "TMHSDigital" }, "license": "CC-BY-NC-ND-4.0", "keywords": [ "cursor-plugin", + "claude-code", "developer-tools", - "blender" + "blender", + "bpy", + "blender-addon", + "blender-python" ], "skills": [ "skills/addon-scaffolding/SKILL.md", diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 55823de0..5409f003 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,3 +5,8 @@ updates: schedule: interval: "daily" target-branch: "main" + - package-ecosystem: "pip" + directory: "/scripts/site" + schedule: + interval: "weekly" + target-branch: "main" diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 6967d48a..8ad8d4da 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -76,6 +76,8 @@ jobs: - uses: actions/setup-python@v7 with: python-version: "3.12" + cache: pip + cache-dependency-path: scripts/site/requirements.txt - run: pip install -r scripts/site/requirements.txt @@ -92,9 +94,18 @@ jobs: - uses: actions/configure-pages@v6 + - name: Stage the public site (leave out internal docs and unlinked sheets) + # Everything stays in the repo; only the Pages artifact shrinks. No + # published page links to any of these (tests/check_site_links.py). + run: | + mkdir -p _site + cp -r docs/. _site/ + rm -rf _site/gallery/contact-sheets _site/gallery/asset-sheets _site/gallery/DESIGN_NOTES.md + rm -f _site/*.md + - uses: actions/upload-pages-artifact@v5 with: - path: docs + path: _site - uses: actions/deploy-pages@v5 id: deployment diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b2e46c66..fb300863 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -142,6 +142,17 @@ jobs: flags=re.M, ) open(path, 'w').write(text) + + # Claude Code manifests: same version, rewritten via JSON. + import json + for path in ('.claude-plugin/plugin.json', '.claude-plugin/marketplace.json'): + data = json.load(open(path, encoding='utf-8')) + if 'version' in data: + data['version'] = os.environ['NEW_VERSION'] + for entry in data.get('plugins', []): + entry['version'] = os.environ['NEW_VERSION'] + with open(path, 'w', encoding='utf-8', newline='\n') as fh: + fh.write(json.dumps(data, indent=2) + '\n') PYEOF - name: Commit version bump diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 2691d058..cf0d5a33 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -197,6 +197,8 @@ jobs: - uses: actions/setup-python@v7 with: python-version: "3.12" + cache: pip + cache-dependency-path: scripts/site/requirements.txt - run: pip install -r scripts/site/requirements.txt @@ -275,6 +277,62 @@ jobs: print(f'Manifest verified at v{version}: {counts}') PYEOF + validate-claude-packaging: + name: Validate Claude Code packaging + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Check .claude-plugin manifests match VERSION and the skills on disk + run: | + python3 << 'PYEOF' + import glob + import json + import os + import sys + + errors = [] + version = open('VERSION').read().strip() + plugin = json.load(open('.claude-plugin/plugin.json', encoding='utf-8')) + market = json.load(open('.claude-plugin/marketplace.json', encoding='utf-8')) + + if plugin.get('version') != version: + errors.append(f"claude plugin.json version {plugin.get('version')!r} != VERSION {version!r}") + if plugin.get('name') != 'blender-developer-tools': + errors.append('claude plugin.json name must be blender-developer-tools') + entries = market.get('plugins', []) + if len(entries) != 1 or entries[0].get('name') != plugin.get('name'): + errors.append('marketplace.json must list exactly the one plugin from plugin.json') + elif entries[0].get('version') != version: + errors.append(f"marketplace.json plugin version {entries[0].get('version')!r} != VERSION {version!r}") + elif not os.path.isdir(os.path.join(entries[0].get('source', ''), 'skills')): + errors.append('marketplace.json source must point at a directory containing skills/') + + # Claude Code discovers skills//SKILL.md; each needs name + description. + for path in sorted(glob.glob('skills/*/SKILL.md')): + parts = open(path, encoding='utf-8').read().replace('\r\n', '\n').split('---') + front = parts[1] if len(parts) > 2 else '' + for key in ('name:', 'description:'): + if key not in front: + errors.append(f'{path}: frontmatter missing {key}') + + if errors: + for e in errors: + print(f'::error::{e}', file=sys.stderr) + sys.exit(1) + print('Claude Code packaging verified') + PYEOF + + - name: Check CLAUDE.md carries no personal plugin routing block + run: | + if grep -nE 'context-mode|MANDATORY routing rules|ctx_(execute|batch_execute|search|fetch_and_index)' CLAUDE.md; then + echo "::error::CLAUDE.md contains a personal plugin routing block (see #288)" + exit 1 + fi + + - name: Check claude/blender-rules.md is regenerated from rules/*.mdc + run: python3 scripts/build_claude_rules.py --check + validate-counts: name: Validate content counts runs-on: ubuntu-latest @@ -282,6 +340,8 @@ jobs: - uses: actions/checkout@v7 - name: Check content counts match README + env: + GH_TOKEN: ${{ github.token }} run: | python3 << 'PYEOF' import os @@ -338,6 +398,36 @@ jobs: f'README showcase count mismatch (expected "{showcase_needle}" substring)' ) + # Documented snippet length cap (README, CLAUDE.md, CONTRIBUTING.md). + for f in sorted(os.listdir('snippets')): + if f.endswith('.py'): + n = len(open(os.path.join('snippets', f), encoding='utf-8').read().splitlines()) + if not 5 <= n <= 75: + errors.append(f'snippets/{f} is {n} lines (documented range is 5 to 75)') + + # Secondary docs must not carry stale aggregate counts. + import re + for doc in ('AGENTS.md', 'CONTRIBUTING.md'): + text = open(doc, encoding='utf-8').read() + for kind, actual in (('examples', example_count), ('showcase pieces', showcase_count)): + for m in re.finditer(r'(\d+) ' + kind, text): + if int(m.group(1)) != actual: + errors.append(f'{doc} says "{m.group(0)}" but the repo has {actual}') + + # The GitHub About text drifts silently otherwise. Warn only: it is + # repo metadata edited by hand and must not block content PRs. + import subprocess + try: + about = subprocess.run( + ['gh', 'repo', 'view', '--json', 'description', '--jq', '.description'], + capture_output=True, text=True, timeout=60, check=True, + ).stdout + for needle in (f'{example_count} examples', f'{showcase_count} showcase'): + if needle not in about: + print(f'::warning::GitHub repo description is stale (missing "{needle}"): {about.strip()}') + except (subprocess.SubprocessError, OSError) as exc: + print(f'::warning::could not read the GitHub repo description: {exc}') + if errors: for e in errors: print(f'::error::{e}', file=sys.stderr) diff --git a/CLAUDE.md b/CLAUDE.md index 555eeb2b..56c9d6ad 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -97,7 +97,7 @@ matching its `globs` does. Changing a glob changes when the rule fires. ## Snippets (27) -Small standalone `.py` files at `snippets/.py`, each 5 to 50 lines. +Small standalone `.py` files at `snippets/.py`, each 5 to 75 lines. v0.1.0: canonical object creation and deletion, depsgraph evaluated mesh, bmesh load-edit-free, temp_override context, foreach_set vertex bulk write, register_classes_factory, PointerProperty binding, cross-version property delete, and the `action_ensure_channelbag_for_slot` slotted-actions bridge. @@ -239,66 +239,3 @@ When adding content to a future version: 3. Update ROADMAP.md candidate pool entries. 4. Use `feat:` for new content, `fix:` for corrections. 5. Push. The release pipeline handles VERSION, tags, CHANGELOG, the `**Version:**` line here, and the `**Current:**` line in ROADMAP.md. - -# context-mode — MANDATORY routing rules - -You have context-mode MCP tools available. These rules are NOT optional — they protect your context window from flooding. A single unrouted command can dump 56 KB into context and waste the entire session. - -## BLOCKED commands — do NOT attempt these - -### curl / wget — BLOCKED -Any Bash command containing `curl` or `wget` is intercepted and replaced with an error message. Do NOT retry. -Instead use: -- `ctx_fetch_and_index(url, source)` to fetch and index web pages -- `ctx_execute(language: "javascript", code: "const r = await fetch(...)")` to run HTTP calls in sandbox - -### Inline HTTP — BLOCKED -Any Bash command containing `fetch('http`, `requests.get(`, `requests.post(`, `http.get(`, or `http.request(` is intercepted and replaced with an error message. Do NOT retry with Bash. -Instead use: -- `ctx_execute(language, code)` to run HTTP calls in sandbox — only stdout enters context - -### WebFetch — BLOCKED -WebFetch calls are denied entirely. The URL is extracted and you are told to use `ctx_fetch_and_index` instead. -Instead use: -- `ctx_fetch_and_index(url, source)` then `ctx_search(queries)` to query the indexed content - -## REDIRECTED tools — use sandbox equivalents - -### Bash (>20 lines output) -Bash is ONLY for: `git`, `mkdir`, `rm`, `mv`, `cd`, `ls`, `npm install`, `pip install`, and other short-output commands. -For everything else, use: -- `ctx_batch_execute(commands, queries)` — run multiple commands + search in ONE call -- `ctx_execute(language: "shell", code: "...")` — run in sandbox, only stdout enters context - -### Read (for analysis) -If you are reading a file to **Edit** it → Read is correct (Edit needs content in context). -If you are reading to **analyze, explore, or summarize** → use `ctx_execute_file(path, language, code)` instead. Only your printed summary enters context. The raw file content stays in the sandbox. - -### Grep (large results) -Grep results can flood context. Use `ctx_execute(language: "shell", code: "grep ...")` to run searches in sandbox. Only your printed summary enters context. - -## Tool selection hierarchy - -1. **GATHER**: `ctx_batch_execute(commands, queries)` — Primary tool. Runs all commands, auto-indexes output, returns search results. ONE call replaces 30+ individual calls. -2. **FOLLOW-UP**: `ctx_search(queries: ["q1", "q2", ...])` — Query indexed content. Pass ALL questions as array in ONE call. -3. **PROCESSING**: `ctx_execute(language, code)` | `ctx_execute_file(path, language, code)` — Sandbox execution. Only stdout enters context. -4. **WEB**: `ctx_fetch_and_index(url, source)` then `ctx_search(queries)` — Fetch, chunk, index, query. Raw HTML never enters context. -5. **INDEX**: `ctx_index(content, source)` — Store content in FTS5 knowledge base for later search. - -## Subagent routing - -When spawning subagents (Agent/Task tool), the routing block is automatically injected into their prompt. Bash-type subagents are upgraded to general-purpose so they have access to MCP tools. You do NOT need to manually instruct subagents about context-mode. - -## Output constraints - -- Keep responses under 500 words. -- Write artifacts (code, configs, PRDs) to FILES — never return them as inline text. Return only: file path + 1-line description. -- When indexing content, use descriptive source labels so others can `ctx_search(source: "label")` later. - -## ctx commands - -| Command | Action | -|---------|--------| -| `ctx stats` | Call the `ctx_stats` MCP tool and display the full output verbatim | -| `ctx doctor` | Call the `ctx_doctor` MCP tool, run the returned shell command, display as checklist | -| `ctx upgrade` | Call the `ctx_upgrade` MCP tool, run the returned shell command, display as checklist | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5c79bdba..eccbe0a1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,7 +48,7 @@ showcase/ - **`skills/`** - one directory per skill, each containing `SKILL.md` with YAML frontmatter (`name`, `description`, `standards-version`). - **`rules/`** - Cursor-style rules as `.mdc` files with YAML frontmatter (`description`, `alwaysApply`, `globs`, `standards-version`). -- **`snippets/`** - small standalone `.py` files (5 to 50 lines) demonstrating a single canonical pattern. +- **`snippets/`** - small standalone `.py` files (5 to 75 lines) demonstrating a single canonical pattern. - **`templates/`** - copy-paste starting points; one directory per template. - **`showcase/`** - budget-conformance props, sibling of `examples/`. Not API contracts. Conventions: [`showcase/README.md`](showcase/README.md). @@ -97,7 +97,7 @@ showcase/ ## Adding a Snippet 1. Add a `.py` file under `snippets/`, e.g. `snippets/depsgraph-evaluated-mesh.py`. -2. Keep it 5 to 50 lines, fully working code, with a header comment naming the snippet and citing the relevant Blender doc URL or research section. +2. Keep it 5 to 75 lines, fully working code, with a header comment naming the snippet and citing the relevant Blender doc URL or research section. 3. Snippets are validated for Python syntax in CI. ## Adding a Template @@ -307,7 +307,7 @@ The drift-check workflow enforces these on every push and PR. ## Aggregate Counts -`README.md` declares aggregate counts (e.g. "16 skills, 9 rules, 3 templates, 27 snippets, 59 examples, and 26 showcase pieces"). The `validate-counts` job in `.github/workflows/validate.yml` enforces these substrings against the filesystem on every push and PR. Showcase pieces are counted separately from examples. When you add or remove content, update the README counts in the same commit. +`README.md` declares aggregate counts (e.g. "16 skills, 9 rules, 3 templates, 27 snippets, 64 examples, and 76 showcase pieces"). The `validate-counts` job in `.github/workflows/validate.yml` enforces these substrings against the filesystem on every push and PR. Showcase pieces are counted separately from examples. When you add or remove content, update the README counts in the same commit. ## Pull Request Process diff --git a/README.md b/README.md index e383fcdf..8b2a20b4 100644 --- a/README.md +++ b/README.md @@ -56,8 +56,15 @@ The content is consumed by AI coding agents reading these files directly from a git clone https://github.com/TMHSDigital/Blender-Developer-Tools.git ``` -- **Cursor** — point Cursor at the checkout (or symlink `rules/` into your project). The `.mdc` rules apply automatically by glob scope; skills are referenced by name in chat. -- **Claude Code** — copy `skills/` and `rules/` into your project workspace, or keep this repo as a checkout that Claude Code references directly. +- **Cursor** — open your project with this checkout available and copy (or symlink) `rules/*.mdc` into your project's `.cursor/rules/`. The rules apply automatically by glob scope; skills are referenced by name in chat. +- **Claude Code** — install as a plugin, then run `/skills` to see all 16 skills: + + ```text + /plugin marketplace add TMHSDigital/Blender-Developer-Tools + /plugin install blender-developer-tools@blender-developer-tools + ``` + + Claude Code does not read Cursor `.mdc` rules. For the rules, add one line to your project's `CLAUDE.md` that imports the generated summary from a checkout: `@/path/to/Blender-Developer-Tools/claude/blender-rules.md`. Without the plugin, copy `skills/*` into your project's `.claude/skills/` instead. - **Get Blender** — download **5.2 LTS** (primary target) or **4.5 LTS** (supported fallback) from [blender.org/download/lts](https://www.blender.org/download/lts/); current stable lives at [blender.org/download](https://www.blender.org/download/). The `blender` command below is that binary — on macOS it is inside the app bundle at `/Applications/Blender.app/Contents/MacOS/Blender`. - **Run an example** — every example is a self-checking headless script (exit non-zero on failure, no GPU needed for the check): @@ -1451,7 +1458,7 @@ the duplicates, then glTF ships 24 tris / 48 positions / 8 unique. skills//SKILL.md - 16 skill files, YAML frontmatter, one canonical pattern each rules/.mdc - 9 rule files, anti-pattern + correction templates// - 3 template directories (extension-addon-template, headless-batch-script-template, ai-asset-pipeline-template) -snippets/.py - 27 standalone Python snippets, 5 to 50 lines each +snippets/.py - 27 standalone Python snippets, 5 to 75 lines each ``` ## Using rules in Cursor @@ -1462,13 +1469,13 @@ The `.mdc` files in `rules/` apply automatically when Cursor opens a Blender Pyt - `always-free-bmesh`: flags `bmesh.new()` without paired `bm.free()` in `try`/`finally` - `target-extensions-platform-format`: flags add-ons missing `blender_manifest.toml` - `type-annotate-props-and-defend-context`: flags `bpy.props` assignment form and unguarded `context.active_object` -- `prefer-temp-override-over-context-copy`: flags `bpy.context.copy()` passed to operators (deprecated 4.x, removed 5.x) +- `prefer-temp-override-over-context-copy`: flags `bpy.context.copy()` passed to operators (deprecated 3.2, removed 4.0) - `use-foreach-set-for-bulk-data`: flags Python loops over `mesh.vertices` setting `co`, normals, or other per-element bulk data - `validate-imported-mesh-scale`: flags glTF/FBX import then mesh work with no `transform_apply` and no unit-scale check - `no-unapplied-modifiers-on-export`: flags export of objects with live modifiers when the export does not request evaluated geometry - `use-correct-axis-rna-per-exporter`: flags `export_scene.gltf` calls that pass FBX `axis_forward` / `axis_up`, and `export_scene.fbx` calls that pass glTF `export_yup` -Symlink or clone this repo, then point Cursor at it as a skills/rules source. +Cursor: copy `rules/*.mdc` into `.cursor/rules/`. Claude Code: install the plugin (see Install) and import `claude/blender-rules.md` from your `CLAUDE.md`. ## Using the templates diff --git a/ROADMAP.md b/ROADMAP.md index 7266c458..80acd0b3 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -266,7 +266,7 @@ that comparison is ever needed. ## Asset-quality survey worklist (measured 2026-07-24, `gallery_asset_quality` floors) Full-gallery floor survey after the gate landed (`feat/asset-quality-gate`). -26 of 42 examples pass all floors. The 14 below-floor entries, with +26 of the 42 examples that existed on 2026-07-24 passed all floors (historical; the gallery has grown since). The 14 below-floor entries, with measured numbers — every one is a **contract-vehicle or display subject** (testcard TV, VSE monitors, text bars, swatch/display rigs, single honest primitives), not a game asset meant for reuse, so none is a remodeling diff --git a/claude/blender-rules.md b/claude/blender-rules.md new file mode 100644 index 00000000..551f65f8 --- /dev/null +++ b/claude/blender-rules.md @@ -0,0 +1,60 @@ +# Blender Developer Tools rules (Claude Code) + + + +Apply these when writing or reviewing Blender Python. Each rule links to the +full version with Wrong/Right examples. + +## always-free-bmesh + +Flag bmesh.new() calls without a paired bm.free() in a try/finally block. BMesh allocates C-side memory that Python's garbage collector cannot reclaim; missing free() leaks and eventually crashes Blender. + +Applies to: `**/*.py`. Full rule: `rules/always-free-bmesh.mdc`. + +## no-unapplied-modifiers-on-export + +Flag an export call on objects that still carry unapplied modifiers when the export arguments do not request evaluated geometry. The engine then receives the authored cage, not the modifier result. + +Applies to: `**/*.py`. Full rule: `rules/no-unapplied-modifiers-on-export.mdc`. + +## prefer-data-over-ops-in-loops + +Flag bpy.ops.* calls inside iteration over many objects, meshes, or frames. Each bpy.ops call triggers a full depsgraph evaluation and UI redraw; loops slow down by orders of magnitude. Use bpy.data.* and bmesh instead. + +Applies to: `**/*.py`. Full rule: `rules/prefer-data-over-ops-in-loops.mdc`. + +## prefer-temp-override-over-context-copy + +Flag uses of `bpy.context.copy()` to override context for an operator call. Passing a context dict to an operator was deprecated in Blender 3.2 and removed in 4.0, so it already fails on the 4.5 LTS fallback. Use `bpy.context.temp_override(**overrides)` as a context manager instead. + +Applies to: `**/*.py`. Full rule: `rules/prefer-temp-override-over-context-copy.mdc`. + +## target-extensions-platform-format + +Flag new Blender add-ons that ship only a legacy bl_info dict without a blender_manifest.toml. New add-ons must use the Extensions Platform format. bl_info may appear alongside as a fallback for backward compatibility, but the manifest is the source of truth. + +Applies to: `**/__init__.py`, `**/blender_manifest.toml`. Full rule: `rules/target-extensions-platform-format.mdc`. + +## type-annotate-props-and-defend-context + +Flag two related anti-patterns. (1) bpy.props defined as class-level assignments instead of type annotations (deprecated since 2.8). (2) Code that touches bpy.context.active_object without guarding for None. + +Applies to: `**/*.py`. Full rule: `rules/type-annotate-props-and-defend-context.mdc`. + +## use-correct-axis-rna-per-exporter + +Flag export_scene.gltf calls that pass FBX axis_forward or axis_up, and export_scene.fbx calls that pass glTF export_yup. The two exporters do not share axis RNA. + +Applies to: `**/*.py`. Full rule: `rules/use-correct-axis-rna-per-exporter.mdc`. + +## use-foreach-set-for-bulk-data + +Flag Python loops that set vertex coordinates, normals, UVs, or other bulk per-element data one element at a time. For meshes of more than a few thousand elements, this is 100x to 1000x slower than `mesh.vertices.foreach_set("co", flat_array)`, which writes through to C-level storage in one pass. + +Applies to: `**/*.py`. Full rule: `rules/use-foreach-set-for-bulk-data.mdc`. + +## validate-imported-mesh-scale + +Flag a glTF or FBX import followed by mesh operations with no transform_apply and no unit-scale check. Generated files often arrive with non-identity object scale or a non-meter scene scale; mesh edits then bake the wrong size. + +Applies to: `**/*.py`. Full rule: `rules/validate-imported-mesh-scale.mdc`. diff --git a/scripts/build_claude_rules.py b/scripts/build_claude_rules.py new file mode 100644 index 00000000..447272e2 --- /dev/null +++ b/scripts/build_claude_rules.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Generate claude/blender-rules.md from rules/*.mdc. + +Claude Code does not read Cursor .mdc rules. This writes a plain-markdown +summary (one section per rule: description, load scope, link to the full rule) +that a Claude Code user imports from their own CLAUDE.md with +``@path/to/Blender-Developer-Tools/claude/blender-rules.md``. + + python scripts/build_claude_rules.py # rewrite the file + python scripts/build_claude_rules.py --check # exit 1 if it is stale (CI) +""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + +REPO = Path(__file__).resolve().parent.parent +OUT = REPO / "claude" / "blender-rules.md" + + +def parse(path: Path) -> tuple[str, list[str]]: + text = path.read_text(encoding="utf-8").replace("\r\n", "\n") + m = re.match(r"---\n(.*?)\n---\n", text, re.S) + if not m: + raise SystemExit(f"{path}: missing frontmatter") + front = m.group(1) + desc = re.search(r"^description:\s*(.+)$", front, re.M) + if not desc: + raise SystemExit(f"{path}: missing description") + globs = re.findall(r'^\s+-\s+"?([^"\n]+)"?\s*$', front.split("globs:", 1)[-1], re.M) + return desc.group(1).strip(), globs + + +def render() -> str: + lines = [ + "# Blender Developer Tools rules (Claude Code)", + "", + "", + "", + "Apply these when writing or reviewing Blender Python. Each rule links to the", + "full version with Wrong/Right examples.", + "", + ] + for path in sorted((REPO / "rules").glob("*.mdc")): + desc, globs = parse(path) + lines += [ + f"## {path.stem}", + "", + desc, + "", + f"Applies to: {', '.join(f'`{g}`' for g in globs) or 'n/a'}. Full rule: `rules/{path.name}`.", + "", + ] + return "\n".join(lines).rstrip("\n") + "\n" + + +def main() -> int: + new = render() + if "--check" in sys.argv: + old = OUT.read_text(encoding="utf-8").replace("\r\n", "\n") if OUT.exists() else "" + if old != new: + print("claude/blender-rules.md is stale; run python scripts/build_claude_rules.py", file=sys.stderr) + return 1 + print("claude/blender-rules.md is up to date") + return 0 + OUT.parent.mkdir(exist_ok=True) + OUT.write_text(new, encoding="utf-8", newline="\n") + print(f"wrote {OUT.relative_to(REPO)}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/site/requirements.txt b/scripts/site/requirements.txt index 9894c744..8ea7c2a1 100644 --- a/scripts/site/requirements.txt +++ b/scripts/site/requirements.txt @@ -1 +1,2 @@ -Jinja2>=3.1,<4.0 +Jinja2==3.1.6 +MarkupSafe==3.0.2 diff --git a/site.json b/site.json index ddbe2cb3..4ab92e92 100644 --- a/site.json +++ b/site.json @@ -18,8 +18,8 @@ }, "installSteps": [ "Clone the repo: git clone https://github.com/TMHSDigital/Blender-Developer-Tools", - "Cursor: copy rules/ into your project's .cursor/rules/ — they auto-apply via scope globs; reference skills by name in chat", - "Claude Code: copy skills/ and rules/ into your project workspace, or point Claude Code at the checkout", + "Cursor: copy rules/*.mdc into your project's .cursor/rules/ — they auto-apply via scope globs; reference skills by name in chat", + "Claude Code: run /plugin marketplace add TMHSDigital/Blender-Developer-Tools then /plugin install blender-developer-tools@blender-developer-tools; import claude/blender-rules.md from your CLAUDE.md for the rules", "Grab snippets/ and templates/ as starting points for add-ons and headless batch jobs" ] } From 2094ee7cc9068cf73a31ee38a99108cf6d082119 Mon Sep 17 00:00:00 2001 From: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Date: Sat, 3 Oct 2026 11:19:45 -0400 Subject: [PATCH 2/2] fix(distribution): sync .claude-plugin versions to 0.131.2 Co-Authored-By: Claude Sonnet 5.5 --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index acd53685..da3a1afd 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ "name": "blender-developer-tools", "source": "./", "description": "Cursor and Claude Code skills, rules, snippets, and templates for Blender Python add-on and scripting development", - "version": "0.131.1" + "version": "0.131.2" } ] } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 0f8f5081..73067815 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "blender-developer-tools", "description": "Cursor and Claude Code skills, rules, snippets, and templates for Blender Python add-on and scripting development", - "version": "0.131.1", + "version": "0.131.2", "author": { "name": "TMHSDigital", "url": "https://github.com/TMHSDigital"