Command API Contract
Ownership and scope
core/command/ owns Perastage's GUI-independent semantic command contract.
perastage_command_core contains typed requests, structured results, execution
context abstractions, and the mutation transaction. The separate
perastage_command_serialization target owns JSON output and is the only one of
the two targets that uses the JSON dependency.
Human command text remains an adapter rather than the semantic API. The focused
perastage_command_text_parser target under core/command/ owns the neutral,
GUI-independent Console syntax model and parser. It may consume Command Core's
structured diagnostic contract, while perastage_command_core never depends
on the text parser. Raw Console strings and parsed adapter syntax are therefore
not semantic Request objects.
The embedded Console consumes this neutral parser. The separate
perastage_command_transform_text_adapter target depends on both the parser
and semantic transform boundaries; the parser remains independently usable by
syntax-only consumers and does not depend on scene execution. The adapter
converts position values from the Console grammar's meters to semantic
millimeters, then invokes perastage_command_transform. The Console owns
project-context acquisition, structured-result presentation, and GUI refresh;
it does not own reusable transform or selection algorithms. The focused
perastage_command_selection target owns UUID-based selection updates and the
compatible selection-only clear operation. The
perastage_command_selection_text_adapter target resolves Console fixture IDs
and truss unit numbers against an explicitly supplied scene; lookup remains
outside the syntax-only parser.
Semantic requests and identifiers
A request has a stable commandId and an ordered list of named, typed
arguments. Values are limited to booleans, signed 64-bit integers, doubles,
strings, and homogeneous lists of those numeric or string types. Requests do
not contain raw Console input or a recursive JSON value tree.
Machine-facing identifiers are stable technical English and are never localized:
- command IDs use lowercase ASCII letter-led alphanumeric segments separated
by dots, such as
scene.transform.position; - argument IDs use lowercase ASCII letter-led snake case, without leading, trailing, or consecutive underscores;
- diagnostic codes use dotted lowercase ASCII technical identifiers; each non-empty segment may use snake case without leading, trailing, or consecutive underscores.
Concrete identifiers are introduced with concrete commands; Command Core does not maintain a speculative registry.
Scene selection commands
scene.selection.update uses an explicit object kind and UUID for every
semantic object reference. Its stable arguments are target_kind,
preserve_existing, and aligned operation_kinds, object_kinds, and
object_uuids lists. Updates preserve UUID ordering, ignore repeated adds,
remove matching UUIDs, and leave all non-target selection categories intact.
They set selectionChanged only for an actual ordered selection change and do
not create Undo state or dirty the project.
Console f and t numeric IDs are adapter syntax rather than semantic
identities. Fixture commands retain the prior fixture selection; matching the
historical Console behavior, each truss command starts its target category
empty before applying its ordered operations. Ranges and mixed add/remove
operations are resolved in parser order. Missing numeric IDs remain successful
no-ops with an information diagnostic. An ADD with duplicate fixture IDs or
truss unit numbers previously selected whichever unordered-map entry appeared
first; because that behavior was unsafe and nondeterministic, the adapter emits
scene.selection.numeric_id_ambiguous and skips the ADD. REMOVE instead
resolves against the ordered current target selection and removes every
selected object whose numeric ID matches, even when unselected scene objects
share that ID. Both operations produce only stable object-kind and UUID
references for semantic execution.
scene.selection.clear retains the narrowly characterized embedded Console
behavior: it clears fixtures, trusses, and scene objects but not supports. It
also retains the legacy cli clear Undo publication and dirty-state effect,
including when those three categories were already empty. The latter case is
reported as scene.selection.clear_noop; the Undo publication is compatibility
behavior rather than evidence of scene-content mutation. No general scene
reset behavior is implied.
Selection and clear commands mutate neither scene resources nor transforms. Frontend publication synchronizes tables and viewers after each command. Active-project selection-only publication updates all four tables and the 2D/3D/Layout viewers without reloading scene content, persisting automatic fixture colors, or publishing Undo/dirty state. Because the Console publishes the resulting selection before executing the next parsed command, a transform later in the same line observes the selection produced by every preceding command.
Scene transform commands
The focused perastage_command_transform production target implements the
stable operations: scene.transform.position, whose component values and
ranges are explicitly millimeters, and scene.transform.rotation, whose
component values and ranges are explicitly degrees.
scene.transform.batch applies ordered position and rotation components to
explicit typed UUID targets through the same component and hierarchy helpers.
Its stable flat arguments are seven non-empty, equally sized lists, in order:
target_kinds, target_uuids, component_kinds, axes, values, modes,
and spaces. All are string lists except values, a double list interpreted
as millimeters for position and degrees for rotation. Kinds are fixture,
truss, support, scene_object, and group; components are position or
rotation, axes are x, y, or z, modes are absolute or relative, and
spaces are world or local. Each index is one component, and repeated targets
retain request order. Absolute components retain the existing world-coordinate
position and Euler rotation semantics; the space parameter affects relative
components exactly as it does for existing selection transforms.
The complete batch is validated and applied to a preview scene before one
MutationTransaction publishes its changes. Missing or mismatched identities,
malformed lists, invalid tokens, non-finite values, or failed execution reject
the whole operation. A semantic no-op publishes no Undo or dirty state. A
changed batch records one Undo snapshot, dirties once, and requests one frontend
scene refresh. Selection is preserved exactly. Explicit child targets remain
exact and are never promoted to groups; an explicitly targeted GroupObject uses
the existing descendant world/local synchronization. Batch transforms contain
no arrangement or layout calculation.
A typed selection transform contains ordered X/Y/Z components. Each component retains its
single value or interpolation range, relative/absolute mode, explicit world
or local transform space, and the legacy group-pivot marker. An optional
pivot is an explicit XYZ millimeter triplet. The deterministic generic
Request projection uses axis-specific _millimeters or _degrees arguments
plus _relative, _space, and _group arguments, followed by pivot_mm when
present.
Selection transform execution uses only the scene and current ObjectSelection supplied through
ExecutionContext, plus an explicitly supplied InteractiveTransformPolicy.
Relative transforms use the existing transform-space helpers. Absolute
rotation retains the established Console Euler-axis mapping and preserves
origin and scale. Group rotation uses either its explicit pivot or the center
of the current effective targets' world origins.
Every semantic transform runs through one MutationTransaction. A changed
multi-axis selection command publishes one pre-mutation snapshot with the compatible
cli pos or cli rot Undo label. A tolerance-equivalent no-op succeeds with
an information diagnostic and publishes nothing. Publication failure restores
the exact scene and selection captured before execution.
The application-side mutation host converts neutral object selection to the
project selection snapshot and publishes it through PushUndoSnapshot; it
does not refresh GUI state. Frontends use MutationSummary::sceneChanged to
decide whether to refresh tables or viewers.
There are two Command mutation contexts. The embedded Console obtains the
active GUI scene and publishes each semantic mutation through application Undo
and dirty-state services. The external perastage-cli scene workflow instead
owns an isolated scene read from an explicitly named MVR, uses a headless
ProjectMutationHost with no persistent Undo stack, and publishes only to a
different, explicitly named output MVR after every command succeeds. Both
contexts use this same text processor and typed selection/transform Commands.
The local live CLI adapter executes this same text processor against the active
application scene and selection through GuiProjectMutationHost. Typed
live execute scene.selection.update --args '<JSON>' calls the semantic
selection Command directly, with the stable arguments listed above, and
supports fixtures, trusses, supports, and scene objects. Typed
live execute scene.transform.batch --args '<JSON>' invokes the batch Command
with its aligned-list contract. These are the two allowlisted structured
execute operations; this route is not an unrestricted semantic executor. It preserves the
Command's validation-before-mutation behavior, ordered replace/add/remove
semantics, and selection-only mutation summary. Typed selection does not pass
through Console grammar.
The GUI-independent live-request executor composes the versioned envelope with
an explicit ExecutionContext; the loopback transport contains no scene logic.
Schema version 1 accepts an additive optional arguments JSON object.
operation: "execute" uses the stable operation ID in value; legacy
operation: "command" retains text in value. Queries use their stable ID
and optional structured arguments. Typed execution responses retain the
success, records, and parse_diagnostics wrapper, with an empty parser
diagnostic list for semantic execution. Invalid identities are rejected before
selection changes and semantic failure gives CLI exit code 4.
The application adapter returns mutations through the existing main-window
refresh boundary, choosing scene-content or selection-only refresh from the
Command mutation summary, and cancels pending GUI dispatch safely during
shutdown.
OSC adapter
The initial OSC 1.0 endpoint listens only on IPv4 127.0.0.1, UDP port
49156. Datagrams are limited to 4096 bytes. Bundles, malformed padding,
trailing data, and type tags outside the supported i, f, s, T, and F
set are rejected. This is the focused OSC 1.0 subset required by the adapter,
not support for every OSC 1.0 data type. The endpoint does not use Console text
parsing and logs the existing deterministic Command JSON result after
execution.
The deliberately small address mapping is:
/perastage/selection/clearwith no arguments;/perastage/transform/positionwith OSC tags matching,sNBsB, whereNisiorfand eachBisT,F, ori. Arguments are axis (x,y, orz), millimetre value, relative boolean, space (worldorlocal), and group boolean. Integer booleans accept only0and1;/perastage/transform/rotationwith the same tag pattern and order, with the value interpreted in degrees.
Each transform message carries exactly one axis and one value. Consequently,
OSC exposure of the semantic transform operations is partial: ranges,
multi-axis atomic transforms, and explicit rotation pivots remain unavailable.
Selection clear is fully exposed. Parsed messages execute directly through
ExecuteClear or semantic transform Execute, using the active project's
normal Undo, dirty-state, transform-policy, selection, and GUI refresh path.
Higher-level scene tools
The perastage_command_scene_tools target exposes four stable semantic IDs:
scene.group.createandscene.group.ungrouptake orderedfixture_uuids,truss_uuids,support_uuids, andscene_object_uuids. Creation outputsgroup_uuid; both operations output the four orderedaffected_*_uuidsarrays. Group creation requires at least two distinct typed identities; repeating a UUID within one object kind is ascene.group.duplicate_objectvalidation error rather than an instruction to normalize the request.scene.convert.fixture_to_supporttakes ordered, uniquefixture_uuidsand outputsconverted_uuidspluscleared_motor_reference_uuids.scene.convert.scene_objects_to_trussestakes onesource_scene_object_uuid. Its intentionally explicit scope is every scene object with the source object's existing model identity; it outputsconverted_uuidsandmodel_file.
All identities are stable UUIDs and all arguments are explicit. Conversion is
destructive: it replaces the source node type while preserving UUID, placement,
hierarchy metadata, and the existing Core service's cross-reference behavior.
The commands validate their complete target scope before mutation, update the
semantic selection, and publish exactly one Undo snapshot. Mutation summaries
compare the ordered pre-command and post-command selections, so
selectionChanged is independent from sceneChanged. Same-model truss
conversion rejects the complete request before mutation if any target UUID is
already owned by a truss, and its diagnostic lists every conflicting UUID in
deterministic order. True no-ops publish nothing, and publication failure
restores scene and selection and returns no committed-result outputs.
Main-window handlers now only collect inputs, invoke these commands, present
results, and refresh frontend views.
CMD-440 candidate audit
| Candidate | Existing owner and production caller | Inputs and implicit state | Mutation, determinism, and decision |
|---|---|---|---|
| Group / ungroup | scene_grouping; group and ungroup main-window actions |
Cross-table typed UUID selection; no preference | Mutates GroupObjects, hierarchy, layers and local metadata, then replaces selection with affected UUIDs. Deterministic except newly generated group UUID. Migrated. |
| Add/remove group members | scene_grouping; drag/group interaction paths |
Target group plus interaction-derived membership | The service is reusable, but its current production workflows do not form a standalone semantic command consumer. Deferred rather than creating an unused abstraction. |
| Fixture to Support | scene_node_operations; Convert to Hoist action |
Explicit fixture UUIDs; no preference | Destructively replaces node kind, preserves UUID and hierarchy, clears motor references deterministically, and selects supports. Migrated. |
| Same-model SceneObject to Truss | scene-object truss converter; Convert Scene Objects action | Explicit source UUID; same-model scope is part of the established service | Destructively replaces all matching objects while preserving UUIDs; the adapter sorts converted UUID output and selection for deterministic ordering. Migrated. |
| Whole/selected auto patch | AutoPatcher; Auto Patch action and rider import |
Selection chooses scope; GUI currently supplies implicit default universe/channel | Overwrites DMX addresses and has mature topology/order rules, but the service returns no mutation/result characterization and the importer is a second non-transactional consumer. Deferred pending a focused result boundary rather than guessing change detection in Command Core. |
| Line distribution | fixture_line_distribution; fixture-distribution dialogs and viewport actions |
Ordered fixtures and line/spacing values, but edge extents and points are currently gathered by viewer/dialog workflows | Deterministic fixture transforms, with ordering significant. Deferred because not every exposed GUI variant has neutral geometry ownership; Command Core must not depend on viewer loaders or picking state. |
Grouping previously pushed Undo before attempting the operation and then called Undo for a no-op. The semantic command intentionally produces the same final scene/history state without the transient, meaningless history operation. SceneObject conversion previously left a pre-pushed Undo entry when its source had no valid model; validation now produces no Undo entry.
Results and diagnostics
The four distinct outcomes are success, parse_error, validation_error,
and execution_error. Ordered diagnostics carry a severity (information,
warning, or error), phase (parse, validation, or execution), stable
code, technical-English message, and optional argument ID. Warnings may coexist
with success. A parse error may have no semantic request, represented as a
missing request in C++ and null in JSON.
Mutation summaries report only semantic facts: scene, selection, or project metadata changes; whether an Undo entry was recorded; and whether the project is dirty. Viewer, table, and other presentation refresh decisions belong to the frontend and must not enter this contract.
The optional ordered outputs collection carries the same typed scalar and
homogeneous-array values as request arguments. It is reserved for immediate
machine-readable command results such as created or converted UUIDs, rather
than arbitrary recursive JSON.
Execution and transaction boundary
ExecutionContext explicitly supplies an MvrScene, the shared
scene_grouping::ObjectSelection, and a neutral ProjectMutationHost. A host
publishes one captured pre-mutation scene/selection snapshot under a stable
English Undo label and reports Undo and dirty state. Command Core does not know
about ConfigManager, project-session implementation, panels, or viewers.
MutationTransaction captures scene and selection once. Destruction or an
explicit rollback restores both. A no-op also restores provisional changes and
does not call the host. A real commit calls the host exactly once and copies its
Undo/dirty response into the mutation summary. If publication throws, scene and
selection are restored and the caller converts the exception to an execution
error. Validation and other speculative work therefore cannot publish project
state.
Machine schema
Command JSON has schema_version 1 and the top-level members
schema_version, request, outcome, diagnostics, mutation, and outputs. Arguments
retain their JSON scalar or homogeneous-array types and their in-memory order;
diagnostics also retain order. A parse failure without a request emits
"request": null.
Schema version 1 is additive within its lifetime: consumers must tolerate new object members but can rely on documented members and tokens retaining their meaning. A breaking removal, token change, type change, or semantic change requires a new schema version.