DeclaredOperation
Turns a declared class into the SAME `Operation` value object every projector already reads. This is the derivation half of greenhouse decisions/0212. The rule it implements, in one line: **What changes authority is DECLARED; what a type already says is DERIVED.** So the attributes carry intent — the name, the purpose, what it mutates, whose authority it spends, which argument the human must name — and this class reads the CONSTRUCTOR to derive the input schema and the RETURN TYPE to derive the output one. A `string $name` needs no `['type' => 'string']` written beside it; a PHP enum needs no second list of its own values. ── WHY IT PRODUCES THE OLD VALUE OBJECT AND NOT A NEW ONE ────────────────────────────────────── Because nothing downstream may change. CLI flags, MCP tools, HTTP routes and methods, consent, signatures, the agent's catalogue and `plugins:architecture` all read `Operation` today, and 82 declaration sites across the platform build it by hand. Emitting a different type would fork the ecosystem on the day it landed. Emitting the same one makes the two styles peers: the attribute wins where it exists, and the hand-written form dies by attrition rather than by decree. ── WHY IT RUNS ONCE, AT DECLARATION ──────────────────────────────────────────────────────────── Reflection is not free, and an operation is declared once per process and invoked many times. So every reflective question — the schema, the target, the collaborators `run()` needs, whether they can even be resolved — is asked HERE, and the handler that comes out closes over plain arrays. A request pays nothing for having been declared this way. ── WHAT IT REFUSES TO GUESS ──────────────────────────────────────────────────────────────────── Every refusal below names the class and says what to declare, and every one of them exists because the alternative is a silent lie: - Neither `#[Mutates]` nor `#[Reads]`. Silence is not «harmless»; an author who forgets would ship `mutating: false` on an operation that writes. - A constructor parameter whose type cannot become a schema. Emitting an empty `{}` would tell every surface «anything goes». - Two `#[Target]`s. Two named targets is not a stricter contract, it is an ambiguous one. - A `run()` that needs collaborators with nobody to resolve them — caught at declaration, not on the first request in production.
DeclaredOperation::isDeclared()
public static function isDeclared(string $class): boolDoes this class declare itself an operation? Lets a provider mix both styles in one list.
Parameters
| Name | Type | Description |
|---|---|---|
| $class | string |
DeclaredOperation::from()
public static function from(string $class, ?Closure $resolve = null): Milpa\Command\OperationDerives the operation a declared class describes — once, at declaration.
Parameters
| Name | Type | Description |
|---|---|---|
| $class | class-string | The declaring class. |
| $resolve | (\Closure | null) | `fn (string $type): object` — how `run()`'s collaborators are found. |
Throws
DeclarationException when the declaration cannot be derived without guessing