Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/Operation.php
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,40 @@ public function __construct(
* @var list<string>
*/
public array $citesReceiptsOf = [],
/**
* Whether this operation AMENDS the entity {@see $namedTarget} names — changes, in place, one that
* exists — so that a session's own record can say where that entity came from (greenhouse decisions/0596).
*
* {@see $createsNamedTarget} drew the intent contract's line by the verb: `implement` materialises and is
* not asked, `edit` selects what exists and is. The record drew it elsewhere. Of 34 times a house asked a
* person to confirm an `edit`, 28 were about a class that same session had brought into the house a moment
* before: nothing the person already had was being selected. Correcting what one has just materialised is
* still materialising — the model's interpretive domain; touching what was already there is selecting, and
* stays the human's.
*
* So the target of an operation that declares this is NAMED when the session's record says where it came
* from: the session brought it into the house and it is still what the session left, or the call repairs a
* proposal of that same session. Who decides that is not this class: it is the session's floor, reading
* facts, with no model in the circuit. What the record does not say keeps being asked.
*
* Defaults to `false` — fail-closed. The declaration speaks of a target, so it needs one; and an operation
* cannot both create its target and amend one that exists. Both are refused at declaration.
*/
public bool $amendsNamedTarget = false,
) {
if ($this->amendsNamedTarget && $this->namedTarget === null) {
throw new \InvalidArgumentException(
"Operation '{$this->name}' declares that it amends its named target and names none. The declaration "
. 'speaks of a target: declare namedTarget, or drop it.'
);
}
if ($this->amendsNamedTarget && $this->createsNamedTarget) {
throw new \InvalidArgumentException(
"Operation '{$this->name}' declares that it creates its named target AND that it amends it. One that "
. 'creates its target is never asked about it; one that amends an existing target is asked unless the '
. "session's record says where the target came from. Declare the one this operation does."
);
}
if ($this->citesReceiptsOf !== [] && $this->continues === null) {
throw new \InvalidArgumentException(
"Operation '{$this->name}' names receipts it may cite (" . implode(', ', $this->citesReceiptsOf) . ') '
Expand Down
92 changes: 92 additions & 0 deletions tests/AnOperationSaysItAmendsItsNamedTargetTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

/**
* This file is part of Milpa Command — the Command-as-atom core of the Milpa PHP framework.
*
* (c) Rodrigo Vicente - TeamX Agency — https://teamx.agency <hola@teamx.agency>
*
* @license Apache-2.0
*
* @link https://github.com/getmilpa/command
*/

declare(strict_types=1);

namespace Milpa\Command\Tests;

use Milpa\Command\Operation;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;

/**
* An operation says it AMENDS the target it names — and says nothing it cannot mean (greenhouse decisions/0596).
*
* The intent contract's line was drawn by the verb: an operation that creates its named target is not asked «you did
* not name it», one that selects an existing target is. Of 34 times a house asked a person to confirm an `edit`, 28
* were about a class that same session had brought into the house. So an operation can declare that it amends what
* its target names, and the session's floor reads from that session's own record where the target came from.
*
* The declaration is a statement about a target: it needs one, and it cannot sit beside the statement that the
* operation creates that same target. Both contradictions are refused where they are written, not reconciled by
* whoever reads them later.
*/
#[CoversClass(Operation::class)]
final class AnOperationSaysItAmendsItsNamedTargetTest extends TestCase
{
public function testAnOperationDoesNotAmendUnlessItSaysSo(): void
{
$operation = new Operation('edit', 'Edits a class.', static fn (): array => [], namedTarget: 'class');

self::assertFalse($operation->amendsNamedTarget, 'fail-closed: silence keeps the question');
}

public function testAnOperationThatNamesNoTargetAndSaysNothingIsDeclaredAsBefore(): void
{
$operation = new Operation('list', 'Lists things.', static fn (): array => []);

self::assertNull($operation->namedTarget, 'the control: most operations name no target, and say nothing about one');
self::assertFalse($operation->amendsNamedTarget);
}

public function testAnOperationThatSaysSoCarriesIt(): void
{
$operation = new Operation('edit', 'Edits a class.', static fn (): array => [], namedTarget: 'class', amendsNamedTarget: true);

self::assertTrue($operation->amendsNamedTarget);
self::assertSame('class', $operation->namedTarget);
self::assertFalse($operation->createsNamedTarget, 'amending is not creating');
}

public function testItSurvivesACopyOfTheContract(): void
{
$operation = new Operation('edit', 'Edits a class.', static fn (): array => [], namedTarget: 'class', amendsNamedTarget: true);

$copy = new Operation(...array_replace(get_object_vars($operation), ['description' => 'Edits a class, and says more.']));

self::assertTrue($copy->amendsNamedTarget, 'a host that rebuilds the contract keeps what it declared');
}

public function testAmendingNeedsATargetToSpeakOf(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage("Operation 'edit' declares that it amends its named target and names none");

new Operation('edit', 'Edits a class.', static fn (): array => [], amendsNamedTarget: true);
}

public function testAnOperationCannotBothCreateItsTargetAndAmendOneThatExists(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage("Operation 'edit' declares that it creates its named target AND that it amends it");

new Operation('edit', 'Edits a class.', static fn (): array => [], namedTarget: 'class', createsNamedTarget: true, amendsNamedTarget: true);
}

public function testCreatingAloneIsStillWhatItWas(): void
{
$operation = new Operation('implement', 'Fills a class.', static fn (): array => [], namedTarget: 'class', createsNamedTarget: true);

self::assertTrue($operation->createsNamedTarget);
self::assertFalse($operation->amendsNamedTarget);
}
}
Loading