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
8 changes: 7 additions & 1 deletion docs/schemas/becoming-close.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"not": {
"anyOf": [
{ "required": ["error"] },
{ "required": ["message"] }
{ "required": ["message"] },
{ "required": ["origin"] }
]
}
},
Expand Down Expand Up @@ -42,6 +43,11 @@
"message": {
"type": "string",
"description": "Exception message when the chain ended in failure."
},
"origin": {
"type": "string",
"enum": ["input", "runtime"],
"description": "Provenance of a semantic validation failure: 'input' when the first metamorphosis (incoming input) failed validation, 'runtime' when a later metamorphosis failed. Absent for non-semantic failures."
}
},
"additionalProperties": false
Expand Down
25 changes: 24 additions & 1 deletion src/Becoming.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

namespace Be\Framework;

use Be\Framework\Exception\InputSemanticVariableException;
use Be\Framework\Exception\RuntimeSemanticVariableException;
use Be\Framework\Exception\SemanticVariableException;
use Be\Framework\SemanticLog\Context\BecomingCloseContext;
use Be\Framework\SemanticLog\Logger;
use Be\Framework\SemanticLog\LoggerInterface;
use Be\Framework\SemanticVariable\SemanticValidator;
Expand Down Expand Up @@ -51,15 +55,34 @@ public function __invoke(object $input): object
{
$chainId = $this->logger->openChain($input);
$current = $input;
$isFirst = true;

try {
// Being reveals its becoming, then becomes it
while ($nextForm = $this->being->willBe($current)) {
$current = $this->being->metamorphose($current, $nextForm);
$isFirst = false;
}
} catch (Throwable $e) {
// Refine semantic validation failures by their position in the chain:
// the first metamorphosis validates incoming input (input error),
// any later one validates already-validated state (runtime error).
// Callers receive the refined subtype, while the chain-close log keeps
// the original error class (consistent with the inner span) and carries
// the input/runtime distinction as `origin`.
$loggedException = $e;
$origin = null;
if ($e instanceof SemanticVariableException) {
$origin = $isFirst
? BecomingCloseContext::ORIGIN_INPUT
: BecomingCloseContext::ORIGIN_RUNTIME;
$e = $isFirst
? new InputSemanticVariableException($e->getErrors(), $e)
: new RuntimeSemanticVariableException($e->getErrors(), $e);
}

try {
$this->logger->closeChain(null, $chainId, $e);
$this->logger->closeChain(null, $chainId, $loggedException, $origin);
} catch (Throwable) {
// A failure inside error logging must not mask the original
// metamorphosis exception. Swallow the logging error and
Expand Down
18 changes: 18 additions & 0 deletions src/Exception/InputSemanticVariableException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Be\Framework\Exception;

/**
* Thrown when semantic validation fails on the first metamorphosis (input error)
*
* The very first transformation turns the user-supplied input object into its
* first form, validating the data that flowed in from the outside. A failure
* here means the input itself is invalid - an error attributable to the caller.
*
* @see RuntimeSemanticVariableException for failures in later transformations
*/
final class InputSemanticVariableException extends SemanticVariableException
{
}
19 changes: 19 additions & 0 deletions src/Exception/RuntimeSemanticVariableException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Be\Framework\Exception;

/**
* Thrown when semantic validation fails after the first metamorphosis (runtime error)
*
* Once data has passed input validation, any further metamorphosis works on
* already-validated state. A validation failure at this stage signals an
* internal inconsistency in the transformation logic rather than bad input -
* an error attributable to the program itself.
*
* @see InputSemanticVariableException for failures in the first transformation
*/
final class RuntimeSemanticVariableException extends SemanticVariableException
{
}
13 changes: 11 additions & 2 deletions src/Exception/SemanticVariableException.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Be\Framework\SemanticVariable\Errors;
use DomainException;
use Throwable;

use function array_map;
use function count;
Expand All @@ -16,11 +17,19 @@
*
* Wraps multiple semantic validation errors from the SemanticVariable system.
* Provides access to all individual validation failures.
*
* This is the common base type. Depending on where in the metamorphosis chain
* validation failed, {@see Becoming} refines it into a more specific subtype:
* - {@see InputSemanticVariableException} for the first metamorphosis (input error)
* - {@see RuntimeSemanticVariableException} for any later metamorphosis (runtime error)
*
* Catching this base type still catches both subtypes.
*/
final class SemanticVariableException extends DomainException
class SemanticVariableException extends DomainException
{
public function __construct(
private readonly Errors $errors,
Throwable|null $previous = null,
) {
$errorMessages = array_map(
static fn ($exception) => $exception->getMessage(),
Expand All @@ -31,7 +40,7 @@ public function __construct(
? $errorMessages[0]
: 'Multiple semantic validation errors: ' . implode(', ', $errorMessages);

parent::__construct($message);
parent::__construct($message, 0, $previous);
}

/**
Expand Down
12 changes: 12 additions & 0 deletions src/SemanticLog/Context/BecomingCloseContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,25 @@ final class BecomingCloseContext extends AbstractContext implements JsonSerializ

public const string EXIT_ERROR = 'error';

public const string ORIGIN_INPUT = 'input';

public const string ORIGIN_RUNTIME = 'runtime';

/**
* @param string $exit Exit status for the chain (self::EXIT_SUCCESS|self::EXIT_ERROR).
* @param class-string|null $final FQCN of the terminal being, or null if the chain failed.
* @param string|null $error FQCN of the thrown exception, or null on success.
* @param string|null $message Exception message, or null on success.
* @param string|null $origin Provenance of a semantic validation failure
* (self::ORIGIN_INPUT|self::ORIGIN_RUNTIME), or null when
* the failure is not a semantic validation error.
*/
public function __construct(
public readonly string $exit,
public readonly string|null $final = null,
public readonly string|null $error = null,
public readonly string|null $message = null,
public readonly string|null $origin = null,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
) {
}

Expand All @@ -52,6 +60,10 @@ public function jsonSerialize(): array
if ($this->error !== null) {
$payload['error'] = $this->error;
$payload['message'] = $this->message ?? '';

if ($this->origin !== null) {
$payload['origin'] = $this->origin;
}
}

return $payload;
Expand Down
3 changes: 2 additions & 1 deletion src/SemanticLog/Logger.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public function openChain(object $input): string
* `becoming-close.json`, so we refuse to emit one.
*/
#[Override]
public function closeChain(object|null $final, string $openId, Throwable|null $exception = null): void
public function closeChain(object|null $final, string $openId, Throwable|null $exception = null, string|null $origin = null): void
{
if ($openId === '') {
return;
Expand All @@ -92,6 +92,7 @@ public function closeChain(object|null $final, string $openId, Throwable|null $e
exit: BecomingCloseContext::EXIT_ERROR,
error: $exception::class,
message: $exception->getMessage(),
origin: $origin,
), $openId);

return;
Expand Down
4 changes: 3 additions & 1 deletion src/SemanticLog/LoggerInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ public function openChain(object $input): string;
* @param object|null $final Terminal being reached on success; null if the chain failed
* @param string $openId Open ID from the corresponding openChain call
* @param Throwable|null $exception Exception that ended the chain, or null on success
* @param string|null $origin Provenance of a semantic validation failure ('input'|'runtime'),
* or null when the failure is not a semantic validation error
*/
public function closeChain(object|null $final, string $openId, Throwable|null $exception = null): void;
public function closeChain(object|null $final, string $openId, Throwable|null $exception = null, string|null $origin = null): void;

/**
* Log transformation start
Expand Down
Loading