Skip to content

Commit 3c21c8c

Browse files
TMHSDigitalclaude
andcommitted
feat: every skill lists the snippets that serve it, from snippets/skills.json
Examples reached skills through a generated, validated section, but snippets were linked by hand, and three smoke-tested ones (register-classes-factory, pointerproperty-binding, foreach-get-vertices) were linked from no skill, so agents that find content through skills never saw them. snippets/skills.json maps each snippet to its skills; build_examples_index.py writes a Snippets list into the generated section and fails (Validate) on a snippet that maps to no skill or is missing from the mapping (both falsified). Closes #524 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LpkeXRvikhJZSVGBFM91Dv Signed-off-by: TM Hospitality Strategies <TMHospitalityStrategies@gmail.com>
1 parent ec43b85 commit 3c21c8c

20 files changed

Lines changed: 261 additions & 2 deletions

File tree

‎CONTRIBUTING.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ showcase/
138138
1. Add a `.py` file under `snippets/`, e.g. `snippets/depsgraph-evaluated-mesh.py`.
139139
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.
140140
3. Add an entry to `tests/smoke/snippet_checks.py`: a `check` that calls the snippet's functions and asserts one observable fact, and a `falsify` (old, new) source edit that must make the check fail. Content Smoke (`.github/workflows/content-smoke.yml`) runs the snippet in place on 4.5 and 5.2; Validate fails a snippet without an entry. Run it locally with `blender --background --factory-startup --python tests/smoke/run_content.py -- --only <name>`.
141+
4. Map it to the skills it serves in `snippets/skills.json`, then run `python scripts/build_examples_index.py`: each skill's generated section lists its snippets, and Validate fails a snippet that maps to no skill.
141142

142143
## Adding a Template
143144

‎scripts/build_examples_index.py‎

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
#!/usr/bin/env python3
22
"""Generate examples/index.json and each skill's "Runnable examples" section.
33
4+
The same generated section also lists the snippets that serve the skill
5+
(snippets/skills.json, #524), so a smoke-tested snippet is never shipped
6+
without a skill that points an agent at it.
7+
48
One file describes every example (#377): its script, the skills it proves,
59
whether it ships a gallery render, the oldest Blender it runs on, the command
610
to run it, every smoke falsifier with its expected exit, and the README's
711
exit-code table. Inputs, all hand-edited elsewhere:
812
913
examples/skills.json example -> skills it proves (edit this one)
14+
snippets/skills.json snippet -> skills it serves (and this one)
1015
tests/smoke/catalog.json scripts, min_version, falsifiers
1116
examples/gallery.json which examples render
1217
examples/<name>/README.md one-line summary and exit-code table
@@ -149,14 +154,35 @@ def build_index() -> tuple[list[dict], list[str]]:
149154
return index, errors
150155

151156

157+
def load_snippets(skills: list[str]) -> tuple[dict[str, list[str]], list[str]]:
158+
"""skill -> snippet files, and the mapping's errors."""
159+
errors: list[str] = []
160+
mapping = json.loads((ROOT / "snippets" / "skills.json").read_text(encoding="utf-8"))
161+
mapping.pop("_comment", None)
162+
files = sorted(p.name for p in (ROOT / "snippets").glob("*.py"))
163+
for name in sorted(set(files) - set(mapping)):
164+
errors.append(f"snippets/{name} is missing from snippets/skills.json")
165+
for name in sorted(set(mapping) - set(files)):
166+
errors.append(f"snippets/skills.json lists {name}, which is not a snippet")
167+
by_skill: dict[str, list[str]] = {}
168+
for name, listed in sorted(mapping.items()):
169+
if not listed:
170+
errors.append(f"snippets/skills.json: {name} maps to no skill; point one at it")
171+
for s in listed:
172+
if s not in skills:
173+
errors.append(f"snippets/skills.json: {name} names unknown skill {s!r}")
174+
by_skill.setdefault(s, []).append(name)
175+
return by_skill, errors
176+
177+
152178
def _flag(f: dict) -> str:
153179
args = " ".join(f["args"])
154180
if f.get("expect_sidecar_fail"):
155181
return f"`{args}` (harness fails the sidecar)"
156182
return f"`{args}` (exit {f['expect_exit']})"
157183

158184

