Skip to content

Distinguish input vs runtime semantic validation failures - #75

Merged
koriym merged 4 commits into
0.xfrom
claude/input-validation-error-codes-71etgl
Jun 27, 2026
Merged

koriym merged 4 commits into
0.xfrom
claude/input-validation-error-codes-71etgl

Conversation

@koriym

@koriym koriym commented Jun 9, 2026 •

Copy link
Copy Markdown
Collaborator

Background

Semantic-variable validation can fail at any point in a metamorphosis chain, but until now every failure raised the same SemanticVariableException with no indication of where it occurred.

Semantically there are two distinct kinds of failure:

  • Input error: validation fails on the first metamorphosis (input → first form). The data the caller supplied is invalid — an external/caller fault.
  • Runtime error: data that already passed validation fails on a later metamorphosis. This is an internal inconsistency in the transformation logic — a program fault.

An early idea was to distinguish them with HTTP status codes (400/500), but bringing HTTP semantics into the framework felt out of place. Instead, the distinction is carried by exception subtype.

Changes

  • SemanticVariableException is kept as the common base type (made non-final, gains an optional ?Throwable $previous). catch (SemanticVariableException) still catches both subtypes, preserving backward compatibility.
  • Two new final subtypes:
    • InputSemanticVariableException — validation failed on the first metamorphosis (input error)
    • RuntimeSemanticVariableException — validation failed on a later metamorphosis (runtime error)
  • Becoming::__invoke() is the only component aware of a transformation's position in the chain, so it classifies the failure there: an $isFirst flag selects the subtype on catch, the original exception is preserved as $previous, and the refined subtype is thrown to callers.
  • Public signatures of BecomingArguments / SemanticValidator are unchanged.

Semantic-log consistency (origin field)

The classification is also surfaced in the semantic log without introducing a span/chain class-name asymmetry:

  • BecomingCloseContext gains ORIGIN_INPUT / ORIGIN_RUNTIME constants and an optional origin field (emitted only on semantic-validation error exits).
  • LoggerInterface::closeChain / Logger::closeChain thread the origin value through.
  • The chain-close log records the original base exception class (consistent with the inner being_error_close span) and expresses input/runtime as origin, rather than swapping the class name.
  • becoming-close.json documents origin (enum input/runtime, forbidden on the success branch).

Tests

  • BecomingTest: first metamorphosis failure → InputSemanticVariableException; later metamorphosis failure → RuntimeSemanticVariableException. Existing tests that expect the base type still pass.
  • Cause-chain hardening on the real Becoming rewrap path: asserts getPrevious() chaining and Errors propagation (not only in direct-construction unit tests).
  • Branching first-step classification: a validation failure through performTypeMatching is classified as an input error.
  • Chain-close logging: a real SemanticLogger is injected to assert the emitted becoming_close payload records the base error class plus the correct origin.
  • InputSemanticVariableExceptionTest / RuntimeSemanticVariableExceptionTest: cover getErrors(), message building, base-type relationship, and $previous chaining.

Verified

  • composer test — 252 passing
  • composer cs-fix — no violations
  • composer sa — PHPStan and Psalm both clean
  • composer phpmd — clean

Backward compatibility

  • Code catching SemanticVariableException keeps working, since both subtypes are instances of the base.
  • The exception message and getErrors() behavior are unchanged.
  • LoggerInterface::closeChain gains a trailing optional parameter only.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RPywNci6PvvBNvYYqDA8v8

Semantic variable validation could fail at any point in a metamorphosis
chain, but all failures raised the same SemanticVariableException with no
indication of where they occurred. The first metamorphosis validates the
data flowing in from the user-supplied input object (an input error),
while any later metamorphosis validates already-validated state (a runtime
error signalling an internal inconsistency). These are semantically
different and callers should be able to tell them apart.

Add two subtypes of SemanticVariableException - InputSemanticVariableException
and RuntimeSemanticVariableException - so the distinction is carried by type.
Becoming, which is the only component aware of a transformation's position in
the chain, refines the base exception into the appropriate subtype on catch,
preserving the original as the previous exception. Catching the base type
still catches both, keeping existing behaviour intact.
@coderabbitai

coderabbitai Bot commented Jun 9, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds InputSemanticVariableException and RuntimeSemanticVariableException as non-final subclasses of SemanticVariableException (which gains an optional $previous parameter). Becoming::__invoke() now classifies semantic failures by metamorphosis position and wraps them accordingly, passing an origin value through LoggerInterface::closeChain() → Logger → BecomingCloseContext → the becoming-close JSON schema.

Changes

Semantic Validation Origin Tracking

Layer / File(s) Summary
SemanticVariableException hierarchy and new subtypes
src/Exception/SemanticVariableException.php, src/Exception/InputSemanticVariableException.php, src/Exception/RuntimeSemanticVariableException.php, tests/Exception/InputSemanticVariableExceptionTest.php, tests/Exception/RuntimeSemanticVariableExceptionTest.php
SemanticVariableException is made non-final and gains an optional $previous parameter; InputSemanticVariableException and RuntimeSemanticVariableException are added as final subclasses with tests covering inheritance, message/error propagation, and chaining.
BecomingCloseContext origin field and Logger wiring
src/SemanticLog/Context/BecomingCloseContext.php, src/SemanticLog/LoggerInterface.php, src/SemanticLog/Logger.php
BecomingCloseContext adds ORIGIN_INPUT/ORIGIN_RUNTIME constants and a nullable $origin property serialized into JSON; LoggerInterface::closeChain and Logger::closeChain are updated to accept and forward the origin value.
Becoming::__invoke origin detection and exception wrapping
src/Becoming.php
Introduces an $isFirst flag to classify SemanticVariableException catches as input or runtime, wraps them into the appropriate subtype, and passes the computed origin to closeChain.
JSON schema update and integration tests
docs/schemas/becoming-close.json, tests/BecomingTest.php
becoming-close.json adds the origin property and forbids it on the success branch; BecomingTest adds tests for exception subtype, cause chaining, Errors propagation, logged origin values, and runtime fixture classes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • sourcery-ai

