@@ -346,24 +346,32 @@ final readonly class When implements Validator
346346 public function __construct(Validator $when, Validator $then, Validator $else) {}
347347}
348348
349- // Modifier: exclude type instead of asserting it
349+ // Exact: passes if and only if the input is of the declared type
350+ #[Assurance(type: 'int', exact: true)]
351+ final readonly class IntType implements Validator { /* ... */ }
352+
353+ // Modifier on a Wrap prefix: negate the wrapped node's assurance
350354#[Assurance(modifier: AssuranceModifier::Exclude)]
355+ #[AssuranceSubject(AssuranceSubjectMode::Wrap)]
351356final readonly class Not implements Validator { /* ... */ }
352357
353- // Modifier: add null to the assured type
354- #[Assurance(modifier: AssuranceModifier::Nullable)]
358+ // Bypass set on a Wrap prefix: 'null' is admitted in union with the
359+ // wrapped node's assurance (nullOrIntType() assures int|null)
360+ #[Assurance(type: 'null', exact: true)]
361+ #[AssuranceSubject(AssuranceSubjectMode::Wrap)]
355362final readonly class NullOr implements Validator { /* ... */ }
356363```
357364
358365Properties:
359366
360- | Property | Type | Purpose |
361- | ----------------| --------------------------| ------------------------------------------------------------------|
362- | ` type ` | ` ?string ` | Fixed type string (e.g. ` 'int' ` , ` 'string' ` ) |
363- | ` from ` | ` ?AssuranceFrom ` | Derive type from a method argument |
364- | ` compose ` | ` ?AssuranceCompose ` | Combine assurances from child validators |
365- | ` composeRange ` | ` ?array{int, int\|null} ` | Subset of arguments to compose (` [from, to] ` , null = open-ended) |
366- | ` modifier ` | ` ?AssuranceModifier ` | Modify how the assurance is applied |
367+ | Property | Type | Purpose |
368+ | ----------------| ------------------------------| ------------------------------------------------------------------|
369+ | ` type ` | ` string\|list<string>\|null ` | Fixed type string (e.g. ` 'int' ` ); a list means their union |
370+ | ` from ` | ` ?AssuranceFrom ` | Derive type from a method argument |
371+ | ` compose ` | ` ?AssuranceCompose ` | Combine assurances from child validators |
372+ | ` composeRange ` | ` ?array{int, int\|null} ` | Subset of arguments to compose (` [from, to] ` , null = open-ended) |
373+ | ` modifier ` | ` ?AssuranceModifier ` | Modify how the assurance is applied |
374+ | ` exact ` | ` bool ` | The node passes * iff* the input is of the declared type |
367375
368376### AssuranceFrom (enum)
369377
@@ -388,7 +396,37 @@ Determines how child assurances are combined:
388396
389397Modifies how an assurance is applied:
390398
391- | Case | Meaning |
392- | ------------| ------------------------------------------|
393- | ` Exclude ` | Removes the type instead of asserting it |
394- | ` Nullable ` | Adds ` null ` to the assured type |
399+ | Case | Meaning |
400+ | -----------| -----------------------------------------------------------------------|
401+ | ` Exclude ` | The wrapped node's assurance is negated: passing implies NOT the type |
402+
403+ ### AssuranceSubject
404+
405+ Declares how a ` #[Composable] ` prefix relates to its wrapped node's subject.
406+ A prefix without it yields no assurance for composed names: tools must drop,
407+ not copy, the wrapped node's assurance.
408+
409+ ``` php
410+ // Same subject, modified: notEmail() negates Email's assurance
411+ #[Assurance(modifier: AssuranceModifier::Exclude)]
412+ #[AssuranceSubject(AssuranceSubjectMode::Wrap)]
413+ final readonly class Not implements Validator { /* ... */ }
414+
415+ // Derived subject: keyEmail('name') assures only the container type
416+ #[Assurance(type: ['array', 'ArrayAccess'])]
417+ #[AssuranceSubject(AssuranceSubjectMode::Container)]
418+ final readonly class Key implements Validator { /* ... */ }
419+ ```
420+
421+ ### AssuranceSubjectMode (enum)
422+
423+ | Case | Meaning |
424+ | -------------| ----------------------------------------------------------------------------------|
425+ | ` Wrap ` | Same subject as the wrapped node: its assurance passes through, modified |
426+ | ` Elements ` | The wrapped node validates each element: assurance becomes ` iterable<T> ` |
427+ | ` Container ` | The wrapped node validates a derived subject: only the container type is assured |
428+
429+ A ` Wrap ` prefix's own ` #[Assurance(type:)] ` declares its * bypass set* : inputs
430+ it admits itself, in union with whatever the wrapped node assures. It is only
431+ meaningful in composition, never as a claim about direct calls; ` exact ` on it
432+ means the bypass set is an exact characterization.
0 commit comments