159-
def skill_section(skill: str, index: list[dict]) -> str:
185+
def skill_section(skill: str, index: list[dict], snippets: dict[str, list[str]] | None = None) -> str:
160186
lines = [BEGIN, "## Runnable examples", ""]
161187
mine = [e for e in index if skill in e["skills"]]
162188
if not mine:
@@ -172,6 +198,12 @@ def skill_section(skill: str, index: list[dict]) -> str:
172198
since = f" Blender {e['min_version']}+." if e["min_version"] != "4.5" else ""
173199
lines.append(f"- [`{e['name']}`]({REPO_URL}/tree/main/examples/{e['name']}): {e['summary']}"
174200
f"{since} Falsify: {first}.")
201+
mine_snippets = (snippets or {}).get(skill, [])
202+
if mine_snippets:
203+
lines += ["", "### Snippets", "",
204+
"Short standalone patterns to copy. Each one runs in CI on 5.2 and 4.5 "
205+
"with a check and a falsifier.", ""]
206+
lines += [f"- [`snippets/{n}`]({REPO_URL}/blob/main/snippets/{n})" for n in mine_snippets]
175207
lines += ["", END]
176208
return "\n".join(lines)
177209

@@ -193,13 +225,17 @@ def main(argv=None) -> int:
193225
ap.add_argument("--check", action="store_true")
194226
a = ap.parse_args(argv)
195227
index, errors = build_index()
228+
snippets, snippet_errors = load_snippets(
229+
sorted(p.parent.name for p in (ROOT / "skills").glob("*/SKILL.md")))
230+
errors += snippet_errors
196231
if errors: # a broken mapping cannot render the skill sections
197232
for e in errors:
198233
print(f"::error::{e}", file=sys.stderr)
199234
return 1
200235
want ={ROOT / "examples" / "index.json": json.dumps(index, indent=2, ensure_ascii=False) + "\n"}
201236
for p in sorted((ROOT / "skills").glob("*/SKILL.md")):
202-
want[p] = render_skill(p.read_text(encoding="utf-8"), skill_section(p.parent.name, index))
237+
want[p] = render_skill(p.read_text(encoding="utf-8"),
238+
skill_section(p.parent.name, index, snippets))
203239
stale = [p for p, text in want.items()
204240
if not p.is_file() or p.read_text(encoding="utf-8") != text]
205241
if a.check:

‎skills/addon-scaffolding/SKILL.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,12 @@ Each example runs headless, asserts the contract, and exits non-zero when it bre
223223

224224
- [`addon-register-lifecycle`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/addon-register-lifecycle): Check-only witness for the first things every add-on author writes. Falsify: `--drop-manifest` (exit 3).
225225

226+
### Snippets
227+
228+
Short standalone patterns to copy. Each one runs in CI on 5.2 and 4.5 with a check and a falsifier.
229+
230+
- [`snippets/register-classes-factory.py`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/snippets/register-classes-factory.py)
231+
226232
<!-- examples:end -->
227233

228234
## References

‎skills/ai-mesh-cleanup/SKILL.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,15 @@ Each example runs headless, asserts the contract, and exits non-zero when it bre
262262
- [`prop-origin-transform`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/prop-origin-transform): Street pedestal origin-to-base-center + data-API scale apply + matrix_parent_inverse for a flanged conduit elbow. Falsify: `--skip-mpi` (exit 8).
263263
- [`unapplied-scale-gltf`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/unapplied-scale-gltf): Synthesizes unapplied non-uniform object scale on a unit cube, then asserts the glTF exporter contract. Falsify: `--identity` (exit 3).
264264

265+
### Snippets
266+
267+
Short standalone patterns to copy. Each one runs in CI on 5.2 and 4.5 with a check and a falsifier.
268+
269+
- [`snippets/convex_hull_collider.py`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/snippets/convex_hull_collider.py)
270+
- [`snippets/decimate_to_budget.py`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/snippets/decimate_to_budget.py)
271+
- [`snippets/gltf_draco_export.py`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/snippets/gltf_draco_export.py)
272+
- [`snippets/lod_chain.py`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/snippets/lod_chain.py)
273+
265274
<!-- examples:end -->
266275

267276
## References

‎skills/bake-high-to-low/SKILL.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,4 +172,13 @@ Each example runs headless, asserts the contract, and exits non-zero when it bre
172172

173173
- [`bake-normal-high-to-low`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/bake-normal-high-to-low): A collapse-decimated hatch plate receiving a Cycles cage-baked tangent normal map from a ribbed high-poly source Falsify: `--flat-source` (exit 5).
174174

175+
### Snippets
176+
177+
Short standalone patterns to copy. Each one runs in CI on 5.2 and 4.5 with a check and a falsifier.
178+
179+
- [`snippets/bake_normal_high_to_low.py`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/snippets/bake_normal_high_to_low.py)
180+
- [`snippets/decimate_to_budget.py`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/snippets/decimate_to_budget.py)
181+
- [`snippets/save_baked_image.py`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/snippets/save_baked_image.py)
182+
- [`snippets/setup_bake_target_image.py`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/snippets/setup_bake_target_image.py)
183+
175184
<!-- examples:end -->

‎skills/compositor-nodes/SKILL.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,12 @@ Each example runs headless, asserts the contract, and exits non-zero when it bre
180180