Poem

🐇 A hop through the chain, first step or the rest,
Input or runtime — now each gets its crest.
The origin field blooms in JSON's bright glow,
Semantic exceptions now know where to go.
~CodeRabbit 🌸

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: distinguishing input and runtime semantic validation failures.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/input-validation-error-codes-71etgl

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

claude added 2 commits June 27, 2026 13:34
The exception-type contract was only pinned by direct-construction unit
tests and by instanceof checks in the integration tests, leaving regression
gaps a green suite would not catch:

- The cause chain (getPrevious) through Becoming's rewrap was never asserted
  on the real metamorphosis path, so dropping the $previous argument would
  silently lose the original validation site.
- The array/branching first step (performTypeMatching) had no test proving a
  validation failure there is classified as an input error.
- The chain-close log now records the refined subtype FQCN; nothing pinned
  the "refine before closeChain" ordering.

Add getPrevious/getErrors assertions to the two integration tests, a
branching first-step input-classification test, and a chain-close logging
test that inspects the emitted becoming_close payload via a real
SemanticLogger.
When a constructor body threw the base SemanticVariableException, the inner
being_error_close span logged that base class while the outer becoming_close
span logged the refined subtype, so a single failure was labelled with two
different class names across spans.

Keep the chain-close log faithful to the actual error class (matching the
inner span) and express the input/runtime distinction as a dedicated
`origin` field on BecomingCloseContext instead of swapping the class name.
Becoming now logs the original base exception plus origin while still
throwing the refined subtype to callers, so the type-based contract is
unchanged.

- Add ORIGIN_INPUT/ORIGIN_RUNTIME and an optional origin field to
  BecomingCloseContext, emitted only on semantic-validation error exits.
- Thread origin through LoggerInterface::closeChain / Logger::closeChain.
- Document origin in the becoming-close schema (enum input|runtime, and
  forbidden on the success branch).
- Update the chain-close logging tests to assert the base error class plus
  the input/runtime origin.
@koriym
koriym marked this pull request as ready for review June 27, 2026 14:10

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/SemanticLog/Context/BecomingCloseContext.php`:
- Around line 41-46: Validate the origin value in BecomingCloseContext before it
reaches jsonSerialize(), since the constructor currently accepts any string but
becoming-close.json only permits input or runtime. Add normalization/validation
in the BecomingCloseContext constructor or a dedicated helper so invalid origin
values are rejected or mapped to an allowed value, and ensure jsonSerialize()
only emits a permitted origin from the existing origin property.

In `@tests/BecomingTest.php`:
- Around line 762-768: The intentional unused constructor parameter in
BecomingTest::__construct is only silencing PHPCS, so PHPMD still flags
UnusedFormalParameter. Update the fixture annotation on the $value parameter to
suppress the PHPMD rule as well, keeping the existing PHPCS ignore in place, so
both analyzers are covered for this deliberate unused Input parameter.
- Around line 304-312: The log-shape validation in becomingCloseContext
currently uses assert(), which can be disabled and allow malformed
SemanticLogger output to slip through. Replace those checks with PHPUnit
assertions in becomingCloseContext so the structure checks on $logData['open'],
$logData['open'][0]['close'], and the type/context fields fail reliably and
clearly before returning the context.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f7f86b1c-28fe-46fc-9908-ab4108e8376f

📥 Commits

Reviewing files that changed from the base of the PR and between 1f4fb6b and cf9cd0a.

📒 Files selected for processing (11)
  • docs/schemas/becoming-close.json
  • src/Becoming.php
  • src/Exception/InputSemanticVariableException.php
  • src/Exception/RuntimeSemanticVariableException.php
  • src/Exception/SemanticVariableException.php
  • src/SemanticLog/Context/BecomingCloseContext.php
  • src/SemanticLog/Logger.php
  • src/SemanticLog/LoggerInterface.php
  • tests/BecomingTest.php
  • tests/Exception/InputSemanticVariableExceptionTest.php
  • tests/Exception/RuntimeSemanticVariableExceptionTest.php

Comment thread src/SemanticLog/Context/BecomingCloseContext.php
Comment thread tests/BecomingTest.php
Comment thread tests/BecomingTest.php
… param

- becomingCloseContext(): promote the becoming_close type check from an
  assert() to a real PHPUnit assertion so it still holds when zend.assertions
  is disabled; keep the is_array() narrowing asserts that match the repo-wide
  logger-test convention.
- BecomingTestRuntimeMiddle: use the #[Input] $value instead of a hardcoded
  string, removing the unused parameter (and its phpcs:ignore) at the root
  rather than papering over it with a suppression annotation. The seed value
  is itself an invalid email, so the second metamorphosis still fails.
@koriym
koriym merged commit b8dad69 into 0.x Jun 27, 2026
7 checks passed
@koriym
koriym deleted the claude/input-validation-error-codes-71etgl branch June 27, 2026 14:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants