--- name: author-rhai-tool description: > Create, extend, register, or debug a reusable LunCoSim Rhai tool library for live USD authoring, component linting, inspection, or test support. Use when a task needs a new `name::function(...)` tool, a typed USD plan builder, a read-only requirement report, or a same-session tool registration. Use author-scenario for mission policy and edit-usd-assembly for the assembly workflow that consumes these tools. --- # Author a reusable Rhai tool library A tool library is named, reusable Rhai policy. It is not a second simulator core, a hidden USD writer, or a vehicle-specific Rust feature. A good tool turns a repeated authoring or inspection decision into a deterministic, discoverable function while leaving USD, the document journal, projection, physics, and solver ownership in their existing typed owners. Read the focused contract when implementing one: [`references/tool-authoring-contract.md`](references/tool-authoring-contract.md). For queued pointer/menu calls, follow the canonical [`one-shot ownership contract`](../../docs/architecture/rhai-integration.md#how-to-load--run-a-scenario). ## Choose the tool's home - Put a reusable engine/editor policy in `assets/scripting/tools/.rhai`. The authored `scripting.source.classify` startup policy admits this source as a callable library; it is still a Rhai tool, not a Rust vehicle implementation. Other `.rhai` files, including tests and scenarios, are loaded only by an explicit scene/runtime request or the CLI test path. In an external Twin session, verify admission with `ListToolLibraries` after `/api/ready`; a policy-layer handoff must re-admit standard tools when the classifier returns. Startup admission, Bevy source publication, and runtime preparation are ordered. The runtime keeps the current admitted sources and closes script execution while the required classifier is temporarily unavailable during a policy replacement. - Put a Twin-specific builder, component lint, or requirement helper in `/tools/.rhai`. It is persisted with that Twin and must not leak into unrelated Twins. Persisted library names are portable single file stems; Windows device names, reserved punctuation and trailing dots/spaces are rejected on every host. Literal `#`, `%`, spaces and Unicode are supported; runtime discovery uses typed asset paths to preserve their spelling. - Edit an existing Twin `.rhai` asset through the source Editor: open its Twin-relative path with `OpenTwinSource`, edit the buffer, and persist it with `SaveSourceText`. Use Save & Update when the source needs to be reloaded. The save command only accepts a registered Twin and a file already open in that Editor; do not patch a loaded source file behind its buffer. - Put a one-off mission sequence in a scenario, not a tool library. Use [`author-scenario`](../author-scenario/SKILL.md). - Put continuous control equations in Modelica and generic substrate in Rust. A missing generic typed USD, physics, projection, or solver capability is a capability report, not permission to add a model-specific Rust path. Name the library after its reusable boundary (`assembly_builder`, `component_requirements`), and name functions by intent: `*_plan`, `*_report`, `*_lint`, `*_test`, or `*_query`. Avoid names that encode an implementation detail or a temporary failure. ## Separate the four responsibilities Keep these responsibilities distinct even when they live in one small file: 1. **Plan** — pure calculation of a typed operation list from explicit inputs. A plan must not call `cmd`, mutate a document, mutate ECS, or repair a failed result. It accepts the exact `doc_id`, `edit_target`, paths, and generation needed by its caller. 2. **Apply** — the caller sends the reviewed plan to the existing document owner, normally `assembly_edit::batch` or the proposal/review/commit flow. A domain tool may provide a thin apply convenience only if it still routes through that owner and exposes the acknowledgement and new generation. 3. **Inspect/lint** — read-only queries over composed USD or live runtime state. Return structured facts, errors, warnings, and an `ok` value. A lint must never hide a missing prim, invent a default, or modify the model to make itself pass. 4. **Test** — an authored Rhai observer that samples the live model and emits one bounded verdict. Keep component tests separate from assembly integration tests; use the shared `auto_tests.rhai` assertions rather than private copies. The normal authoring shape is: ```text inspect exact target and generation -> pure component/assembly plan -> typed document batch or reviewed proposal -> projection/readback -> screenshot in the same headful session -> component lint/test -> assembly integration test -> save through the document owner ``` For the common post-edit checkpoint, compose the built-in `editor_workflow::after_edit(doc)` helper after a reviewed apply/commit. It keeps inspect, current projection, document lint, and optional authored save in one explicit result; authored autosave is disabled unless the Twin explicitly sets `usd.editor_autosave = true`. For physics scene evidence, compose `physics_acceptance` instead of adding one-off threshold code to every test. It reads existing runtime facts and leaves solver policy in Rust. ### Native value boundary Use the common engine's native `Vec3`/`Quat` values for repeated geometry, pose, and control math. They are the simulator's `bevy::math::DVec3` and `DQuat`, registered once by `lunco-scripting-rhai-world`; do not define tuple/vector helpers in a tool library. `world_pos3`, `world_forward3`, and `world_rotation_quat` keep the hot path native, and `vadd`/`vsub`/`vscale`/ `vcross`/`vdot`/`vlen`/`norm_squared`/`vnorm`/`qrot` dispatch to Rust for native operands. Constructors and quaternion/Euler conversions reject non-finite or degenerate values with a script error. Use `[x, y, z]`/`[x, y, z, w]` only when a standard USD literal, JSON command or query parameter, telemetry payload, or legacy scenario requires the interchange representation. Call `vec3_array`/`quat_array` explicitly at that boundary. The bridge accepts native vectors in `cmd`, `query`, and `set` and lowers them once; unknown custom values must be rejected, never stringified or changed into `null`. Rhai's built-in scalar math (`sin`, `cos`, `exp`, `sqrt`, `atan(x, y)`, and related functions) is already Rust-backed, so do not shadow those names in a tool. For reusable mechanical/CAD policy, compose `assets/scripting/tools/mechanical_relations.rhai` instead of adding a vehicle-specific checker. Pass native vectors and an explicit tolerance; keep the returned residual/evidence record attached to the caller's SysML requirement or verification identity. The library covers distance, coincidence, plane distance, under/clearance, parallel/perpendicular, collinear/coplanar, mirroring, and plane/axis symmetry, and remains reloadable Rhai policy. Use the shared Rust boundary functions for dynamic values: `f64_from` for the permissive numeric edge, `f64_only` for settings that must be authored as f64, and `array_is`/`map_is`/`string_is` plus native vector predicates for shape checks. Use `sysml_model_is`, `sysml_quantity_is`, and `sysml_enum_is` for SysML wrapper values. Do not compare `type_of(value)` strings for ordinary numeric, array, map, string, or SysML-wrapper dispatch. Keep semantic strings for paths, qualified names, relation labels, and enum literals only. Keep complex Rhai plans readable to the compiler as well as to reviewers. Extract long compound validation conditions and large record construction into named predicates and constructors instead of one deeply nested expression. For `UsdGeomMesh` edits, keep points and topology as numeric arrays until the schema boundary, then use `assembly_builder::mesh_points_update_plan` or `assembly_builder::mesh_geometry_update_plan`. These return ordinary `SetAttribute` operations and preserve the rest of the composed mesh contract; only the USD attribute's canonical literal is serialized as text. Resolve a numerical profile from `numerical_settings.rhai` once per report or solve. Keep length, angle, scalar, time, and solver tolerances as distinct fields. A tool may use a runtime numerical setting for algorithm policy, but a normative requirement tolerance must remain sourced from SysML and appear in the evidence. ### Generic parameter edits Use `assembly_builder::parameter_plan(edit_target, path, parameters)` for a vehicle-independent parameter update. Each entry is `{ name, type_name, value }`, where `value` is the canonical USD literal. The function only validates the exact path and unique typed fields and returns `SetAttribute` operations. Submit those operations with `assembly_edit::batch` or the proposal/review/commit flow. The Inspector's Apply action lowers to the same `ApplyUsdOps` boundary. Rhai may choose which parameters to expose and validate cross-field relationships, but it must not add a second writer, infer targets by name, or encode vehicle policy in a reusable core tool. Modelica parameter meaning and lifecycle stay with the Modelica declaration/compiler; an instance override remains a USD `inputs:` edit. ### AI-readable assembly authoring For an exact composed prim, call `assembly_builder::authoring_context(doc, path, edit_target)` before planning. It returns the document generation and resolved edit target together with the prim, topology, collision envelope, exact mount sockets/occupancy, plug frames, and a small authored-affordance list. Treat every returned path and generation as a checkpoint; do not infer a socket, component, or parent from a leaf name. Use `assembly_builder::place_or_attach_plan` to keep generated intent dry: `attach_component` returns a reviewed AttachSpec for the existing generic owner, while `realign_existing_mount` returns ordinary typed USD operations. After the user or agent reviews the plan, submit `.spec` through `assembly_edit::attach_component` or `.ops` through `assembly_edit::propose`/`review_session`/`commit_proposal`. Do not put this workflow in Rust or create an AI-only writer; Rhai supplies the policy and the existing USD owners supply validation and journalling. For Editor-driven authoring, add `assembly_builder::selected_authoring_context(preview)` as the first discovery call. It accepts `()` for the focused preview or an explicit hidden preview id and fails unless exactly one current, unambiguous prim is selected. It returns the selection identity and generation alongside the normal `authoring_context`; do not replace those exact values with a display name. Use `functional_frame_catalog(doc, edit_target, root_path)` to enumerate authored functional frames and `align_frames_plan(...)` to compute a dry source-frame-to-target-frame placement. Keep the same-parent and rigid-stack constraints visible in the tool result, and send only the returned typed ops through the existing review/journal path. For a reusable component recipe, compose the existing libraries with ordinary Rhai imports through `component_editor::update_context` or `component_editor::selected_update_context`. Its `update_plan` is a thin facade over `assembly_builder::component_bundle_update_plan`; keep the bundle recipe in the owning Twin/model package, return a dry plan, and let `assembly_edit` own proposal, review, commit, generation checks, and journalling. Do not add a Rust component registry or infer a recipe from USD child names. ### Model-authoring facades For a new assembly or scene, prefer the generic `model_authoring` library. It keeps the workflow in Rhai while reusing the existing typed USD owners: - `model_context(doc, root, edit_target)` reads the exact composed subtree and returns paths, references, variants, components, frames, mounts, bodies, joints, colliders, ports, generation, and available actions. - `readiness_report(doc, root, edit_target, policy)` combines only the checks requested for topology, physicality, mounts, connections, controls, and runtime. Missing sections are `not_requested`; failed lookups are errors. - `scene_recipe(doc, edit_target, recipe, parent_generation)` returns dry typed USD ops for references, terrain, cameras, and initial state, plus explicit hand-offs for `waypoint_editor` routes and `assembly_edit::attach_program` programs. - Connections source drops use the same typed program lowering and USD journal. `diagram.drop.plan` chooses parent/name from immutable source facts; its Rhai policy never reads source bytes or authors USD directly. Models-palette drag contracts retain explicitly declared ports. Browser `.mo`/`.rhai` drops do not infer ports; inspect `InspectConnectionDiagram` program facets and then author their port contract through the document tools. - `port_graph(doc, root, edit_target)` discovers standard USD `inputs:`/`outputs:`/`connectors:` endpoints and composed connections. `wiring_plan(doc, edit_target, root, connections, parent_generation)` validates direction and type and returns typed `SetConnection` ops. - `publish_component(doc, root, edit_target, output, provenance)` validates a standalone `kind = "component"` root, `defaultPrim`, schemas, references, and provenance, then returns the normal metadata ops and explicit Save-As command. Review/apply through `assembly_edit`; provenance remains a caller-owned manifest or standard `assetInfo`, not a new LunCo schema. When testing a reusable tool or linter behavior, put the authored fixture in `assets/scenes/tests/` and the observer in `assets/scenarios/tests/`. Exercise the production command/query surface and assert the returned facts in Rhai; do not copy a large USDA string or an observable policy assertion into a Rust unit test. Keep Rust coverage only for a generic mechanism that the public Rhai surface cannot reach. ### Measurement evidence Use the built-in `authoring_measurements` library for reusable dimensional requirements instead of adding a model-specific validator or Rust geometry policy. `requirement_report(doc, requirements)` evaluates every explicit `distance` or collision `extent` rule through `QueryUsdPrim` and retains the exact document, paths, frame, units, expected value, tolerance, method, and source. Its `checks` list is the complete execution record; `findings` contains only failed or unavailable rules. Missing subjects, missing collision bounds, stale document projections, and unsupported units must remain unavailable or failed, never a passing default. Put the positive and negative behavior in a production Rhai scene test when a live stage is required. Pass the exact document id, edit target, and generation returned by the read. These facades do not identify parts by vehicle name, write USDA directly, or hide missing references, endpoints, mounts, or runtime evidence. ## Use an existing tool first Before creating a library, query the live surface with `DiscoverSchema`, `ListToolLibraries`, and `GetToolLibrary`. Search the existing `assets/scripting/tools/` sources and the relevant Twin `tools/` directory. Prefer composing `assembly_edit`, `assembly_builder`, `assembly_audit`, `assembly_ui`, `nurbs`, or an existing generic prelude helper. A new tool is justified when it adds a reusable contract, a missing generic operation composition, or a repeatable requirement/report boundary—not when it merely shortens one call site or hides a rejected command. For a new component, create its requirement contract and test alongside its tool. The assembly tool may orchestrate components, but it must not duplicate their internal geometry or silently become the only place their requirements are checked. ## Register and call a tool Edit the `.rhai` source normally, then register the source in the existing session: ```json { "type": "ExecuteCommand", "command": "RegisterToolLibrary", "params": { "name": "component_builder", "source": "..." } } ``` With an active Twin this command persists the source to `/tools/` and publishes the named module. On the next normal engine-maintenance pass the module is callable as `component_builder::function(...)`; no Rust rebuild is needed for Rhai source changes. Tool dependencies use Rhai's normal import syntax and load when referenced: ```rhai import "assembly_edit" as assembly_edit; ``` The registration command validates the candidate against the production Rhai engine (prelude, host verbs, asset imports, and current tool registry) before persistence. Missing tools and import cycles are reported as registration errors; do not copy a dependency into another library or add a Rust dispatcher. Verify all three layers, in order: 1. `RegisterToolLibrary` acknowledged the exact name and source. 2. `ListToolLibraries`/`GetToolLibrary` show the expected backend and function surface. 3. A minimal call succeeds from the actual execution context (`RunRhai` for a one-shot check or `RunScenario` for a persistent hook). For interaction tools, verify the registered scope as well as callability. Pass the captured Twin flow owner through `RunRhaiToolHook.owner_twin_id` when the UI flow belongs to a Twin. Include an authored replacement case that queues a call, closes its Twin, and proves a same-name tool in the replacement Twin receives no old call. Keep application-tool lifetime checks separate from Twin-flow checks. Discovery is not invocation proof. If the name is listed but a call reports `Module not found`, first allow the same process one update/maintenance pass and confirm the active Twin scope. If it still fails, record it as a bridge capability gap with the command response and do not work around it by pasting the library into every scenario or by adding Rust-specific dispatch. Keep one existing production process for live work. A second binary launch is not a tool test. For a user-visible assembly, the process must remain headful; use `RunRhai`/`RunScenario` against that process and capture the focused Editor preview after the typed operation commits. ## Live USD rules `.rhai` source may be edited and hot-registered. `.usda` source must not be hand-edited for a live Editor task. Do not use `sed`, a generated USDA replacement, `SetDocumentSource`, a raw file writer, or direct ECS mutation to create or repair geometry. Build typed `UsdOp` plans and let the document owner maintain transforms, journal entries, undo/redo, projection, and save output. Use the exact `doc_id`, `edit_target`, absolute prim paths, and inspected `parent_gen`. Group facts that must change together into one atomic batch or reviewed proposal. Let standard schemas own standard concepts; if the typed surface cannot author a required reference list, variant set/block, metadata, or inherited-prim deletion, report the missing generic Rust capability instead of faking it with hidden duplicate geometry or a compatibility alias. A successful command acknowledgement is not visual proof. After projection, query the composed prims and capture/inspect a project-local screenshot. A preflight parse or lint is not runtime proof, and a screenshot is not physics proof. Keep these evidence classes separate in the handover. ## Tool quality gate Before handing off a library, verify: - it compiles and is callable after `RegisterToolLibrary` in the already-running production process; a separate `luncosim --validate` invocation is not runtime evidence for a live tool; - its public functions have explicit inputs, units, return shapes, and `()` or structured error behavior for unavailable data; - repeated calls are deterministic and either idempotent or explicitly reject an already-complete topology; - it uses canonical USD paths and root-qualified `lunco://`/`twin://` asset references, never name-prefix discovery or local FreeCAD authority; - it uses standard USD schemas and typed operations, with no hidden placeholders standing in for deleted parts; - its lints are read-only and its tests assert that data was measured, not only that a hook happened to run; - component tests and assembly tests run in the same requested live session; - public/reference-backed values are labelled separately from Twin assumptions; - any missing Rust capability is documented with evidence, impact, and the proper generic owner. If registration can persist a source while compilation/callability is still unknown, treat that as a Rust UX gap: the command should fail atomically or return structured compile diagnostics and a callable-engine readiness state.