Validation honors the conformance an element inherits - #4369
Merged
Conversation
An operational extension carries only the properties it overrides -- id, name and whatever it restates -- and resolves everything else through the model it extends. The conformance compiler read the element's own conformance, so an extended element compiled as if it stated none, which is "optional", which installs no validator at all. A discovered peer cluster overrides its elements this way, so conformance was not enforced for them. The compiler now reads the effective conformance, and the effective quality alongside it: reading an element's own quality while resolving its conformance through the base would give an extension inherited conformance but not inherited nullability. ConformanceError reports the effective conformance too, so the message names what the decision was made on rather than an empty string. Judging inherited conformance means a fabric-scoped list entry is now held to the mandatory FabricIndex the seed field states, which exposed two places where an entry was judged before it was complete: - The managed list supplied the accessing fabric inside writeEntry, after the entry had already been validated. Completion moves to a prepareEntry hook the fabric-scoped handler overrides, invoked before validation. - Assigning a whole list to a fabric-scoped member that held no value assigned it directly rather than merging through the managed list, bypassing the fabric filtering and the accessing fabric along with it. An absent member is now an empty list for merge purposes. A write from local code that adds an entry without a fabricIndex now fails validation rather than storing an entry that belongs to no fabric. Nothing supplies the fabric for a local actor, so such an entry was already unusable to anyone reading it. Writes from a peer are unaffected: the accessing fabric is injected into every decoded write before validation runs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Updates validation to honor inherited model conformance and quality, including fabric-scoped list handling.
Changes:
- Validate effective conformance, quality, and enum-member conformance.
- Prepare fabric-scoped entries before validation and merge absent lists through managed proxies.
- Add regression tests and changelog entries.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
CHANGELOG.md |
Documents behavior changes. |
packages/protocol/src/action/errors.ts |
Reports effective conformance. |
packages/node/src/behavior/state/validation/conformance-compiler.ts |
Compiles inherited validation rules. |
packages/node/src/behavior/state/managed/values/ListManager.ts |
Prepares fabric-scoped entries before validation. |
packages/node/src/behavior/state/managed/values/StructManager.ts |
Routes absent fabric-scoped lists through managed merging. |
packages/node/test/behavior/state/validation/conformanceTest.ts |
Tests operational extensions. |
packages/node/test/behavior/state/managed/values/ListManagerTest.ts |
Tests mandatory fabric-index writes. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…r absent Seeding an absent member as an empty list happened outside the scope that undoes a failed write, so a per-entry validation failure caught within the transaction left the member changed from absent to []. Consumers enumerate slot keys to discover which members hold a value, so that is a visible change from a write that did not take. The undo now covers the merge as well as the validation that follows it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
|
Tick the box to add this pull request to the merge queue (same as
|
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Suppressed comments (1)
packages/node/src/behavior/state/managed/values/StructManager.ts:263
- This rollback is ineffective when the existing list was already modified earlier in the same transaction. In that case the managed reference no longer clones on
proxy[i] = ..., so the loop mutatespreviousValueitself; if a later entry fails and the caller catches the error,undo()reassigns that partially overwritten array and the accepted prefix remains stored. Snapshot the pre-merge list (or stage the merge in an isolated copy) and add a regression covering a prior list write followed by a whole-list assignment whose later entry is invalid.
} catch (e) {
undo();
throw e;
The list write paths this branch touches were covered only for fabric-scoped lists. These characterize the same paths for a plain list, where nothing completes an entry on the caller's behalf: assignment by index, a whole list assigned to a member that had none, and a rejected entry in each case. The last test states the boundary rather than characterizing it. A list that is not fabric-scoped whose entry declares a mandatory FabricIndex must reject a write that omits it, so moving entry completion up into the base handler, where it would apply to every list, fails here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
An operational extension carries only the properties it overrides —
id,nameand whatever it restates — and resolves everything else through the model it extends. The conformance compiler read the element's own conformance, so an extended element compiled as if it stated none, which is "optional", which installs no validator at all.A discovered peer cluster overrides its elements this way (
PeerBehaviorextends attributes to record element support and the peer's cluster revision), so conformance was silently not enforced for them.The change
astToFunctionnow reads the effective conformance, and the effective quality alongside it — reading an element's own quality while resolving its conformance through the base would hand an extension inherited conformance but not inherited nullability.ConformanceErrorreports the effective conformance too, so the message names what the decision was made on instead of an empty string.What that exposed
Judging inherited conformance means a fabric-scoped list entry is now held to the mandatory
FabricIndexthe seed field states. Two places were validating an entry before it was complete:The managed list supplied the accessing fabric inside
writeEntry, after validation had already run. Completion moves to aprepareEntryhook that the fabric-scoped handler overrides, invoked before validation. There is one preparation point rather than two.Assigning a whole list to a fabric-scoped member that held no value assigned it directly, bypassing the managed list and with it the fabric filtering and the accessing fabric. An absent member is now an empty list for merge purposes.
Reachability of that second one is worth stating precisely: a member that is mandatory under the active features receives a synthesized default, so it holds
[]rather than nothing. I verified thatAccessControl.aclandAccessControl.extension(with theExtensionfeature enabled) both start as[]and therefore take the merge path already. So this closes a real hole in the managed-value layer, but I have not found a stock cluster that reaches it — it would need a fabric-scoped list that is optional under its active features and unset.Behavior change
A write from local code that adds an entry to a fabric-scoped list without a
fabricIndexnow fails validation instead of storing an entry that belongs to no fabric. Nothing supplies the fabric for a local actor and nothing ever did, so such an entry was already unusable to anyone reading it — this surfaces a latent data bug rather than creating one. Every in-repo writer already supplies the index.Writes from a peer are unaffected:
AttributeWriteResponseinjects the accessing fabric into every decoded write before validation, andTlvArray.injectFieldrecurses into each list element.The standard model is otherwise untouched. Of the elements whose effective conformance differs from their own, the 212
ClusterRevision/FeatureMapattributes are skipped by struct validation, and the ModeBase and OperationalState members are already enforced byValidatedElements, which has always read the effective conformance. All 20 elements whose effective quality differs stateconformance: "X", where nullability is never consulted.Testing
Five production hunks, each mutation-verified individually — revert one and its test fails, restore it and the suite is green:
conformanceTest.tsgains anoperational extensionblock: a field its base disallows, an enum member its base disallows, a field its base declares nullable, and the error message naming the conformance it judged.ListManagerTest.tsgains amandatory fabricIndexblock over five write shapes — indexed assignment,push, whole list over an existing one, whole list onto an absent member, and the accessing fabric being supplied. It usestype: "FabricIndex"(the seed field, which statesM) rather thanfabricIndex: "fabric-idx"as the existing fixtures do, since the raw datatype states no conformance and so cannot exercise this.npm run build-clean,npm run format-verify,npm run lintand the fullnpm testall pass.🤖 Generated with Claude Code