Skip to content

Fold inherited rules and associations into the entity that inherits them - #18

Merged
peopleworks merged 1 commit into
mainfrom
fix/fold-inherited-rules
Aug 17, 2026
Merged

Fold inherited rules and associations into the entity that inherits them#18
peopleworks merged 1 commit into
mainfrom
fix/fold-inherited-rules

Conversation

@peopleworks

Copy link
Copy Markdown
Owner

Closes #14.

What was wrong

Folding inherited properties (#15) carried what lives on a property and left everything recorded on the class behind. Measured against the extended AuditedXpoSolution fixture, Receipt had no validation rules, no appearance rules and no relationships — while its own property table said CreatedBy was required. The document contradicted itself on the same page.

All three of those are real for the entity:

  • a RuleCriteria on an audit base is enforced every time any entity in the application is saved
  • an [Appearance] greys a field on every screen below it
  • an association gives every descendant a collection that really is populated

Receipt now reads:

### Relationships
- **AuditEntries** -> `AuditEntry` (One to many) (composition/aggregation) — inherited from `AuditedObject`

### Validation Rules
- **RuleCriteria** `Audit_ChangedNotBeforeCreated`: `ChangedOn >= CreatedOn` — “A record cannot be changed before it was created.” — inherited from `AuditedObject`
- **RuleRequiredField** in `CreatedBy` `Audit_CreatedByRequired` — inherited from `AuditedObject`

### Appearance Rules
- **Audit_ReadOnlyOnceVersioned** — when `RowVersion > 0`: enabled=false — inherited from `AuditedObject`

Marking is not decoration. Folding without it trades one defect for a quieter one: the reader now sees every rule that governs the entity and cannot tell which of them changing would change the whole application. InheritedFrom has been recorded on properties since 0.13.0 and displayed by nothing — the property table gained it here too.

The debatable half

The issue asks that relationships be decided on purpose rather than by omission. They are, and so is everything else: an entity's own section reports what governs it; an index, a count, a diagram, a search and the diff report what the application declares.

One rule on a base shared by two hundred entities is one rule. Following the fold everywhere would make every total, map and search result a measurement of the class hierarchy — the same failure as #16, one collection over. On this fixture the declared count is 5 validation rules; the folded count is 12, on six entities, and it would grow when somebody adds a subclass and changes nothing about the validation.

Pinned in DeclaredRatherThanFoldedTests. Declarer-marking is what lets each rendering choose.

Found while probing

The four-argument rule attribute was misread:

[RuleCriteria("Sale_TotalNotNegative", DefaultContexts.Save, "Total >= 0", "A sale total cannot be negative.")]

PositionalCriteria took the last positional literal as the criteria, which is right for the three-argument overload and wrong for this one — so Expression held the sentence explaining the rule and MessageTemplate stayed empty. Every fixture in the suite passed its message as CustomMessageTemplate =, so all 299 tests agreed with the wrong answer.

Fixed here rather than in a follow-up, because the fold multiplies whatever a rule holds. A rule now also carries its identifier and its validation contexts, which were read into the raw parameter bag as arg0 and arg1 — and printed to the published documentation that way:

- RuleCriteria: class - arg0=Audit_ChangedNotBeforeCreated, arg1=DefaultContexts.Save, arg2=ChangedOn >= CreatedOn, arg3=A record cannot be changed before it was created.

The enforced criteria was extracted in 0.13.0 and shown by neither the MCP detail tool nor the HTML page; both show it now.

Verification

  • 318 tests, up from 299. AuditedXpoSolution gained a rule, an appearance rule, an association and a third inheritance level, so a rule now has to arrive from a grandparent and a redeclaration has to win.
  • Run against ComisionSOS (19 entities, flat): totals unchanged — 5 validation rules, 7 appearance rules.
  • TheDemoApplicationKeepsItsShape still pins 24 relationships and 9 rules on the flat demo.

Folding properties carried what lives on a property and left everything
recorded on the class behind. A RuleCriteria on an audit base is enforced
every time any entity in the application is saved; an [Appearance] greys a
field on every screen below it; an association gives every descendant a
collection that really is populated. All three appeared under the base
alone, so a reader told the inventories were complete read an entity's
section and was told of no rule.

Each folded declaration now names the class that wrote it -- in the entity's
properties too, where InheritedFrom has been recorded since 0.13.0 and shown
by nothing. Folding without marking would trade one defect for a quieter
one: the reader sees every rule that governs the entity and cannot tell
which of them changing would change the whole application.

The other half of #14 is what an index, a count, a diagram or a search
should say once every entity carries its ancestors' declarations. They are
answering a different question -- what this application declares -- and
following the fold there would make every one of them a measurement of the
class hierarchy. One rule on a base shared by two hundred entities is one
rule. So those read declarations; an entity's own section reads everything
that governs it.

Found while probing: the four-argument rule attribute --
[RuleCriteria("id", DefaultContexts.Save, "Total >= 0", "A sale total
cannot be negative.")] -- put the message in the field holding what the
rule enforces and left the message field empty. Every fixture passed its
message as CustomMessageTemplate =, so all 299 tests agreed with the wrong
answer. Fixed here rather than after, because the fold multiplies whatever
a rule holds. A rule now also carries its identifier and its validation
contexts, read as arg0 and arg1 and printed to the published documentation
that way.

Closes #14

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment on lines +1111 to +1121
foreach (var item in fromParent)
{
// Redeclaring wins: a descendant that reuses an identifier is replacing the rule, and
// listing both would show a reader two rules that contradict each other.
if (!own.Add(keyOf(item)))
continue;

var copy = clone(item);
markDeclarer(copy, parentClassName);
target.Add(copy);
}
@peopleworks
peopleworks merged commit ecddf09 into main Aug 17, 2026
6 checks passed
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.

Rules an entity inherits are not folded with its properties

2 participants