181181
- [`compositor-glare`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/compositor-glare): Bloom where it actually lives — a compositor Glare (Fog Glow) node fed by Render Layers. Falsify: `--threshold-high` (exit 4).
182182

183+
### Snippets
184+
185+
Short standalone patterns to copy. Each one runs in CI on 5.2 and 4.5 with a check and a falsifier.
186+
187+
- [`snippets/shader-node-group.py`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/snippets/shader-node-group.py)
188+
183189
<!-- examples:end -->
184190

185191
## References

‎skills/custom-properties/SKILL.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,13 @@ Each example runs headless, asserts the contract, and exits non-zero when it bre
275275
- [`addon-register-lifecycle`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/addon-register-lifecycle): Check-only witness for the first things every add-on author writes. Falsify: `--drop-manifest` (exit 3).
276276
- [`cross-version-property-delete`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/cross-version-property-delete): Custom ID properties are removed with del, not property_unset. Falsify: `--skip-delete` (exit 7).
277277

278+
### Snippets
279+
280+
Short standalone patterns to copy. Each one runs in CI on 5.2 and 4.5 with a check and a falsifier.
281+
282+
- [`snippets/cross-version-property-delete.py`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/snippets/cross-version-property-delete.py)
283+
- [`snippets/pointerproperty-binding.py`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/snippets/pointerproperty-binding.py)
284+
278285
<!-- examples:end -->
279286

280287
## References

‎skills/depsgraph-and-evaluated-data/SKILL.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,13 @@ Each example runs headless, asserts the contract, and exits non-zero when it bre
222222
- [`solidify-even-thickness`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/solidify-even-thickness): Solidify's use_even_offset on a folded strip. Falsify: `--no-even` (exit 4).
223223
- [`usd-export-evaluation-mode`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/usd-export-evaluation-mode): The USD exporter evaluation_mode chooses viewport versus render modifier quality. Falsify: `--subdivision BEST_MATCH` (exit 4).
224224

225+
### Snippets
226+
227+
Short standalone patterns to copy. Each one runs in CI on 5.2 and 4.5 with a check and a falsifier.
228+
229+
- [`snippets/depsgraph-evaluated-mesh.py`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/snippets/depsgraph-evaluated-mesh.py)
230+
- [`snippets/usd-export-evaluation-mode.py`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/snippets/usd-export-evaluation-mode.py)
231+
225232
<!-- examples:end -->
226233

227234
## References

‎skills/drivers-and-app-handlers/SKILL.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,13 @@ Each example runs headless, asserts the contract, and exits non-zero when it bre
330330
- [`driver-wave`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/driver-wave): A driver_namespace function driving sixteen organ-pipe heights through SCRIPTED drivers — the sine skyline of the pipe tops is entirely driver-evaluated. Falsify: `--flat-expr` (exit 3).
331331
- [`exit-pre-sidecar`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/exit-pre-sidecar): `bpy.app.handlers.exit_pre` writes `$BDT_SMOKE_SIDECAR` as Blender dies. Blender 5.1+. Falsify: `--atexit-instead` (harness fails the sidecar).
332332

333+
### Snippets
334+
335+
Short standalone patterns to copy. Each one runs in CI on 5.2 and 4.5 with a check and a falsifier.
336+
337+
- [`snippets/app-handler-registration.py`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/snippets/app-handler-registration.py)
338+
- [`snippets/driver-with-custom-function.py`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/snippets/driver-with-custom-function.py)
339+
333340
<!-- examples:end -->
334341

335342
## References

‎skills/engine-export-presets/SKILL.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,15 @@ Each example runs headless, asserts the contract, and exits non-zero when it bre
149149
- [`gltf-skin-roundtrip`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/gltf-skin-roundtrip): A rigged mech scorpion exported to glTF with skins and re-imported, verifying the skinning contract the geometry round-trip left uncovered. Falsify: `--no-skins` (exit 5).
150150
- [`unapplied-scale-gltf`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/unapplied-scale-gltf): Synthesizes unapplied non-uniform object scale on a unit cube, then asserts the glTF exporter contract. Falsify: `--identity` (exit 3).
151151

152+
### Snippets
153+
154+
Short standalone patterns to copy. Each one runs in CI on 5.2 and 4.5 with a check and a falsifier.
155+
156+
- [`snippets/export_preset_godot.py`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/snippets/export_preset_godot.py)
157+
- [`snippets/export_preset_unity.py`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/snippets/export_preset_unity.py)
158+
- [`snippets/export_preset_unreal.py`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/snippets/export_preset_unreal.py)
159+
- [`snippets/gltf_draco_export.py`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/snippets/gltf_draco_export.py)
160+
152161
<!-- examples:end -->
153162

154163
## References

0 commit comments

Comments
 (0)