Skip to content

Latest commit

 

History

History
347 lines (277 loc) · 19.7 KB

technical-audit.md

File metadata and controls

347 lines (277 loc) · 19.7 KB

Blender Developer Tools — Technical Audit & Roadmap

Audit date: 2026-06-20 · Repo @ v0.5.0 · Auditor: Principal Eng review

Findings the release notes did not list

Standing log. Notes-derived 5.1→5.2 (and later) deltas are incomplete by default.

The Blender python_api release notes are a starting set, not a closed inventory. A sweep that only copies listed removals will miss behavior changes that keep the same RNA names. Record those here when CI or authoring finds them, even when upstream notes omit the break. Do not treat a clean notes-diff as proof that two series are interchangeable.

2026-09-09 — COLOR strip intrinsic size (5.2, not in the notes)

4.5 LTS / 5.1: COLOR strips have no intrinsic size. transform.scale_* is a fraction of the output frame. Creating at factory resolution then rendering a tiny buffer is fine.

5.2+: new_effect(..., type='COLOR') bakes readonly width / height from scene.render.resolution_* at creation. Scale and offset are in that media space. Factory 1920×1080 + later 96×54 render → 0.36-scaled cells larger than the output; the long-runner covers the frame.

A lone COLOR strip does honor transform.scale_* on 5.2. The break is media size vs a later output size, not "transform is ignored."

Set scene resolution before building strips.

  • Taught: skills/vse-python/SKILL.md
  • Witness: examples/vse-cut-list/ --check-pixels (on 5.2 asserts A.width, A.height == (96, 54)). No separate example — that script already fails if the bake drifts.

2026-09-09 — USD export_subdivision='BEST_MATCH' silences evaluation_mode

Default wm.usd_export export_subdivision='BEST_MATCH' writes the cage plus subdivisionScheme = catmullClark. VIEWPORT and RENDER USDA files are then identical, so evaluation_mode looks like a no-op. TESSELLATE makes the mode observable. Catmull-Clark on a cube: verts = 2 + 6 × 4^n (n=1 → 26, n=2 → 98). Same on 4.5 LTS and 5.2 LTS; not a version split, an exporter-default trap. Not listed in the python_api notes as a behavior change because the RNA names did not move.

  • Taught: skills/depsgraph-and-evaluated-data/SKILL.md
  • Witness: examples/usd-export-evaluation-mode/

2026-09-09 — Geometry Nodes zone pairing is load-bearing

Creating Repeat / For Each Element input and output nodes and linking sockets is not enough. pair_with_output creates the Geometry items; unpaired Repeat Input has only Iterations, unpaired For Each evaluates empty. For Each's main Geometry output is the input-mesh passthrough; generated meshes live on Generation_0. Tree-structure checks are vacuous. Count-only is also insufficient: joining N+1 cubes at the origin hits 8×(1+N) verts with one X-center. Same on 4.5 LTS and 5.x (zones are 4.3+). Not a version split.

  • Taught: skills/geometry-nodes-python/SKILL.md
  • Witness: examples/gn-zone-iterate/

2026-09-09 — Bundle RNA renamed; 4.5 is experimental

5.x Combine / Separate Bundle RNA is NodeCombineBundle / NodeSeparateBundle. GeometryNodeCombineBundle is undefined on 5.2 and is the 4.5 experimental id. 4.5 LTS evaluates empty unless preferences.experimental.use_bundle_and_closure_nodes is on; with the flag the same closed form lands. Official contract is 5.0+. Item names on bundle_items are load-bearing. Vert count alone is not a witness.

  • Taught: skills/geometry-nodes-python/SKILL.md
  • Witness: examples/gn-bundle-roundtrip/

Contrast — NodesModifier dict assignment (5.2, was in the notes)

mod[identifier] = value raises TypeError on 5.2 (id properties not supported for this type). Replacement: getattr(mod.properties.inputs, ident).value. This one was listed in the 5.2 python_api notes. Taught in skills/geometry-nodes-python/SKILL.md, witnessed by examples/gn-modifier-inputs/. Keep both classes of finding in this log: notes-listed removals that still need a taught split, and behavior changes the notes never mentioned.

0. Important reframing

The audit brief assumed a runnable add-on (operators, UI panels, a socket/file-watcher execution model). That is not what this repo is. This is a content / knowledge-pack repository — an AI "skill pack" of SKILL.md workflows, .mdc anti-pattern rules, copy-paste snippets/, starter templates/, and self-verifying examples/ consumed by Cursor and Claude Code. The only executable code is (a) the templates/examples/snippets, which are teaching artifacts, and (b) the CI tooling (tests/smoke/, scripts/build_gallery.py).

So "core operator logic" and "UI panels" exist only as demonstration code in templates/extension-addon-template/__init__.py and the example scripts. The architectural gaps that matter are about content correctness, validation coverage, and distribution, not runtime robustness of a live tool.

1. Architecture map

Layer What it is Notable detail
skills/ (12) Markdown workflows w/ YAML frontmatter name must match dir; standards-version gated in CI
rules/ (6) .mdc anti-pattern rules alwaysApply / scope globs for Cursor
snippets/ (17) Standalone .py patterns py_compile-checked only
templates/ (2) extension add-on + headless batch real, registerable add-on code
examples/ (4) self-verifying demo scripts run inside real Blender in CI, exit non-zero on failure
tests/smoke/ headless Blender harness already exists — runs on 5.1 + 4.5 matrix
scripts/build_gallery.py stdlib HTML generator builds docs/gallery/ Pages site
.github/workflows/ 8 workflows validate, blender-smoke, drift-check, release, pages, label-sync, stale, dependabot
.cursor-plugin/plugin.json distribution manifest stale: v0.2.3, missing arrays

Architectural style: static content pack + automated content-integrity CI. Distribution is "copy skills/+rules/ into the workspace" or reference the checkout directly. No MCP server (explicitly noted in CLAUDE.md/AGENTS.md).

Genuine strengths (the brief assumed these were missing — they are not):

  • A real headless Blender testing harness already exists. tests/smoke/run_smoke.py asserts content, not just "no exception" (e.g. EEVEE engine-id polarity, slotted-actions boundary, driver id_type fix, SDF GridToMesh link validity, render-non-black). It deliberately copies content rather than importing it, to catch drift in shipped files.
  • blender-smoke.yml runs that harness on the current stable (5.2 LTS) and fallback LTS (4.5), plus 5.1 on the weekly cron, plus runs the shipped templates and examples end-to-end (glTF magic-bytes check, exit-code-2 no-mesh path).
  • Conventional-commit-driven release automation with doc-sync; count-integrity gate in CI.

2. Gap analysis

G1 — Distribution manifest drift (concrete bug)

.cursor-plugin/plugin.json is frozen at version: 0.2.3, enumerates only skills (12) and rules (6), and has no snippets, templates, or examples arrays. Nothing in validate.yml checks the manifest against the filesystem, so it rotted silently while the repo reached v0.5.0. Consumers reading the manifest see a v0.2.0-era subset.

G2 — Validation coverage holes

validate.yml enforces structure, frontmatter, name=dir, py_compile, and README counts. It does not: (a) lint/run snippets beyond byte-compilation (no Blender import, no ruff); (b) verify every snippet/skill/template is referenced from README/ROADMAP/manifest (orphan detection); (c) check internal/external doc links; (d) validate gallery.json against the on-disk examples. The smoke harness covers a hand-picked subset, so a new snippet can ship un-exercised.

G3 — Smoke harness selectivity (silent coverage cap)

The harness exercises ~6 headline behaviors and 4 examples. 17 snippets + 12 skills are largely un-run. There is no manifest of "what is covered vs. not," so coverage erosion is invisible — a classic silent-truncation smell. No per-snippet smoke and no coverage report.

G4 — No MCP / programmatic surface

The brief's strongest forward-looking point. Today content is human/AI-read-only. There is no Model Context Protocol server exposing skills/rules/snippets as retrievable resources or tools (e.g. get_skill, search_patterns, lint_blender_py). This is the natural next-gen evolution and the cleanest way to make the pack first-class in MCP-aware clients beyond Cursor/Claude Code.

G5 — Performance/dense-data guidance is pattern-level only

Rules flag bpy.ops-in-loops and per-element loops, and snippets show foreach_set/get. But there is no skill on profiling / scaling to dense meshes (e.g. numpy buffer interop, foreach_get into preallocated numpy arrays, attribute-domain bulk ops, depsgraph cost). For a tool whose whole pitch is "do bulk work the fast way," this is a content gap.

G6 — LTS/version sweep is a matrix + human delta pass

5.2 LTS is current stable. The smoke matrix is 5.2 + 4.5 on PRs, with 5.1 on the weekly cron. A 5.1→5.2 API removal (NodesModifier dict assignment) still required a human reading of the python_api notes — the LTS "no API change" guarantee does not cover the 5.1 to 5.2 minor step.

3. Roadmap

Stage 1 — Short-term / Quick Wins (days)

  • Fix + gate the Cursor manifest (G1). Regenerate plugin.json from the filesystem and add a CI check so it can't drift again.
  • Orphan + link audit job (G2). Fail CI if any snippet/skill/template/example is unreferenced by README/ROADMAP/manifest, or if a markdown link 404s.
  • gallery.json ↔ disk validator (G2). Assert every example dir + hero image referenced exists and vice-versa.
  • Coverage manifest for smoke (G3). Emit a printed "covered / not covered" table so gaps are visible in the CI log.

Stage 2 — Mid-term / Core Enhancements (weeks)

  • Per-snippet smoke harness (G3): a generic runner that imports/execs each snippet in headless Blender behind a small # smoke: skip opt-out, closing the un-run-content gap.
  • ruff + import-time lint of all Python content with a Blender-aware config.
  • New skill: performance-and-dense-data (G5) — numpy foreach_get/foreach_set buffers, attribute domains, when bmesh beats data API, profiling with time/cProfile headless.
  • New skills from the committed pool: modal-operators, mathutils-patterns, usd-pipelines.
  • Automated LTS-series watcher (G6): scheduled job that diffs download.blender.org series against the matrix and opens a sweep issue.

Stage 3 — Long-term / Next-Gen Capabilities (quarters)

  • MCP server (G4) exposing the pack as MCP resources + tools: list_skills, get_skill, search_patterns, and a lint_blender_python tool wrapping the .mdc rules as executable checks. Ships as an optional adjacent package so the content repo stays a pure pack.
  • Rules-as-linter: compile the 6 .mdc anti-patterns into an actual AST linter (libcst/ast) that runs in CI and is reusable by the MCP lint tool and by consumers.
  • 5.2 LTS targeting is the current primary; keep blender_version_min at 4.5.0 unless a 5.2-only API is required. NodesModifier input writes must version-gate dict vs mod.properties.inputs.
  • Fleet Pages examples support (already in ROADMAP) — lift gallery.json into the shared template, retire the local generator.

4. GitHub Issues (copy-paste ready)

Issue 1 — Fix and CI-gate the stale .cursor-plugin/plugin.json manifest

Labels: bug, ci, distribution

Context / Problem Statement .cursor-plugin/plugin.json is pinned at "version": "0.2.3" while the repo is at v0.5.0 (VERSION). It lists only the skills (12) and rules (6) arrays and omits snippets, templates, and examples entirely. No job in .github/workflows/validate.yml checks the manifest against the filesystem, so it has drifted silently. Consumers that read the manifest see a v0.2.0-era view of the pack.

Proposed Solution / Implementation Steps

  1. Decide the manifest schema for snippets/templates/examples (mirror the existing relative-path-array style used for skills/rules). If the upstream cursor-plugin schema only supports skills+rules, document that and instead just fix version + add the gate.
  2. Update .cursor-plugin/plugin.json: bump version to match VERSION, and add the missing arrays enumerating files under snippets/, templates/, examples/.
  3. Add a validate-manifest job to validate.yml (a python3 heredoc like the existing validate-counts job) that:
    • loads plugin.json,
    • asserts every path in each array exists on disk,
    • asserts every skills/*/SKILL.md and rules/*.mdc on disk is present in the manifest,
    • asserts plugin.json version equals VERSION (or is intentionally decoupled — pick one and enforce it).
  4. If version should track releases, add it to the release-doc-sync owned-lines list so the release pipeline rewrites it; otherwise document why it is independent in AGENTS.md.

Definition of Done

  • plugin.json enumerates all current skills, rules, snippets, templates, examples.
  • plugin.json version reconciled with VERSION (synced or documented as independent).
  • New CI job fails on any manifest↔filesystem mismatch (proven by a deliberately broken commit).
  • AGENTS.md "CI/CD workflows" section documents the new job.

Issue 2 — Add orphan-content and dead-link validation to CI

Labels: ci, content-integrity

Context / Problem Statement validate.yml checks structure, frontmatter, name=dir, py_compile, and README aggregate counts, but nothing detects a snippet/skill/template/example that ships without being referenced by README.md, ROADMAP.md, or the manifest, nor does anything catch broken markdown links. New content can land orphaned (discoverable only by directory listing), and doc links can rot.

Proposed Solution / Implementation Steps

  1. Add an orphan-check job to validate.yml: enumerate files under snippets/, skills/, templates/, examples/; for each, grep README.md + ROADMAP.md for its name; fail listing any unreferenced item.
  2. Add a lightweight link check: collect markdown links from *.md, validate that relative targets exist on disk; optionally check external https:// links with a --allow-fail soft mode to avoid flakiness (or gate external checks behind the weekly schedule, not PRs).
  3. Keep it stdlib-only (consistent with build_gallery.py) so no new CI deps are needed.

Definition of Done

  • CI fails when a snippet/skill/template/example is unreferenced in README/ROADMAP.
  • CI fails on a broken relative markdown link.
  • External link checks run on the weekly schedule, not on every PR (no PR flakiness).
  • A deliberately orphaned test file proves the gate fires, then is removed.

Issue 3 — Generalize the smoke harness to per-snippet coverage + emit a coverage table

Labels: testing, enhancement

Context / Problem Statement tests/smoke/run_smoke.py is excellent but exercises only ~6 hand-picked headline behaviors and blender-smoke.yml runs 4 examples. The 17 snippets and 12 skills are largely un-run in real Blender, and there is no record of what is covered. A new snippet can ship having only passed py_compile, and coverage can erode invisibly (silent-truncation risk).

Proposed Solution / Implementation Steps

  1. Add tests/smoke/run_snippets.py: iterate snippets/*.py, exec each inside a reset() factory-empty scene in headless Blender, treating any raised exception as failure (mirror the try/except wrapper already in run_smoke.py).
  2. Support a # smoke: skip <reason> first-line pragma for snippets that need scene state they can't self-provide; print the skipped list so skips are visible, never silent.
  3. Emit a coverage table at the end: N snippets run, M skipped (reasons), K skills with a smoke check. Print it to the CI log.
  4. Wire a new step into blender-smoke.yml after the existing smoke driver, on the same 5.1/4.5 matrix, xvfb-run-wrapped.

Definition of Done

  • Every snippet is either executed in headless Blender or explicitly # smoke: skip-ged with a reason.
  • CI log prints a run/skip/coverage summary table.
  • A snippet that raises in Blender fails the job (proven once).
  • Runs green on both 5.1 and 4.5 matrix legs.

Issue 4 — New skill: performance-and-dense-data (numpy buffer interop + profiling)

Labels: content, skill

Context / Problem Statement The pack's core thesis is "do bulk Blender work the fast way," and rules (prefer-data-over-ops-in-loops, use-foreach-set-for-bulk-data) plus the foreach-* snippets point that direction. But there is no skill that teaches scaling to dense meshes: numpy buffer interop with foreach_get/foreach_set, attribute-domain bulk operations, the bmesh-vs-data-API decision under load, and how to profile headless. AI agents currently get the "don't do it slow" rules without a positive "here is the fast, measured pattern" skill.

Proposed Solution / Implementation Steps

  1. Create skills/performance-and-dense-data/SKILL.md with the standard YAML frontmatter (name: performance-and-dense-data, one-line description, standards-version: 1.10.0).
  2. Cover: preallocated numpy arrays with foreach_get/foreach_set (np.empty(n*3)), reshaping conventions, attribute API bulk read/write across domains, when bmesh wins, and a headless cProfile/time.perf_counter measurement pattern. Show 5.1 and 4.5 paths where they diverge.
  3. Add 1–2 supporting snippets (e.g. numpy-foreach-vertices.py) and a self-verifying assertion in the smoke harness (roundtrip equality + a coarse timing sanity check).
  4. Update README.md aggregate counts, ROADMAP.md candidate pool, AGENTS.md, and plugin.json; commit with feat:.

Definition of Done

  • New skill passes frontmatter + name=dir CI checks.
  • Shows numpy buffer interop and at least one profiling pattern, with 4.5/5.1 branches where relevant.
  • Supporting snippet(s) added and smoke-exercised.
  • README counts, ROADMAP, AGENTS, and manifest updated (counts gate green).

Issue 5 — Prototype an MCP server exposing the pack as resources + a Blender-Python lint tool

Labels: enhancement, mcp, next-gen

Context / Problem Statement CLAUDE.md and AGENTS.md both note "there is no MCP server." Today the content is consumable only by clients that natively read skills/+rules/ (Cursor, Claude Code). An MCP server would make the pack first-class in any MCP-aware client and turn the 6 .mdc anti-pattern rules into an executable lint tool — a far stronger guarantee than prose rules.

Proposed Solution / Implementation Steps

  1. Add an optional adjacent package (e.g. mcp-server/) so the content repo stays a pure pack; the server reads skills/, rules/, snippets/ from the repo at runtime.
  2. Expose MCP resources: list_skills, get_skill(name), list_snippets, get_snippet(name), search_patterns(query).
  3. Expose an MCP tool lint_blender_python(source) implementing the rules as AST checks: bpy.ops.* inside loops, bmesh.new() without paired free(), prop assignment vs annotation, bpy.context.copy() override, per-element bulk loops. Reuse this checker in CI (see the rules-as-linter long-term item).
  4. Document install/usage in a mcp-server/README.md; keep it out of the count/validate gates (it's tooling, not pack content) but add its own minimal test job.

Definition of Done

  • MCP server lists and serves skills/rules/snippets as resources.
  • lint_blender_python flags all 6 anti-patterns on a fixture file and passes clean code.
  • Server runs against the live repo checkout with documented setup.
  • CI runs the server's own tests without touching the content-count gates.