11constexpr size_t kMaxPayloadDepth = 32;
12constexpr size_t kMaxPayloadElements = 100'000;
20Result<void> EditorCommandService::checkExecutionPolicy(
const CommandDescriptor&
descriptor,
23 const HostProfile& profile)
const {
24 if (!profile.allowsCommand(
id))
25 return eve::editing::failed<void>(EditorStatus::Rejected,
RuleId(
"editor.command.profile-denied"),
26 "Command is unavailable in this host: " +
id.
value());
27 if (!profile.hasFeatures(
descriptor.requiredFeatures))
28 return eve::editing::failed<void>(EditorStatus::Rejected,
RuleId(
"editor.command.feature-denied"),
29 "Command requires features unavailable in this host: " +
id.
value());
31 return eve::editing::failed<void>(EditorStatus::Rejected,
RuleId(
"editor.command.automation-denied"),
32 "Command is not available to automation: " +
id.
value());
33 if (!
payload.isWithinLimits(kMaxPayloadDepth, kMaxPayloadElements, profile.maxPayloadBytes()))
34 return eve::editing::failed<void>(EditorStatus::Rejected,
RuleId(
"editor.command.payload-limit"),
35 "Command payload exceeds host limits: " +
id.
value());
36 return eve::editing::applied<void>();
42 return error(EditorStatus::Rejected,
"editor.command.invalid-registration",
43 "Command id, owner module and handler are required");
45 auto it = std::find_if(commands_.begin(), commands_.end(),
46 [&](
const Registration& entry) { return entry.descriptor.id == descriptor.id; });
47 if (it != commands_.end()) {
48 if (!replace || it->descriptor.ownerModule !=
descriptor.ownerModule)
49 return error(EditorStatus::Rejected,
"editor.command.duplicate",
50 "Command is already registered: " +
descriptor.id.value());
51 *it = {std::move(
descriptor), std::move(
handler), {}, {}, nextRegistrationGeneration_++};
53 commands_.push_back({std::move(
descriptor), std::move(
handler), {}, {}, nextRegistrationGeneration_++});
56 return eve::editing::applied<EditorValue>(
EditorValue{});
64 return error(EditorStatus::Rejected,
"editor.command.invalid-registration",
65 "Command id, owner module, planner and executor are required");
66 auto it = std::find_if(commands_.begin(), commands_.end(),
67 [&](
const Registration& entry) { return entry.descriptor.id == descriptor.id; });
68 if (it != commands_.end()) {
69 if (!replace || it->descriptor.ownerModule !=
descriptor.ownerModule)
70 return error(EditorStatus::Rejected,
"editor.command.duplicate",
71 "Command is already registered: " +
descriptor.id.value());
72 *it = {std::move(
descriptor), {}, std::move(planner), std::move(executor),
73 nextRegistrationGeneration_++};
75 commands_.push_back({std::move(
descriptor), {}, std::move(planner), std::move(executor),
76 nextRegistrationGeneration_++});
79 return eve::editing::applied<EditorValue>(
EditorValue{});
83 auto it = std::find_if(commands_.begin(), commands_.end(), [&](
const Registration& entry) {
84 return entry.descriptor.id == id && (ownerModule.empty() || entry.descriptor.ownerModule == ownerModule);
86 if (it == commands_.end())
return false;
93 if (ownerModule.empty())
return 0;
94 const size_t before = commands_.size();
95 std::erase_if(commands_, [&](
const Registration& entry) {
return entry.descriptor.ownerModule == ownerModule; });
96 const size_t removed = before - commands_.size();
102 if (commands_.empty())
return;
108 auto it = std::find_if(commands_.begin(), commands_.end(),
109 [&](
const Registration& entry) { return entry.descriptor.id == id; });
110 return it == commands_.end() ? nullptr : &it->descriptor;
114 const auto it = std::find_if(commands_.begin(), commands_.end(),
115 [&](
const Registration& entry) { return entry.descriptor.id == id; });
116 return it != commands_.end() &&
static_cast<bool>(it->planner) &&
static_cast<bool>(it->planExecutor);
120 std::vector<CommandDescriptor> result;
121 result.reserve(commands_.size());
122 for (
const auto& entry : commands_)
124 result.push_back(entry.descriptor);
130 auto it = std::find_if(commands_.begin(), commands_.end(),
131 [&](
const Registration& entry) { return entry.descriptor.id == id; });
132 if (it == commands_.end())
133 return error(EditorStatus::NotFound,
"editor.command.not-found",
"Command is not registered: " +
id.
value());
135 return error(EditorStatus::Failed,
"editor.command.missing-profile",
"Command context has no host profile");
136 if (
auto policy = checkExecutionPolicy(it->descriptor,
id,
context.source,
payload, *
context.profile);
141 return error(EditorStatus::Unsupported,
"editor.command.requires-plan",
142 "Command must be planned before it can be executed: " +
id.
value());
145 }
catch (
const std::exception& exception) {
146 return error(EditorStatus::Failed,
"editor.command.handler-exception",
147 "Command handler failed: " + std::string(exception.what()));
149 return error(EditorStatus::Failed,
"editor.command.handler-exception",
150 "Command handler failed with an unknown exception");
158 auto it = std::find_if(commands_.begin(), commands_.end(),
159 [&](
const Registration& entry) { return entry.descriptor.id == request.id; });
160 if (it == commands_.end())
161 return failure(EditorStatus::NotFound,
"editor.command.not-found",
162 "Command is not registered: " +
request.id.value());
163 if (
auto policy = checkExecutionPolicy(it->descriptor,
request.id,
request.source,
request.payload, profile);
167 return failure(EditorStatus::Conflict,
"editor.command.revision-conflict",
168 "Expected revision does not match the captured context");
170 return failure(EditorStatus::Unsupported,
"editor.command.planning-unsupported",
171 "Command does not provide a planning handler");
177 result.
value().baseRevision =
request.context.targetRevision;
178 if (result.
value().targetGeneration == 0)
179 result.
value().targetGeneration =
request.context.targetGeneration;
180 result.
value().commandGeneration = it->generation;
183 if (result.
value().id.empty())
184 result.
value().id =
PlanId(
request.id.value() +
".plan." + std::to_string(++planSequence_));
187 }
catch (
const std::exception& exception) {
188 return failure(EditorStatus::Failed,
"editor.command.planner-exception", exception.what());
190 return failure(EditorStatus::Failed,
"editor.command.planner-exception",
191 "Command planner failed with an unknown exception");
202 return failure(EditorStatus::Rejected,
"editor.command.invalid-plan",
203 "Plan identity or target does not match the request");
204 if (
plan.baseRevision !=
request.context.targetRevision)
205 return failure(EditorStatus::Conflict,
"editor.command.stale-plan",
206 "Plan was produced for another target revision");
207 if (
plan.targetGeneration !=
request.context.targetGeneration)
208 return failure(EditorStatus::Conflict,
"editor.command.stale-target-generation",
209 "Plan was produced for another target lifetime");
211 return failure(EditorStatus::Conflict,
"editor.command.revision-conflict",
212 "Expected revision does not match the captured context");
213 auto it = std::find_if(commands_.begin(), commands_.end(),
214 [&](
const Registration& entry) { return entry.descriptor.id == request.id; });
215 if (it == commands_.end())
216 return failure(EditorStatus::NotFound,
"editor.command.not-found",
217 "Command is not registered: " +
request.id.value());
218 if (
plan.commandGeneration != it->generation)
219 return failure(EditorStatus::Conflict,
"editor.command.stale-registration",
220 "Command registration changed after planning");
221 if (
auto policy = checkExecutionPolicy(it->descriptor,
request.id,
request.source,
request.payload, profile);
225 return failure(EditorStatus::Rejected,
"editor.command.plan-input-mismatch",
226 "Plan payload or source differs from the validated request");
227 if (!it->planExecutor)
228 return failure(EditorStatus::Unsupported,
"editor.command.execution-unsupported",
229 "Command does not provide a plan executor");
232 }
catch (
const std::exception& exception) {
233 return failure(EditorStatus::Failed,
"editor.command.executor-exception", exception.what());
235 return failure(EditorStatus::Failed,
"editor.command.executor-exception",
236 "Command executor failed with an unknown exception");
const GltfImportRequest & request
wgpu::PopErrorScopeStatus status
const UnitySourceAsset & source
const VegetationPresetContext & context
Move-only operation result carrying either a value or Status.
const T & value() const &
Borrow the value from a const lvalue after checking success.
bool ok() const noexcept
Whether this result represents a non-failure outcome.
UUID-backed identifier adapter for legacy textual boundaries.
Deterministic owning value tree shared by authoring hosts.
size_t unregisterOwner(const std::string &ownerModule)
Remove every command registered by a module.
Result< EditorValue > execute(const CommandId &id, const CommandContext &context, const EditorValue &payload) const
Execute a command after payload and profile checks.
bool supportsPlanning(const CommandId &id) const
Return whether the registered command requires plan/commit execution.
const CommandDescriptor * find(const CommandId &id) const
Return command metadata or nullptr when it is not registered.
Result< EditorValue > registerCommand(CommandDescriptor descriptor, EditorCommandHandler handler, bool replace=false)
Register one command.
std::vector< CommandDescriptor > commands(const HostProfile &profile) const
Return descriptors visible to a host profile.
Result< EditorValue > registerPlannedCommand(CommandDescriptor descriptor, EditorCommandPlanner planner, EditorCommandPlanExecutor executor, bool replace=false)
Register a side-effect-free planner and explicit plan executor.
void clear()
Remove all registrations. Primarily useful for isolated tests.
Result< TransactionReceipt > executePlan(const CommandRequest &request, const CommandPlan &plan, const HostProfile &profile) const
Execute a plan only when its lifetime, registration, policy and input bindings remain valid.
Result< CommandPlan > plan(const CommandRequest &request, const HostProfile &profile) const
Produce a side-effect-free plan bound to the target, command generation, payload and source.
bool unregisterCommand(const CommandId &id, const std::string &ownerModule={})
Remove one command, optionally requiring a matching owner.
Upper capability boundary for one editor host.
bool hasFeatures(HostFeature required) const
True when features.
bool allowsCommand(const CommandId &id) const
Allows command.
std::function< Result< TransactionReceipt >(const CommandRequest &, const CommandPlan &)> EditorCommandPlanExecutor
eve::editing::Value EditorValue
Compatibility alias for the UI-independent authoring value tree.
eve::editing::PlanId PlanId
eve::editing::CommandId CommandId
std::function< Result< EditorValue >(const CommandContext &, const EditorValue &)> EditorCommandHandler
eve::editing::RuleId RuleId
eve::editing::CommandSource CommandSource
Compatibility alias for the UI-independent authoring command source.
std::function< Result< CommandPlan >(const CommandRequest &)> EditorCommandPlanner
eve::StatusCode EditorStatus
StatusCode
Stable outcome category for an operation.
CommandRequest public API.
Immutable execution context captured for a synchronous command.
Presentation and execution metadata for a registered command.