Skip to content

Run filters of branch properties only while their branch is active - #193

Merged
wol-soft merged 29 commits into
claude/sweet-wright-0c9rawfrom
claude/busy-cerf-st2c30
Oct 23, 2026
Merged

wol-soft merged 29 commits into
claude/sweet-wright-0c9rawfrom
claude/busy-cerf-st2c30

Conversation

@wol-soft

@wol-soft wol-soft commented Oct 9, 2026 •

Copy link
Copy Markdown
Owner

Summary

Stacked on #192 (base branch claude/sweet-wright-0c9raw, GitHub retargets to master once #192 is merged). #192 still has to be merged: this PR changes parts of what #192 introduced (see "Changes to tests of #192").

A filter on a property declared in a then, else or oneOf branch ran on the generated parent class for every input, because the transferred copy of the branch property kept the executing FilterValidator. #192 documents this as a known limitation; this PR fixes it.

Behaviour

  • if/then/else, oneOf: the branch class owns the filter. The active branch hands its filtered value to the parent (also through an allOf and through nested compositions); while the branch is inactive the property keeps the value as provided. A setter or populate() which switches the active branch validates the dependent properties again (also when populate() re-passes the filtered property unchanged).
  • Atomic updates: the values which the active branches hand over, and the dependent properties which are validated again after a branch switch, are prepared without assigning anything and only applied once the whole update was validated (constructor, setter, populate()). A rejected setter or populate() leaves the model untouched (also in collect-errors mode), including the case where the dependent property rejects the update.
  • if condition: the filter keeps running inside the condition, but it is neither applied to the parent nor handed over. Filters in a schema dependency behave the same way.
  • Transforming filters: the property holds the raw or the transformed value, so the getter/setter types (native and docblock) are the raw type plus the types returned by the filters of all branches. then and else (or the branches of a oneOf) may use different transforming filters or the same filter with different options on one property; such a property remembers which filter produced its value and is serialized by exactly that filter (otherwise the serializer only applies to a value of the filter's output type).
  • allOf: the filter of a single branch applies to every value. Several branches may declare an identical filter list (filters, options and order); the filters run once.
  • Filter adoption: if the schema declares the property without a filter and the filter of an allOf branch (or the identical filter of every anyOf branch) applies to every value, the schema property adopts the filter (the filter runs first, the schema's validators check the filtered value). Before, such a filter was silently dropped.
  • anyOf: several branches can match, so a filtered property is only accepted when every branch declares the property with the same filter. Every other filtered property in an anyOf branch is rejected with a SchemaException, including a oneOf branch nested below an anyOf.
  • Filtered more than once (SchemaException): a property which is filtered by declarations that can apply to the same value: the schema itself plus a branch (also an identical repeat), allOf branches with diverging filter lists or with values forwarded from nested compositions, the branches of different compositions, or a matching patternProperties filter on a property which is filtered already (by itself, a branch or another pattern). then/else and the branches of one oneOf exclude each other and may filter the same property differently.
  • Shared $ref definitions: a property referencing a definition is detached from the definition before it is transferred from a branch or merged with a branch declaration, so other properties referencing the same definition keep their filters, validators and types.
  • Class deduplication: nested schemas which only differ in the order of a list whose order matters (filter, tuple items/prefixItems, composition branches) are no longer mapped to the same class. Lists which are sets (type, required, enum, the dependants of a property dependency) are still compared order-insensitive.

Implementation

  • FilterValidator::withoutExecution() is a non-executing copy that still describes the filter, so the output type and serialization post processors keep working. getIdentifier() identifies a filter including its options; it is rendered into the generated code as the filter token, followed by an md5 of the options if the filter has options (e.g. 'dateTime_899030ba…').
  • AbstractComposedPropertyValidator::getBranchFilteredKeyMap() / publishesFilteredValueOf() decide which branch properties hand a value over. The compositions publish the filtered values (and, where needed, the producing filter, see getFilterSourceMap()) and stage them; _prepareBranchFilteredValues() validates the dependent properties and computes the new state without assigning, _applyBranchFilteredValues() assigns it. A BranchFilteredValueValidator (a template validator, BranchFilteredValue.phptpl, which substitutes the value by reference in an IIFE like the filter validators) takes the value in front of the other validators of the property.
  • Utils\BranchFilterScope (used by SchemaProcessor) converts the transferred copy, lets the schema property adopt filters executed for every value, adds the substitution to the property which is registered after the merge and rejects ambiguous filters.
  • PropertyProxy::detach(); Schema::addProperty() replaces a registered property which a definition shares (a proxy, or the resolved property of a definition which every proxy delegates to, see ResolvedDefinitionsCollection::isShared()) by a copy of its own before a branch declaration is merged into it. Other registered properties keep their identity.
  • Utils\SchemaSignatureNormalizer sorts only the set-like lists before the class signature is hashed.

Breaking changes (release note)

  1. Branch properties are no longer filtered while their branch is inactive.
  2. Schemas which worked before now throw a SchemaException at generation time: a filtered property in an anyOf branch (unless every branch declares the same filter), a property which is filtered more than once, and a patternProperties filter matching a property which is filtered already (before, the filters were stacked; only two transforming filters were rejected, with a different message). Previously the filters were silently dropped, stacked or applied regardless of the branch.
  3. A filter of an allOf branch (or of every anyOf branch) is now applied to a property which the schema declares without a filter. Before, it was silently dropped.
  4. Type hints (from Accept filters in nested allOf object branches; widen conditional-only properties to mixed #192): a property which is declared only in then or only in else changes from ?T to mixed. A transforming filter in a branch widens the getter/setter types of the property to the raw type plus the filter's output types.
  5. More classes may be generated for nested schemas which only differ in the order of a filter list, tuple items or composition branches (they were wrongly merged before).
  6. A setter called with the current value records it as raw input (meta()->rawInput()), before the early return left the previous raw value.

Review fixes

A first combined review of #192 + #193 found three problems in this PR (serialization with the wrong filter options, a partially applied update when a dependent property rejects it, a silently dropped allOf filter) plus a pre-existing class signature bug (a tuple [string, integer] reused the class of [integer, string]), all fixed here.

A second combined review (against master including #195-#197) found four more, fixed with a failing test first:

  1. A filtered $ref definition used in a oneOf / if/then/else branch was changed while the branch property was transferred: the filter was dropped, other properties referencing the definition crashed on valid input, received the branch's filter, transforming type or the mixed widening of Accept filters in nested allOf object branches; widen conditional-only properties to mixed #192.
  2. The docblock of a property with branch-owned transforming filters listed only the first type, so the builder class didn't import DateTime and rejected it.
  3. A setter returning early (value equal to the filtered value) kept the previous raw input, which a later branch switch validated again.
  4. A patternProperties filter stacked with a branch filter (now rejected together with every other stacking, see breaking change 2).

Code review notes on the rendering: the substitution is rendered from a template instead of a special case in RenderHelper, the set-up code of the compositions is collected in arrays, the rendered filter identifier is a token plus hash instead of JSON. Tests which asserted generated code or internal state were removed (covered by the executed tests), and tests generating the same or an identical schema with the same configuration were merged.

Changes to tests of #192 and master

  • The playlist test now expects the title to stay untouched while its then branch is inactive (it previously pinned the leak).
  • The nested-compositions schema no longer filters its anyOf branch property (now rejected); the original shape is covered as a rejection test.
  • The accepted static row for a filtered property in a root allOf branch moved to the rejection tests (parent filter plus branch filter).
  • PatternPropertiesTest::testMultipleTransformingFiltersForPatternPropertiesAndObjectPropertyThrowsAnException asserts the "filtered more than once" message which replaced the transforming-filter-only check.
  • ComposedIfTest::testCrossTypedThenOnly… (renamed in Accept filters in nested allOf object branches; widen conditional-only properties to mixed #192) is removed: testThenOnlyPropertyKeepsValueOfAnyTypeWhileTheBranchIsInactive covers the same schema shape including the mixed types.

Merging with master

Master (up to #197) is merged into #192 and, through #192, into this branch. The one conflict was Schema::addProperty() (#197 changed the merge call). Resolution: keep the copy of a shared registered property before the merge and assign the merge result as #197 does ($replacesExisting = $this->propertyMerger->merge(...), plus the $compositionProcessor === null argument).

Documentation

filter.rst ("Filters on properties of object branches": adoption, identical allOf lists, atomic updates, serialization by the producing filter, pattern property filters), notes on the anyOf, allOf and if pages, the dependency schema note in object.rst, and the list order rule for class re-usage in namespaces.rst. The docs were not built locally (Sphinx is not installed in the sandbox).

Test plan

Developed test-first (the commits show it).

  • Full suite on PHP 8.4.26 (built from source) with PHPUnit 13.4.1, run in per-directory chunks, on this branch and on Accept filters in nested allOf object branches; widen conditional-only properties to mixed #192 after merging master: all green except the 4 ReferencePropertyTest cases which fetch raw.githubusercontent.com (blocked in the sandbox).
  • phpcs --standard=phpcs.xml on all changed PHP files: clean
  • FilterCompositionBranchScopeTest, FilterCompositionRuntimeTest, ComposedIfTest, JsonSchemaSignatureTest, IdenticalNestedSchemaTest (tuple order)
  • Not run locally: PHP 8.5, coverage, qlty.sh (needs a manual trigger on the PR page)

🤖 Generated with Claude Code

https://claude.ai/code/session_01GXn5VCTemp5rnjGrXMMvJs

claude added 11 commits October 9, 2026 12:09
A filter declared on a property inside a root-level then/else branch is
copied onto the root property by cloneTransferredProperty() and runs
unconditionally, even while the branch is inactive. The test asserts the
correct behaviour and currently fails (an inactive else filter still
trims the value). The fix direction is still to be decided.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXn5VCTemp5rnjGrXMMvJs
… anyOf/allOf rejection

The filter of a property declared inside a root-level composition branch
currently runs for every input because the transferred copy on the parent
class keeps the FilterValidator. The new FilterCompositionBranchScopeTest
asserts the intended behaviour: a filter only affects the value while its
branch is active (if/then/else, oneOf, including a transforming filter),
and a filtered property declared directly in an anyOf or allOf branch is
rejected at generation time. These tests are expected to fail until the
implementation lands.

Tests that pinned the old behaviour are adjusted:
- the playlist test now expects the title to stay untouched while its
  then branch is inactive
- the nested-compositions schema no longer filters the anyOf branch
  property (the original shape is covered as a rejection)
- the accepted row for a filtered property in a root allOf branch moves
  to the rejection tests

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXn5VCTemp5rnjGrXMMvJs
Pins what works today as regression guards: a property-level anyOf with a
single object branch (the shape accepted by the static tests), and
property-level anyOf (merged class) and oneOf with several object branches,
where only the matching branch may filter the value.

Adds the property-level conditional case, which goes through the same
branch transfer into a parent class as a root-level conditional and
therefore shows the same leak; it fails until the implementation lands.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXn5VCTemp5rnjGrXMMvJs
A single filtered property in an allOf branch works today and keeps
working (new regression test). allOf is rejected only when a property is
filtered more than once (several branches, or the parent schema plus a
branch), because the first filter wins and the others are silently
dropped. A filtered property in an anyOf branch is rejected.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXn5VCTemp5rnjGrXMMvJs
A oneOf branch could own a filtered property, but its value would have to
be forwarded through the enclosing anyOf, where several branches can match.
It is rejected with the same message as a filter declared in an anyOf
branch. Fails until the implementation lands.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXn5VCTemp5rnjGrXMMvJs
…arker and branch key map

FilterValidator can now return a copy which still describes the filter but
is not rendered into the validation code, so a property which receives the
filtered value from the branch owning the filter still exposes the filter
to the output-type and serialization post processors.

Composed validators expose getBranchFilteredKeyMap(), the properties for
which the active branch of a oneOf or if/then/else (or a value forwarded
through an allOf) hands the filtered value over to the containing schema.

No generated code changes yet; the runtime consumption follows.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXn5VCTemp5rnjGrXMMvJs
…ranch

A filter on a property of the if condition keeps running inside the
condition but must not be applied to the parent. A property which has its
own filter and is filtered again in a then or oneOf branch is rejected like
the allOf duplicates; the duplicate-filter message is unified.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXn5VCTemp5rnjGrXMMvJs
The filter of a property declared in a then, else or oneOf branch ran on the
parent class for every input because the transferred copy kept the executing
FilterValidator. The branch class now owns the filter and the active branch
hands the filtered value to the parent:

- the transferred copy keeps a non-executing description of the filter
- the compositions publish the filtered values of the active branch from
  the modified values they already compute (also through an allOf)
- a substitution validator takes the published value in front of the other
  validators of the property; without a published value the value stays as
  provided
- when a setter switches the active branch, dependent properties are
  validated again
- a filter on a property of the if condition keeps running inside the
  condition but is neither applied to the parent nor published

Transforming filters (serializer type guard) and the generation time
rejections follow.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXn5VCTemp5rnjGrXMMvJs
A property whose transforming filter is owned by a then, else or oneOf
branch may hold the value as provided (branch inactive) or the transformed
value:

- the generated serializer only serializes a value of the output type of a
  filter and returns any other value as it is
- the output type of the property is the raw type plus the types returned by
  the filters of its branches, also when then and else transform the same
  property with different filters
- the transferred copy of a branch property carries the widened output type so
  merging the copies of several branches keeps the raw type

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXn5VCTemp5rnjGrXMMvJs
A filtered property declared in a branch of an anyOf is rejected: several
branches can match, so the filtered value cannot be attributed to a single
branch. This includes a oneOf branch below an anyOf.

A property which is filtered more than once by declarations that can apply to
the same value is rejected as well: the schema itself plus a branch, several
allOf branches or the branches of different compositions. The filters have no
defined order and only the first declaration used to take effect while the
others were silently dropped. The branches of one oneOf or if/then/else
exclude each other and may filter the same property differently.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXn5VCTemp5rnjGrXMMvJs
Replaces the note that a filter of a conditional branch property also runs
while the branch is inactive with the rules of the filter scope: which
compositions can own a filtered property, the if condition, the rejection
of anyOf and of a property which is filtered more than once, and transforming
filters (type hints and serialization). Adds notes to the anyOf, allOf and
if pages.

Adds tests for a conditional nested in a oneOf branch, a oneOf nested in a
then branch and two compositions which filter the same property.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXn5VCTemp5rnjGrXMMvJs
@coveralls

coveralls commented Oct 9, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 39933508518

Coverage increased (+0.2%) to 99.18%

Details

  • Coverage increased (+0.2%) from the base build.
  • Patch coverage: 3 uncovered changes across 1 file (686 of 689 lines covered, 99.56%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
src/Utils/SchemaSignatureNormalizer.php 34 31 91.18%
Total (19 files) 689 686 99.56%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 8656
Covered Lines: 8585
Line Coverage: 99.18%
Coverage Strength: 588.04 hits per line

💛 - Coveralls

- Keep the filter declarations of the branches in a WeakMap keyed by schema
  and read the filters of the schema itself fresh on every check (deferred
  while the property is unresolved), so a property declared via $ref is
  covered and a recycled object id can't inherit foreign declarations
- Share the output type of a branch-owned transforming filter between the
  transfer step and the output type post processor
- Compute the filtered key map once per render step
- Try the serializer of a filter without a return type last so it can't
  hide the serializers of the other filters
- Add isAllOf()/isAnyOf() and FilterValidator::of() instead of repeating the
  checks

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXn5VCTemp5rnjGrXMMvJs
- A filtered property in an anyOf is accepted when every branch declares it
  with the same filters: the outcome doesn't depend on the matching branch.
  Any other filtered property in an anyOf branch is still rejected.
- The compositions stage the filtered values of the active branches and
  the constructor, setters and populate() adopt them only after the update
  succeeded (dependent properties are validated again at that point). A
  failing setter or populate() no longer leaves the values of the branch it
  tried to activate behind.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXn5VCTemp5rnjGrXMMvJs
Drop the then/oneOf-overrides-root rows of the filtered-more-than-once
provider (the same assertion path as the allOf rows), the positive
branch-filtered key map tests (covered by the runtime tests) and two
FilterValidator unit tests that the integration tests already cover.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXn5VCTemp5rnjGrXMMvJs
Schema::addProperty registers by name immediately, so the transferred
branch property is always registered and the null check in
takeOverBranchFilteredValue could never trigger. The validator's
getCheck() was only reachable through a unit test.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXn5VCTemp5rnjGrXMMvJs
…ilter scoping

- populate() passed all provided keys as processed to the commit, but skips
  values equal to the current attribute: a re-passed filtered property was
  excluded from the dependent re-validation while the holder changed. Only
  the keys populate() processed count now.
- The commit addressed a property whose name starts with a digit by its
  unprefixed attribute; use the class attribute.
- Move the filter scoping code out of SchemaProcessor into
  Utils\BranchFilterScope.
- Docs: the dateTime example declares the property in the parent so the
  documented type hints are accurate, mention the mixed hint of a branch-only
  property and the atomic-update exception.
- Tests: populate re-passing the filtered property, digit-leading property
  name, immutable models.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXn5VCTemp5rnjGrXMMvJs
allOf and oneOf answer a component from its cached result when none of its
properties is part of the update. Such a component publishes no value, and
staging null for its keys dropped the committed filtered value of the
unchanged active branch (oneOf: updating a property of an inactive branch;
allOf: updating one of two conditionals). Compositions now track the
evaluated components and only stage keys whose components were evaluated.

As the staged slots are no longer always overwritten, populate() resets the
staging at its start so a failed populate cannot leak a stale value into a
later one.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXn5VCTemp5rnjGrXMMvJs
Identical schemas are mapped to one class by a signature which sorted every
list. Schemas which only differ in the order of their filters or of the
items of a tuple were therefore mapped to the same class, and the second
one validated with the tuple of the first.

Only lists which are sets are compared order-insensitive now (type,
required, enum, the dependants of a property dependency), so the
deduplication stays as broad as possible. Every other list keeps its
order, values (const, default, examples, enum items) are never sorted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXn5VCTemp5rnjGrXMMvJs
…f filter lists

A filter which a branch executes on the schema for every input (a single
allOf branch, the identical filter lists of every anyOf branch) was silently
dropped if the schema declared the property without a filter: the merge kept
the declaration which was registered first. The registered property now
adopts the filters as if it declared them (the filters run first, its own
validators check the filtered value).

Several allOf branches which declare an identical filter list (filters,
options and order) filter the same input; they are accepted and the filters
run once. Diverging lists and values forwarded from nested compositions stay
rejected as filtered more than once.

Tests written by the review session which started this work.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXn5VCTemp5rnjGrXMMvJs
…pdate is assigned

A setter or populate() which switched the active branch validated the
dependent properties again after the update was already assigned. A dependent
property which failed (e.g. a trimmed title violating minLength of the
schema) left the model with the new branch and the old value, a state the
constructor rejects.

The commit is split into a prepare step, which validates the dependent
properties and computes the new state without assigning anything, and an
apply step. The setter prepares at the end of its before-validation hook and
applies in the after hook, populate() prepares inside its rollback block and
applies once the update succeeded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXn5VCTemp5rnjGrXMMvJs
Then and else (or the branches of a oneOf) may transform the same property
with the same filter but different options, or with different filters
returning the same type. The property only kept one description per filter
name and the serializer picked the first filter matching the type of the
value, so a value of the other branch was serialized with the wrong filter
or options.

A property which can hold the values of several such filters now remembers
which filter produced its value: the active branch hands over the filter
together with the value (read from the nested object when the value comes
from a deeper composition), the source is staged, prepared and applied
together with the value, and the serializer dispatches on it before falling
back to the type check. Classes without such a property are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXn5VCTemp5rnjGrXMMvJs
Filter adoption by the schema property, identical allOf filter lists, the
update which is rejected by a property depending on the branch, the
serialization by the producing filter and the list order in the
deduplication of nested classes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXn5VCTemp5rnjGrXMMvJs
…ranch properties

The first property referencing a definition is the property every other
reference delegates to, and a clone of a reference still delegates every
change to it. Transferring a branch property (scoping its filter to the
branch, widening its type) and merging it into a registered property
(adopting a filter, adding the branch value substitution) therefore changed
the definition: other properties referencing it lost their filter, crashed
on valid input or got the type and filters of the branch.

A transferred reference is detached from the definition, and a registered
property is replaced by a copy of its own before a branch declaration is
merged into it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXn5VCTemp5rnjGrXMMvJs
…of an early setter return

- A property whose transforming filter is owned by a branch got the native
  type 'string | DateTime | null' but the docblock only rendered the first
  type name ('string|null'). The builder class derives its imports from the
  docblock, so DateTime was not imported and its setter rejected a DateTime.
  The docblock now lists every type.
- A setter returns early if the new value equals the current value. The
  current value of a branch filtered property may be the filtered value, the
  raw input kept the value provided before; a branch switch validated the
  property again from that stale raw input. The early return records the
  provided value as raw input.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXn5VCTemp5rnjGrXMMvJs
The filter of a matching patternProperties entry was added to a declared
property behind its own filter or the filter of a composition branch, and
two matching entries stacked their filters as well. Only two transforming
filters were rejected. As the filters of a property have no defined order,
any combination is now rejected as filtered more than once (the previous
check for two transforming filters is covered by it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXn5VCTemp5rnjGrXMMvJs
Copying every registered property changed its identity for every merge.
Only a property which other references to a definition delegate to (the
resolved property of a definition, or a proxy) has to be copied.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXn5VCTemp5rnjGrXMMvJs
…y the tests

- BranchFilteredValueValidator is a template validator: an IIFE substitutes the value by reference
  (like the filter validators), so RenderHelper no longer special-cases it
- the composition validators collect their set-up statements in an array
- the filter identifier rendered into the generated code is the token, followed by a hash of the
  options if the filter has options, instead of the JSON representation
- remove tests which asserted generated code or internal state, merge tests which generated the
  same (or an identical) schema with the same configuration

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXn5VCTemp5rnjGrXMMvJs
@wol-soft
wol-soft added this pull request to stack #205 October 23, 2026 09:30
@wol-soft
wol-soft merged commit a4866ba into master Oct 23, 2026
7 checks passed
wol-soft pushed a commit that referenced this pull request Oct 23, 2026
Resolution: the setters validate the compositions themselves instead of via
the composition setter hook, so the staging of the filtered values of the
active branches (#193) moves from that hook into the core setter rendering:
each setter path which validates a composition resets the staging and prepares
the filtered values, the setter applies them once the update succeeded.
populate() prepares them after the dependency validation of the properties it
doesn't update. The per-composition restore of the validation state stays
removed in favour of the model state snapshot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016TbSpXPq3pUmw9Hrh9CfRG
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.

3 participants