feat(vfx): agent control of Unity Visual Effect Graph#227
Open
arghhhhh wants to merge 82 commits into
Open
Conversation
chore(release): v0.11.4
The skills linter test imported std::os::unix::fs::symlink unconditionally, which broke `cargo test` on Windows. Gate the import and the symlink-creating test with #[cfg(unix)] so the test suite builds on all platforms. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Introduce vfx_describe_graph, vfx_list_library, and vfx_apply across every layer so they are native unity-cli tools rather than raw-only additions: - Unity bridge handler (VfxGraphHandler) driving the internal UnityEditor.VFX authoring API via reflection, with read-back, library discovery, and an add_block mutation; wired into the host dispatch switch. - Rust tool catalog: names, explicit param schemas, read-only flags for the two query tools, parity-count bump, and per-tool schema unit tests. - unity-vfx-graph skill (Skill Contract v1) with runtime checklist and the .claude/.agents symlinks; unity-asset-management cross-listed as a sibling. - EditMode contract tests (argument validation / op routing) and a docs/tools.md Visual Effect Graph section. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Consolidate VFX development into the UnityCliBridge test project so the authoring commands have real, CI-runnable coverage: - Add com.unity.visualeffectgraph 17.3.0 to the test project (URP already present); VFXManager + lockfile updated by the import. - Commit a Minimal.vfx fixture under Assets/VfxFixtures. - Gate behavioral tests behind a UNITY_VFX_GRAPH versionDefine so the test assembly still builds without the package; add tests that describe the fixture and add a Turbulence block via VfxGraphHandler against a copy. Verified: all VfxGraphHandlerTests pass in EditMode (6 methods, incl. the two behavioral ones) in the consolidated project. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- vfx_describe_graph now reports each block's index + [VFXSetting] values, making setting changes verifiable (the Tier-1 oracle this op depends on). - vfx_apply gains op "set_block_setting": target a block by (contextType, blockIndex), convert the value to the field type (e.g. string -> enum), SetSettingValue, persist. Adds shared FindContext/FindField/Persist helpers (AddBlock refactored onto them). - tool_catalog: widen vfx_apply schema (blockIndex, setting, value). - Tests: 2 contract + 1 behavioral (set Turbulence NoiseType -> Perlin, verified via describe); all VfxGraphHandlerTests pass in EditMode. - Skill: document the op, bump to 0.2.0. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- vfx_describe_graph now reports per-context index and inputs/outputs flow links (via VFXContext.inputContexts/outputContexts), making context linking verifiable. - vfx_apply gains op "add_context": create a context from a VFXLibrary descriptor (graph.AddChild), with optional linkFrom to flow an existing context into the new one via VFXContext.LinkTo (data auto-wires). No-match errors list available context names for discovery. - tool_catalog: widen vfx_apply schema (contextName, linkFrom, fromIndex, toIndex). - Tests: 1 contract + 1 behavioral (add a Point output linked from Update, assert 5 contexts and Update has 2 outputs); all VfxGraphHandlerTests pass in EditMode. Verified live with zero console errors. - Skill: document the op, bump to 0.3.0. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- vfx_describe_graph now reports an operators array and, for every block and
operator, input/output slots with their resolved links (node address +
slot index) — the oracle for slot wiring.
- vfx_apply gains add_operator (VFXLibrary.GetOperators descriptor ->
graph.AddChild) and link_slots (connect a from-output slot to a to-input
slot via VFXSlot.Link, with from/to addressed as {node,...,slot}).
- vfx_list_library gains kind (block|operator|context) for discovery.
- tool_catalog: widen vfx_apply (operatorName, from, to) and vfx_list_library
(kind) schemas; refresh descriptions.
- Tests: 3 contract + 2 behavioral (add operators, wire output->input, assert
links via describe); all 16 VfxGraphHandlerTests methods pass in EditMode.
- Skill bumped to 0.4.0; docs/tools.md updated.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add exposed blackboard parameters (VFXParameter) via vfx_apply add_parameter (name/type/default value/tooltip/category/exposed), extend vfx_describe_graph with a parameters array, support linking a parameter's output slot into a block/operator input via link_slots (node kind "parameter"), and add kind=parameter to vfx_list_library. Covers authoring half of Blackboard (akiojin#9). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
New first-class vfx_runtime tool drives a scene VisualEffect via its public UnityEngine.VFX API (reflection): set_asset, set_float/int/bool/vector2-4, send_event, reinit, get_state. Completes the runtime-verification machinery for Blackboard (akiojin#9) — an exposed float can be created, linked into a slot, and changed at runtime with the value round-tripping through HasFloat/GetFloat. Reused later by Events/Instancing. Includes EditMode round-trip test, contract test, Rust catalog entry + schema, skill + docs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…kiojin#6) Add the link_flow op (context->context flow edge via VFXContext.LinkTo, endpoints by contextType or index), optional settings on add_context (e.g. an Event context's eventName), and per-context settings in vfx_describe_graph. Together with the existing vfx_runtime send_event this proves Events breadth: a custom named Event context can be authored, flow-linked into Spawn, and triggered at runtime. Includes EditMode authoring test + contract test, catalog descriptions, skill + docs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Align VfxGraphHandler with the package's handler style (ComponentHandler et al.):
validation failures now return `new { error = ... }` and each public entry point
(DescribeGraph, ListLibrary, Apply, Runtime) wraps its body in try/catch that
logs via BridgeLogger and returns `{ error = ex.Message }`, instead of throwing
into the host's INTERNAL_ERROR envelope. Required-arg checks are quiet early
returns; only unexpected errors are logged. Contract tests updated to assert the
returned error field. Success results stay JObject (also a package idiom).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
set_bounds switches the Init context's VFXDataParticle boundsMode (Manual/Recorded/Automatic) and writes the bounds AABox center/size and boundsPadding when supplied. boundsMode lives on the data, not the context, but VFXBasicInitialize bridges GetSettings to the data so it already surfaces on the Init context's settings map in describe. Extends the describe oracle so the manual extents are verifiable: contexts now emit inputSlots/outputSlots (previously only blocks and operators did), and every slot carries its resolved value. ToJToken hand-serializes Vector2/3/4, Color, and any UnityEditor.VFX / UnityEngine.VFX struct (e.g. AABox) by public fields — Newtonsoft recurses through Vector3.normalized otherwise. Closes the bounds pass-1 breadth proof. Recording-mode capture stays Pass-2/manual (interactive editor-only).
add_sticky_note appends a StickyNoteInfo to VFXGraph.UIInfos.stickyNoteInfos with title/contents/position(Rect)/colorTheme/textSize. The notes sit on the VFXUI ScriptableObject sidecar, not the model graph; reach it via graph.UIInfos (auto-creates) and the nested type UnityEditor.VFX.VFXUI+ StickyNoteInfo. StickyNoteInfo is a plain [Serializable] class with public fields (not VFXModel), so go through FindField + Activator.CreateInstance rather than Prop/SetSettingValue. Describe gets a top-level stickyNotes array (index/title/contents/position/ colorTheme/textSize/theme). ToJToken now hand-serializes UnityEngine.Rect alongside the Vector and Color structs. Closes the sticky-notes pass-1 breadth proof. Update/delete/reorder stay Pass-2.
set_instancing writes VisualEffectResource.instancingMode (enum, live
values include Auto/Disabled/ForceOn) and instancingCapacity (uint) on
the asset behind a .vfx graph. Reaches the resource via the same
graph.visualEffectResource accessor Persist uses; coerces JSON ints
through Convert.ChangeType so the uint capacity property accepts them.
A SerializedObject fallback on m_Infos.m_InstancingCapacity covers
future package versions that drop the public setter.
Describe now reports a top-level instancing: {mode, capacity} block.
Closes the instancing pass-1 breadth proof (authoring side). The
3-gate reconciliation (Preference master + Asset Mode + component flag)
and multi-instance runtime rendering stay Pass-2.
Custom HLSL needs no dedicated op. The block descriptor "Custom HLSL" (category HLSL) and operator (Operator/HLSL) compose through the existing add_block/add_operator paths; the inline source field m_HLSLCode is a [VFXSetting] writable via set_block_setting. The describe oracle hid m_HLSLCode because GetSettings(false, Default) uses HasFlag(filter), and m_HLSLCode lacks the InGeneratedCodeComments bit that Default requires. Flipping to listHidden=true surfaces every [VFXSetting] regardless of visibility — backward-compatible: existing settings still appear, previously-hidden ones additionally do. EditMode test authors a Custom HLSL block on Update, writes an inline function via set_block_setting, and asserts describe reports the stored m_HLSLCode. Closes the Custom HLSL pass-1 breadth proof. ShaderInclude external file mode, function selector, and operator-side source-setting (needs a new set_operator_setting op) stay Pass-2.
Two rigour gaps were identified after the initial Custom HLSL proof: slot resync from the custom signature was unverified, and a Tier-2 compile-clean assertion was missing (bad HLSL silently leaves the asset present in describe). vfx_describe_graph now accepts an opt-in includeErrors flag. When set, it constructs a fresh VFXErrorReporter, walks the graph (+ contexts + blocks + operators + parameters) calling each model's GenerateErrors into it, then reads m_Errors by reflection and emits one JObject per entry. Off by default — describe stays cheap unless callers ask. The Custom HLSL EditMode test now asserts both gaps: - writing m_HLSLCode with a single-`scale`-input function collapses the block's inputSlots from _offset+_speedFactor to a single _scale - no Error-tier validation entries exist after the edit (filtering the Minimal fixture's pre-existing NeedsRecording Warning)
create_subgraph_asset copies the VFX package's default subgraph
templates (DefaultSubgraphBlock.vfxblock / DefaultSubgraphOperator.
vfxoperator) into a target path. It's the only vfx_apply op whose
target is its own new path rather than an existing parent graph, so
ApplyCore's assetPath guard now exempts it (catalog required dropped
to ["op"]; handler enforces per-op).
set_block_setting now detects UnityEngine.Object-derived fields and
loads asset-path strings via AssetDatabase rather than asking
Newtonsoft to deserialize a ScriptableObject. This is the lever that
makes m_Subgraph writable from a JSON value.
ToJToken handles UnityEngine.Object early — emits {type, name,
assetPath} instead of recursing through GameObject/Transform. Applies
to every Object-typed setting the oracle reports, not just subgraph
references.
The EditMode test creates a Block subgraph asset, references it from
an Empty Subgraph Block in Update, and asserts describe resolves
m_Subgraph back to the created asset path — full round-trip across a
two-asset boundary.
Closes the Subgraph (Block) pass-1 breadth proof. Operator subgraph
needs a Pass-2 set_operator_setting op; System (.vfx) subgraph and
convert-selection stay Pass-2 too.
…Id oracle A particle system is just Init→Update→Output sharing one VFXDataParticle, auto-merged by VFXContext.LinkTo. No new authoring op required — chains build from existing add_context + link_flow primitives. Describe now emits dataInstanceId per context (instance id of GetData() cast to UnityEngine.Object). Equal ids prove system membership; different ids prove disjoint systems. This is the oracle that turns "I added some contexts" into a verifiable "I built a system." EditMode test takes the Minimal fixture (one existing system) and builds a SECOND system from scratch: adds Init+Update+Output, link_flow's them by index, then asserts the new chain's contexts share one dataInstanceId distinct from the original system's. Tier-2 includeErrors=true gate confirms zero Error-tier validation entries. Descriptor gotcha: the Output context's full library name is "Output Particle|Unlit|Quad", not "Output Particle|Quad" — captured in HANDOFF.
vfx_list_library kind:"template" scans the VFX package's template directory (VisualEffectAssetEditorUtility.templatePath) and returns the 6 built-in starter templates with on-disk paths. create_from_template instantiates a fresh .vfx by calling the package's static UnityEditor.VisualEffectAssetEditorUtility.CreateTemplateAsset (copies the template's serialized graph + imports). targetPath = new .vfx; template = a name (stem, resolved against the template dir) or an explicit .vfx path. Asset-creation op, so exempt from ApplyCore's assetPath guard (alongside create_subgraph_asset). EditMode tests: list reports >0 templates incl. a Minimal one; create instantiates 03_Simple_Burst and asserts the result describes as a real multi-context graph with zero Error-tier validation entries. Closes the Templates pass-1 breadth proof. Insert-into-open-graph and designate-custom-template stay Pass-2.
New first-class tool (akiojin#12 Project Settings, Pass-1 breadth proof) reading and writing UnityEngine.VFX project settings in ProjectSettings/VFXManager.asset. - VfxGraphHandler.Settings: op `get` reports a `properties` block (public static VFXManager props — fixedTimeStep/maxDeltaTime, round-trip on re-read) plus a `serialized` block (asset fields m_MaxCapacity/m_MaxScrubTime/ m_BatchEmptyLifetime). op `set` writes through the static property setter when one exists (via:"property"), else the matching serialized field m_PascalCase (via:"serialized"). - 5-layer vertical: catalog name+schema+description (parity count 133→134, +unit test), host case, skill section + example, docs/tools.md row. - Tests: 3 contract (unsupported op, set without setting/value) + 2 behavioral (get reports fixedTimeStep/maxDeltaTime; set fixedTimeStep round-trips via re-read, restored in finally). All 45 EditMode tests green, 0 skipped. Verified live via raw: get returns current values; set fixedTimeStep=0.02 re-reads as 0.02; maxCapacity exercises the serialized fallback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…refs Pass-1 breadth proof for Preferences (akiojin#13). Reuses the existing vfx_settings tool with a new `scope` param (default "project"; "preferences" routes to EditorPrefs via UnityEditor.VFX.VFXViewPreference). - VfxGraphHandler: VFXViewPreferenceType + a canonical 11-entry preference table (propName, keyConstName, type). GetVfxPreferences reflects each public static property — instancingEnabled, displayExperimentalOperator, multithreadUpdateEnabled, forceEditionCompilation, advancedLogs, cameraBuffersFallback (enum→name), authoringPrewarm{Step,Max}, ... - SetVfxPreference resolves the key constant by reflection, calls the matching EditorPrefs.SetBool/SetInt/SetFloat, invalidates the cache via VFXViewPreference.SetDirty(), then re-reads through the canonical property to confirm the round-trip. Result echoes the resolved editorPrefsKey (e.g. "VFX.InstancingEnabled"). - Catalog: schema widened with `scope` string; description updated to cover both scopes. Tool-level mutating unchanged (parity count stays 134). - Tests (3 new, total 48 EditMode, 0 skipped): * contract: unknown pref name returns a descriptive error listing the 11 known names; * behavioral get: properties block surfaces instancingEnabled, displayExperimentalOperator, multithreadUpdateEnabled; * behavioral set: instancingEnabled flips and round-trips via re-read, editorPrefsKey resolves to the package's "VFX.InstancingEnabled" constant, restored in finally. - Skill + docs: split scope:project / scope:preferences examples and call out the Instancing master gate (gate akiojin#1 of akiojin#16's 3-gate reconciliation). Verified live via raw: get scope:preferences surfaces all 11 prefs; set instancingEnabled=false re-reads as false; set authoringPrewarmStepCountPerSeconds=40 (int) round-trips; restored. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s-1 complete The last Pass-1 breadth checkbox. No new op, no oracle extension — Attributes compose entirely from add_block + set_block_setting + add_operator: - All `Set <Attribute>` blocks ship as descriptor `|Set|_<Name>` (Color, Position, Lifetime, ...) and instantiate the single SetAttribute block class with the `attribute` [VFXSetting] pre-wired to the descriptor's attribute name. - All `Get <Attribute>` operators ship as descriptor `Get|_<Name>` (Position, Direction, ...) and instantiate VFXAttributeParameter. - Composition mode (Overwrite/Add/Multiply/Blend), Random, Source, and per-component channels are ordinary [VFXSetting] fields — set_block_setting already writes them; describe already surfaces them. Behavioral test (49 EditMode tests now, 0 skipped): - add_block `|Set|_Color` → SetAttribute on Init, set_block_setting Composition=Add (round-trips via describe), - add_operator `Get|_Position` → VFXAttributeParameter, - includeErrors:true reports zero Error-tier validator issues. Skill: adds an Attributes paragraph calling out the load-bearing `|`/`|_` separators in descriptor names (agents will trip on them otherwise) + discovery via vfx_list_library, plus a corresponding example. Pass-1 breadth is now 16/16 — the project enters the Pass-2 depth sweep. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…kiojin#5) Pass-2 sweep begins. New `vfx_apply` op `set_slot_value` writes a constant into an unlinked input slot, addressed by the same `{node, …address, slot}` shape as a `link_slots` endpoint. The bare op coerces the JSON `value` to the slot's value type (number/bool/[x,y,z] vector/[r,g,b,a] color, enums); an optional `subPath` walks into compound value structs via the box-and-write trick generalized to arbitrary depth (`["center"]`, `["size","x"]`), leaving sibling components untouched. Verifiable through the existing oracle: every slot already carries its resolved `inputSlots[].value` in vfx_describe_graph. Verified live (scalar Rate=42.5, bounds.center/size.x nested writes, whole-vector padding) and recompiles clean (Tier-2 includeErrors → 0 Error-tier entries). 5-layer vertical: handler op + reused ResolveNode/GetSlot/Persist helpers; catalog schema gains `target`/`subPath` params + description; skill examples + prose; docs/tools.md row; 3 behavioral + 2 contract EditMode tests (all green, 59 passed / 0 failed / 0 skipped). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pass-2 companion to link_slots. New `vfx_apply` op `unlink_slots` removes a
slot connection: `target` is the input-slot endpoint `{node, …address, slot}`
(same shape as a link_slots endpoint), resolved via the shared
ResolveNode/GetSlot helpers. By default UnlinkAll(recursive, notify) clears
every link on the slot (input slots hold one link in VFX, so this is
unambiguous); an optional `from` output-slot endpoint unlinks only that edge
via VFXSlot.Unlink(other). Returns linksRemoved/remainingLinks.
Verifiable through the existing oracle: describe emits inputSlots[].hasLink /
links, so an unlink flips hasLink false / empties links. Verified live (link
two operators → unlink → hasLink True→False, linksRemoved 1; from-specific
edge path; missing-target error) and recompiles clean (0 Error-tier).
5-layer vertical: handler op + LinkCount helper; catalog description (target/
from/to params already present); skill example + prose; docs/tools.md row;
1 behavioral + 1 contract EditMode test (all green, 61 passed / 0 failed /
0 skipped).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…iojin#4) Symmetrical to set_block_setting, for operators. New `vfx_apply` op `set_operator_setting` writes a [VFXSetting] field on a graph operator addressed by `operatorIndex`. Reuses the setting-coercion path (extracted into a shared `CoerceSettingValue` helper, which also handles UnityEngine.Object fields by asset path — so an Operator subgraph's `m_Subgraph` works the same way the block side does). Oracle extension (prerequisite): `vfx_describe_graph` now emits `settings` per entry in the `operators` array via the existing ModelSettings helper — without it the write would be unverifiable. Unblocks the Operator subgraph reference path and Custom HLSL operator source. Verified live: set `m_OperatorName`/`m_HLSLCode` on a Custom HLSL operator, the single-arg HLSL signature reshaped the operator's input slots to one `k` port (real model resync), round-tripped through `operators[].settings`, recompiled clean (0 Error-tier). 5-layer vertical: handler op + shared helper + oracle; catalog description (operatorIndex/setting/value params already present); skill example + prose; docs/tools.md row; 1 behavioral + 2 contract EditMode tests (all green, 64 passed / 0 failed / 0 skipped). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ontexts (akiojin#1/akiojin#2/akiojin#4/akiojin#9) Four new `vfx_apply` ops sharing the "locate by address → unlink → RemoveChild → persist" shape: - remove_block — {contextType, blockIndex} → context.RemoveChild - remove_operator — {operatorIndex} → graph.RemoveChild - remove_parameter — {parameterIndex} → graph.RemoveChild - remove_context — {contextType} or {index} → graph.RemoveChild Cascade contract (investigated against the 17.3 package source): VFXModel. RemoveChild does NOT unlink a removed node's data slots, and VFXContext. OnRemoved does NOT drop flow edges — both would leave dangling links in the nodes on the other end. So every op explicitly unlinks first via a shared UnlinkContainerSlots helper (UnlinkAll on each top-level slot), and remove_context additionally calls VFXContext.UnlinkAll() for flow edges. Each response reports remaining* counts. Verified live + EditMode: add-then-remove each kind; removing a linked operator leaves the survivor's output link-free; removing the Output context clears Update's downstream flow edge; all recompile with zero Error-tier (no dangling refs). Required-arg checks precede LoadGraph (contract tests use a fake path). Catalog: added operatorIndex/parameterIndex/index top-level params (also retroactively completes set_operator_setting's first-class schema) + description. 5-layer vertical: 4 behavioral + 2 contract EditMode tests (70 passed / 0 failed / 0 skipped); skill examples + prose; docs/tools.md row. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ta settings (akiojin#2) New `vfx_apply` op `set_context_setting` writes a [VFXSetting] field on a context addressed by `contextType` or `index`. One op covers two addressing styles: it tries the context's own field first (Spawn loopDuration/loopCount/ delay, Update ageParticles/reapParticles/integration, Output blendMode/uvMode/ shader), then falls back to the context's particle data via GetData() — the same bridge set_bounds uses — so Init `capacity`/`stripCapacity`/`boundsMode` resolve too. The response's `via` field reports `context` vs `data`. Reuses the shared CoerceSettingValue helper and ResolveContextRef. Verifiable through the existing oracle: describe already surfaces both context- and data-level settings on `contexts[].settings`. Verified live + EditMode: Spawn loopDuration=Constant (via context), Update ageParticles=false (via context), Init capacity=256 (via data), Output blendMode=Additive (via context) — all round-trip through describe and recompile clean (0 Error-tier). Required-arg checks precede LoadGraph. 5-layer vertical: handler op; catalog description (contextType/index/setting/ value params already present); skill examples + prose; docs/tools.md row; 1 behavioral + 2 contract EditMode tests (73 passed / 0 failed / 0 skipped). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…/max (akiojin#9) Round out add_parameter so an agent can declare the full exposed-parameter palette, not just Float: - Full type matrix verified end-to-end: Bool/Int/Uint/Float/Vector2/3/4/Color/ Texture2D/Texture3D/Cubemap/Gradient/Animation Curve/Mesh. - Value coercion fixed per type: routed through a new ParamCoerce helper that uses CoerceToType for the math structs Newtonsoft can't build ([x,y,z] vectors, [r,g,b,a] color) and loads UnityEngine.Object types (Texture/Mesh) from an asset-path string. Previously only Newtonsoft-buildable primitives worked. - Descriptor names carry spaces ("Vector 3", "Texture 2D"); added a space-insensitive match so "Vector3"/"Texture2D" resolve too. - Constant (non-exposed) params via exposed:false. - min/max numeric Range: sets VFXParameter.valueFilter=Range (enum parsed off the property's own type) then min/max via the public properties. Oracle: describe's parameters[] now emits valueFilter/min/max alongside the existing fields, so ranges round-trip. Verified live (all 13 types + Int/Float ranges + non-exposed constant + "Texture 2D" spaced form) and recompiles clean (0 Error-tier). 5-layer vertical: handler + ParamCoerce helper + oracle; catalog min/max params; skill examples + prose; 1 behavioral EditMode test (74 passed / 0 failed / 0 skipped). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes the operator-subgraph half of akiojin#8. The pieces already existed (create_subgraph_asset kind:"operator" copies DefaultSubgraphOperator.vfxoperator; set_operator_setting m_Subgraph lands the Object-by-path ref via CoerceSettingValue) but nothing proved them composing — this also retroactively validates that set_operator_setting's Object-field branch works for operator subgraphs (only the block side was asserted before). New EditMode behavioral test: create an operator subgraph asset, add an "Empty Subgraph Operator" (VFXSubgraphOperator), set m_Subgraph to the asset path, and assert describe surfaces operators[].settings.m_Subgraph as {type:VisualEffectSubgraphOperator, name, assetPath} with zero Error-tier. Verified live first (ref resolves, recompiles clean), no handler change needed. Skill updated with the operator-subgraph example + prose (both kinds now documented as verified end-to-end). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ators (akiojin#1/akiojin#4) Add two vfx_apply ops that clone a block or graph operator via the editor's VFXMemorySerializer.DuplicateObjects (mirrors VFXContextController.DuplicateBlock): the clone carries the same [VFXSetting]s + slot values with fresh GUIDs, slots unlinked. duplicate_block takes contextType+blockIndex, with optional `index` insert position and `toContextType` to copy into another compatible context (VFXContext.Accept-validated, like move_block). duplicate_operator mirrors it by operatorIndex. Shared DuplicateModelViaSerializer helper. Closes akiojin#1's and akiojin#4's copy/paste/duplicate boxes. 5-layer vertical: tool_catalog op-list + docs/tools.md + unity-vfx-graph SKILL.md updated (no new params — reuses existing vfx_apply schema); 4 EditMode tests (1 contract + 3 behavioral: within-context clone fidelity, cross-context copy, operator clone). All 123 VfxGraphHandlerTests green; describe-verified + clean recompile via the probe loop. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Build Initialize Particle Strip -> shared Update Particle -> Output ParticleStrip|Shader Graph|Quad via add_context + link_flow and assert the three contexts share one ParticleStrip VFXData (distinct from the original particle system) with no Error-tier validation. Compose-only, no new op. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Output Particle|Unlit|Mesh particle chain shares one VFXData; a standalone Output Single Mesh (VFXStaticMeshOutput) is its own disjoint single-context system and force-disables instancing (disabledReason=MeshOutput). Compose-only, no new op. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Set a particle/spawner system's display label, addressing the system by any one member context (contextType/index) + name. Routes through the static VFXSystemNames.SetSystemName: particle systems write the shared VFXData.title (every Init/Update/Output member reports it), Spawners write the context label. Describe gains a per-context systemName field (VFXSystemNames.GetSystemName). 5-layer vertical: catalog op-list, handler op + oracle, skill, EditMode contract + behavioral tests, docs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
One Spawner fans out to two disjoint particle systems (each keeps its own VFXData despite the shared spawner) and also controls a second Spawner via its Start flow input (spawner->spawner chaining). Compose-only, no new op. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
create_subgraph_asset gains kind:"system" — creates a plain .vfx via
VisualEffectAssetEditorUtility.CreateNewAsset (no .vfxblock/.vfxoperator
template). The parent references it via add_context "Subgraph" +
subgraphPath: VFXSubgraphContext isn't in the node library, so AddContext
special-cases it (CreateInstance + AddChild + SetSettingValue m_Subgraph),
mirroring VFXConvertSubgraph. Describe surfaces the reference as
contexts[].settings.m_Subgraph {type,name,assetPath} (existing Object
oracle). EditMode test builds a populated system subgraph, references it,
asserts the reference + clean recompile. Skill v0.15.0.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…jin#8) An exposed parameter added inside an operator subgraph asset (add_parameter exposed:true) automatically surfaces as an input slot on the parent's subgraph operator node. Compose-only proof + EditMode test; skill v0.16.0 documents the expose-input flow. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
akiojin#8) add_parameter gains an isOutput flag: a parameter with isOutput=true becomes a subgraph OUTPUT (VFXSubgraphOperator.OutputPredicate), surfacing as an output slot on the parent's subgraph operator node. The model property setter swaps the param's slot output->input and forces non-exposed, so it is set last (the swap preserves the value). Describe gains parameters[].isOutput. Catalog schema + skill v0.17.0 + EditMode test. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…kiojin#8) A block subgraph asset holds a single BlockSubgraph context whose m_SuitableContexts [VFXSetting] (flags enum Spawner/Init/Update/Output + combos, default InitAndUpdateAndOutput) controls which contexts accept the subgraph block. Set it with the existing set_context_setting; describe already surfaces it in contexts[].settings. Compose-only proof + EditMode test; skill v0.18.0. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Setting uvMode:"Flipbook" surfaces a flipBookSize input slot on the Output (a FlipBook x/y grid, set via set_slot_value subPath); flipbookBlendFrames and flipbookMotionVectors are plain [VFXSetting] bools. Compose-only proof over existing set_context_setting + set_slot_value + EditMode test. Also documents shaderGraph-asset assignment (already wired via the Object-by-path set_context_setting branch; positive test fixture-blocked on a VFX-target .shadergraph). Skill v0.19.0. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…kiojin#2) set_context_setting gains a composed-settings fallback: when a setting isn't a direct [VFXSetting] field on the context/data, resolve it through the model's virtual GetSetting (which surfaces composed-output nested settings + their owning instance) and write via SetSettingValue (response via:"context-composed"). This unblocks assigning a ShaderGraphVfxAsset to the dedicated Shader Graph output (VFXComposedParticleOutput) whose shaderGraph field lives on a nested shading sub-object — assigning to a plain Unlit output instead raises WrongOutputShaderGraph. Adds a committed VFX-target shader graph fixture (VfxUnlit.shadergraph, Support VFX Graph enabled) + EditMode test. Skill v0.20.0. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Move a sticky note from index to toIndex within VFXUI.stickyNoteInfos (the array position is the note's order; StickyNoteInfo has no order field). Mirrors the reorder_block/reorder_parameter shape. Describe's stickyNotes[] reflects the new order. 5-layer: catalog op-list + handler op + skill v0.21.0 + behavioral & contract tests + docs. Fit-to-text is a UI text-measurement with no model-level size -> out of scope headless. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…kiojin#11) insert_template clones every top-level node (context/operator/parameter) of a built-in template into an EXISTING graph (vs create_from_template, which makes a new asset), preserving the template's internal flow + slot links and nested blocks via VFXMemorySerializer.DuplicateObjects, then AddChilds the clones as a new disjoint system. The GraphView's merge path (VFXCopy/VFXPaste) is Controller/View-coupled; this is the model-level equivalent (a template is self-contained, so no boundary inference). Factored a shared ResolveTemplateFile helper (also used by create_from_template). 5-layer: catalog op-list + handler op + skill v0.22.0 + behavioral & contract tests + docs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…kiojin#12) GetVfxSettings now surfaces the VFXManager compute/empty shader refs + runtime-resources ScriptableObject (m_IndirectShader/m_CopyBufferShader/ m_PrefixSumShader/m_SortShader/m_StripUpdateShader/m_EmptyShader/ m_RuntimeResources/m_RenderPipeSettingsPath) as {type,name,assetPath}, and AssignSerialized gained an ObjectReference branch so set writes them by asset path (empty clears). These are usually Unity-managed defaults — surfaced for inspection, overridable when needed. EditMode test does a safe cross-assign round-trip with restore (VFXManager.asset stays clean). Skill v0.23.0. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…kiojin#1) link_slots/unlink_slots gain an `activation:true` flag on the to/target block endpoint that targets the block's special activationSlot (`_vfx_enabled`) instead of an indexed input slot, via a new shared ResolveInputSlot helper. Describe surfaces blocks[].activationSlot (name/valueType/value/links) so link-driven activation is verifiable. Drives a block on/off per particle/frame from any bool graph output, vs set_block_enabled's static toggle. akiojin#1 Blocks now fully complete. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
rename_operator_input (SetOperandName) + reorder_operator_input (OperandMoved, moves the matching input slot so links survive) for cascaded numeric operators (Add/Multiply/Append-style). The operand name drives the input slot name, so describe's existing operators[].inputSlots[].name oracle verifies both. Non-cascaded operators return a clear error. akiojin#4 Operators now fully complete. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
reorder_category moves a whole blackboard category within VFXUI.categories (list position = display order). VFXView's MoveCategory is controller- coupled, so the op replicates it at model level: it lazily syncs the categories list from the params (mirroring VFXViewController) before moving, since headless set_parameter_category only writes the param's category string. Describe gains a top-level categories[] oracle (name/collapsed/order). akiojin#9 Blackboard now complete bar deferred cross-graph copy-paste. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…jin#11) designate_template writes the VFX importer's template metadata (name/category/description + optional icon/thumbnail by path) and the useAsTemplate flag via the package's official VFXTemplateHelperInternal.TrySetTemplateStatic, then persists the importer settings and reimports so the .meta carries the template block. Describe gains a top-level `template` oracle (read back via TryGetTemplateStatic; null when not a template). akiojin#11 Templates now fully complete (sort/favorite stay out-of-scope UI). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ojin#15) Compose-only proofs over the existing m_HLSLCode/m_ShaderFile ops — the package HLSLParser already resolves VFXSampler2D→Texture2D and StructuredBuffer<T>→GraphicsBuffer slots (s_KnownTypes) and #include directives (s_IncludeParser) when source is set, so no new code is needed. Adds committed fixture HLSLMain.hlsl (which #includes the existing HLSLInclude.hlsl) + two EditMode tests asserting the resolved slot types and clean recompile. akiojin#15 Custom HLSL now complete — clears the last niche Pass-2 tail box. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…iojin#17) New top-level tool (sibling of vfx_runtime/vfx_settings) for the SDF Bake Tool's programmatic path. Uses the package's PUBLIC MeshToSDFBaker (construct → BakeSDF → read back the 3D SdfTexture RenderTexture → save as a Texture3D asset). The package's own SaveToAsset is internal and goes through an interactive SaveFilePanel (headless blocker), so we do our own AsyncGPUReadback (per-layer, to capture all depth slices) + AssetDatabase.CreateAsset at the caller's path. Params: meshPath, outputPath, maxResolution, center/size (default mesh bounds), signPassCount, threshold, sdfOffset, overwrite. Guards on SystemInfo.supportsComputeShaders. Full 5-layer: catalog + host case + handler + EditMode tests (committed Cube.obj fixture; test Asserts.Ignore without compute) + skill v0.29.0 + docs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ed contexts Block ops (add_block/set_block_setting/set_block_enabled/remove_block/ reorder_block/move_block/duplicate_block) resolved a context only by contextType, always hitting the first match — so a graph with two same-typed contexts (e.g. two Spawners across two systems) could not be targeted unambiguously. Surfaced by the agent-mode eval: a clean-room agent had to destructively delete+rebuild a Spawner to add a block to the right one. Add an optional contextIndex (absolute position in the context list) via a shared ResolveBlockContext helper, threaded through every block op and the link-node resolver (link_slots/unlink_slots/set_slot_value block/context endpoints). move_block/duplicate_block also accept toContextIndex for the destination. contextType stays first-match (back-compat). LocateBlock is now parameter-based so it honors the index. 5-layer: tool_catalog schema (contextIndex/toContextIndex) + handler + EditMode test (ApplyAddBlock_ContextIndexTargetsSameTypedSiblingContext; 150/0 green) + skill v0.30.0 (contextIndex + Invocation flags note). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the agent-mode eval that proves an agent can BUILD the right Visual Effect Graph from a natural-language prompt using ONLY the unity-vfx-graph skill — the project's real thesis. It is an on-demand DISCOVERY tool (surfacing skill/docs ergonomics gaps), not a CI gate; the runner always exits 0. Outcome layer (new): tests/fixtures/vfx-agent-eval/benchmark.jsonl (7 tasks, each with an NL prompt + a vfx_describe_graph oracle grader) + scripts/vfx-agent-eval/vfx-agent-eval.sh (runner + embedded Python scorer whose graders are ported verbatim from the manual proof) + README + an opt-in nightly workflow gated behind secrets.VFX_AGENT_EVAL_AGENT_CMD (needs a Unity-equipped runner; skips cleanly when unset). The runner has two modes: --agent-cmd (full clean-room-agent loop) and probe-only (grade hand-built probes). Routing layer: 13 VFX rows (SR-128..SR-140) appended to the existing skill-routing benchmark, covering vfx_describe_graph/list_library/apply/runtime/ settings/bake_sdf incl. the new contextIndex addressing. Mechanism was validated manually first (clean-room subagent per task, oracle- graded) — all 7 tasks PASS. See VFX-Graph-LOG.md SD/SD.2 in the project root. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bulletproofing pass: 8 more clean-room agent tasks (VAE-008..015) spanning
capabilities the first 7 didn't touch — Custom HLSL operator, GPU-event chain,
blackboard categories, instancing, custom attributes, flipbook output, sticky
notes, system naming. All 8 oracle-graded PASS (15/15 total). Added the 8 tasks
to tests/fixtures/vfx-agent-eval/benchmark.jsonl and 8 matching graders to the
scorer (ported verbatim from the oracles used to grade the run).
Discovery payoff: the flipbook task surfaced a real SKILL.md bug — it listed
uvMode:"FlipbookBlend", but the live UVMode enum rejects it ("Could not convert
'FlipbookBlend' to UVMode"); frame-blending is the separate flipbookBlendFrames
bool. Fixed the doc to list only "Flipbook" and clarify the blend/motion-vector
bools. Also added a cross-reference on add_parameter noting the blackboard
category lazy-population behavior. Skill v0.31.0.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… tasks) Two more clean-room agent tasks (VAE-016 particle strip, VAE-017 mesh output), both oracle-graded PASS -> 17/17. These were thought to need a new contextModelType oracle, but describe already surfaces each context's C# class as contexts[].type, so they grade off that (VFXComposedParticleStripOutput / VFXMeshOutput). Added 2 graders to the scorer. Finding (both agents): no convert/swap-context-type op exists — changing a system's output type means remove_context + add_context + link_flow. Documented that recipe in SKILL.md plus the four output type class names for verification (VFXPlanarPrimitiveOutput / VFXComposedParticleStripOutput / VFXMeshOutput / VFXStaticMeshOutput). Skill v0.32.0. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the `simulate` runtime op and the runtime eval tier the ROADMAP scoped. simulate op: vfx_runtime gains `simulate` (params deltaTime=0.05, steps=1) → VisualEffect.Simulate(float,uint), advancing a live effect's sim headlessly. 5-layer: handler case + tool_catalog schema (deltaTime/steps) + description + docs/tools.md + skill v0.33.0 + a PlayMode test (Runtime_SimulateOp_AdvancesSpawnUntilBurstAlive) that drives the op per frame and asserts a 15-particle burst comes alive (8/8 PlayMode green). Runtime eval tier: tests/fixtures/vfx-agent-eval/runtime-benchmark.jsonl (3 tasks: set exposed float, set exposed texture, override initialEventName) + scripts/vfx-agent-eval/vfx-runtime-eval.sh (builds a rig, lets a clean-room agent drive vfx_runtime, grades vfx_runtime get_state). Validated clean-room: 3/3 PASS. Committed fixture RuntimeRig.vfx (exposed Rate float + Tex texture, both wired so they survive into the runtime sheet). Scope: value round-trips are deterministic in the editor (no play mode). SPAWN behavior (aliveParticleCount>0) needs play mode + Camera + per-frame simulate-with-yields, so it is graded C#-side by the PlayMode test, not the raw-driving agent (documented in the skill + README + runtime-checklist). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sync the 78-commit VFX branch onto upstream v0.12.0. Conflicts resolved: - tool_catalog.rs: keep upstream get_index_status + our 6 vfx_* tools; TOOL_NAMES parity count -> 136 (130 upstream + 6 vfx). - UnityCliBridgeHost.cs: take upstream's router-based dispatch (inline command switch removed upstream). - BridgeCommandRouter.cs: register the 6 vfx_* handlers in the new registry (satisfies remote_tools_have_unity_bridge_dispatch_cases). - docs/tools.md: tool totals -> 136 / 125 runtime / 11 reference (satisfies tools_doc_matches_registered_catalog). - manifest.json + manifest.unity6.json: keep VFX Graph, bump 17.3.0 -> 17.4.0 to match upstream URP/SRP 17.4.0; Unity editor -> 6000.4.11f1. - packages-lock.json: reset to upstream's consistent 17.4.0 base; Unity re-resolves VFX Graph 17.4.0 on first project open. Rust gates: cargo fmt clean; tool_catalog 37/37 incl. all 3 new parity gates. Remaining test/clippy noise is upstream-owned + Windows-only (reference-cache fixtures, /etc/passwd path, platform-gated imports) and is green on upstream's Linux CI. Unity-side validation (EditMode/PlayMode against VFX Graph 17.4 reflection) pending an open editor on 6000.4.11f1 — handler is reflection-only so any 17.3->17.4 internal-API drift surfaces there as red tests. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Unity 6000.4.11f1 re-resolved packages-lock.json on first open, adding the builtin com.unity.visualeffectgraph 17.4.0 entry (consistent with URP/core/ shadergraph 17.4.0). Validated against this resolution: VfxGraphHandlerTests 150/150 EditMode + VfxRuntimePlayModeTests 8/8 PlayMode green — no internal-API reflection drift from 17.3 -> 17.4. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Remove the agent-mode outcome eval (scripts/vfx-agent-eval/, tests/fixtures/vfx-agent-eval/) and its nightly workflow. Upstream has a routing eval only (tests/fixtures/skill-routing/) — no outcome/agent-mode harness precedent — so this stays fork-only. The VFX skill-routing rows in tests/fixtures/skill-routing/benchmark.jsonl (which extend the upstream benchmark) remain. No vfx_* op, handler, skill, test, or doc is affected. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
arghhhhh
force-pushed
the
vfx-graph-control-pr
branch
from
June 25, 2026 14:40
8b8e7bb to
89fd6dd
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Effect Graph to unity-cli, via reflection over the internal
UnityEditor.VFX.*API (the entire authoring API is
internal) plus the public runtime API.vfx_*tools covering 16 VFX capabilities + an SDF-bake utility, eachas a full 5-layer vertical (tool catalog -> C# handler -> skill -> tests -> docs).
Changes
src/tooling/tool_catalog.rs: registers 6 remote tools (vfx_apply,vfx_describe_graph,vfx_list_library,vfx_runtime,vfx_settings,vfx_bake_sdf);TOOL_NAMESparity count 136.UnityCliBridge/.../Editor/Handlers/VfxGraphHandler.cs: reflection-basedauthoring + runtime handler (describe oracle, op dispatcher, SDF bake).
UnityCliBridge/.../Editor/Core/BridgeCommandRouter.cs: 6["vfx_*"]registryentries dispatching to
VfxGraphHandler.UnityCliBridge/.../Tests/Editor/Handlers/VfxGraphHandlerTests.cs&.../Tests/PlayMode/Vfx/VfxRuntimePlayModeTests.cs: EditMode + PlayMode suites..claude-plugin/plugins/unity-cli/skills/unity-vfx-graph/: skill v0.33.0.docs/tools.md:vfx_*rows + updated tool totals.UnityCliBridge/Assets/VfxFixtures/: committed.vfx/shadergraph/HLSL/meshfixtures;
ProjectSettings/VFXManager.asset.Testing
cargo fmt --all -- --check— cleancargo clippy --all-targets -- -D warnings— 0 warningscargo test --all-targets -- --test-threads=1— 418 passed, 0 failedcargo run -- skills lint --severity error— 16 skills, 0 violationsdotnet test lsp/Server.Tests.csproj— 42 passed, 0 failedcargo llvm-cov --all-targets --fail-under-lines 90 -- --test-threads=1— 92.13%dotnet test ... /p:Threshold=90 /p:ThresholdType=line— LSP 99.52%VfxGraphHandlerTests150/0, PlayModeVfxRuntimePlayModeTests8/0, clean recompile (errorCount: 0)Closing Issues
None
Related Issues / Links
Checklist
cargo clippy,cargo fmt;svelte-checkN/A)docs/tools.md,unity-vfx-graphskill)Context
This lands 78 VFX commits as a first-class contribution. The branch was synced
onto upstream v0.12.0: 3 conflicts resolved (
tool_catalog.rscount, the newBridgeCommandRouterdispatch, manifests), the VFX handlers migrated into thenew router registry, and the project baseline moved to Unity 6000.4.11f1 / VFX
Graph 17.4.0 (SRP packages are editor-pinned builtins). The full EditMode +
PlayMode suites pass against 17.4 — no internal-API reflection drift.
Risk / Impact
vfx_*tools, the bridge router gains 6entries, the embedded Unity package gains a handler + tests, project baseline
Unity/VFX Graph bumped to 6000.4.11f1 / 17.4.0. No existing tool behavior
changes.
Notes
the bridge still compiles where VFX Graph is absent (e.g. the Unity 2022
manifest variant).
scripts/vfx-agent-eval/,tests/fixtures/vfx-agent-eval/) is kept fork-only and is not in this branch;the VFX skill-routing rows are included.
gwt-speclabel to gwt-spec: vfx-graph-control (Unity Visual Effect Graph authoring + runtime control) #226 — it was opened from a fork accountwithout label permissions, but its title + body markers identify it as a
gwt-spec domain SPEC.