Skip to content

Validation honors the conformance an element inherits - #4369

Merged
mergify[bot] merged 5 commits into
mainfrom
fix/conformance-of-operational-extension
Aug 30, 2026
Merged

Validation honors the conformance an element inherits#4369
mergify[bot] merged 5 commits into
mainfrom
fix/conformance-of-operational-extension

Conversation

@Apollon77

@Apollon77 Apollon77 commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

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 (PeerBehavior extends attributes to record element support and the peer's cluster revision), so conformance was silently not enforced for them.

The change

astToFunction 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 hand an extension inherited conformance but not inherited nullability. ConformanceError reports 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 FabricIndex the 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 a prepareEntry hook 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 that AccessControl.acl and AccessControl.extension (with the Extension feature 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 fabricIndex now 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: AttributeWriteResponse injects the accessing fabric into every decoded write before validation, and TlvArray.injectField recurses into each list element.

The standard model is otherwise untouched. Of the elements whose effective conformance differs from their own, the 212 ClusterRevision/FeatureMap attributes are skipped by struct validation, and the ModeBase and OperationalState members are already enforced by ValidatedElements, which has always read the effective conformance. All 20 elements whose effective quality differs state conformance: "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.ts gains an operational extension block: 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.ts gains a mandatory fabricIndex block 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 uses type: "FabricIndex" (the seed field, which states M) rather than fabricIndex: "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 lint and the full npm test all pass.

🤖 Generated with Claude Code

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>
Copilot AI balanced review requested due to automatic review settings August 30, 2026 16:17

Copilot AI 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.

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.

Comment thread packages/node/src/behavior/state/managed/values/StructManager.ts
…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>
@mergify

mergify Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

Copilot AI 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.

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 mutates previousValue itself; 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;

Apollon77 and others added 2 commits August 30, 2026 20:22
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>
@Apollon77 Apollon77 added the automerge Set this label if the PR is ready to automatically merged after approval label Aug 30, 2026
@mergify
mergify Bot merged commit b4f5ed9 into main Aug 30, 2026
48 checks passed
@mergify
mergify Bot deleted the fix/conformance-of-operational-extension branch August 30, 2026 19:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automerge Set this label if the PR is ready to automatically merged after approval

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants