Skip to content

Fold inherited properties into the entity that inherits them - #15

Merged
peopleworks merged 1 commit into
peopleworks:mainfrom
MBrekhof:fix/fold-inherited-properties
Aug 14, 2026
Merged

Fold inherited properties into the entity that inherits them#15
peopleworks merged 1 commit into
peopleworks:mainfrom
MBrekhof:fix/fold-inherited-properties

Conversation

@MBrekhof

Copy link
Copy Markdown

Fixes #13.

What this changes

Each entity now carries the properties it inherits from classes declared in the same project, folded in declaration order from the root down, each marked with the class that declared it. The abstract base itself is now marked IsAbstract.

Why

Your numbers from #13, on main and on this branch:

                      main                    this branch
NamedBaseObject       Name                    Name
Order                 Number                  Name, Number
PriorityOrder         Rank                    Name, Number, Rank

PriorityOrder.Name and PriorityOrder.Number are persisted, appear in views, and are readable from any code an agent writes; they now appear under the heading that presents the entity, instead of being a stated omission in an inventory that promises to be whole.

How it decides

The walk from #9 already resolves every class's ancestry through the deriving file's scope — the fix is to stop throwing that edge away. SelectPersistentClasses now keeps the resolved parent per accepted class, computed after the fixed point, because a parent may be accepted rounds after the class deriving from it. Nothing is resolved a second way: a bare-name parent map would have been the mistake the Contracts decoys exist to catch.

Folding runs after the partial merge, so a parent's property set is complete before it is folded into anyone, and folds the parent first, so a chain of any depth arrives complete. Each listing is a copy: the same declared property appears under every descendant, and each copy names its own declarer — on MainDemo, Employee.Photo says Party, not Person, even though Employee reaches it through Person.

A property the class redeclares is its own: the inherited one is not added beside it. A new fixture class, LabeledOrder : Order redeclaring Number, pins that — one Number, the redeclaration.

The two decisions #13 left open

  1. Marked as such — yes. ExtractedProperty.InheritedFrom, null for a property the entity declares itself. It is data only for now: no renderer prints it yet, because how "(from AuditedObject)" reads in three renderers and two languages felt like your call, and the field is everything a renderer needs whenever you make it.
  2. The abstract base itself — captured, not decided. IsAbstract is read from syntax and survives the partial merge; the base's own heading is unchanged. Folding does make its duplication question sharper, and what an abstract base's heading should say is editorial.

Also deliberately out of scope: rules and associations an entity inherits are still listed only under their declarer — that is #14, filed separately for the reason #13 was filed out of #9.

Verification

Against the 26.1 demos and the 196-entity legacy application from #1, comparing this branch to main per entity:

Corpus Entities Folded properties Invariant
FeatureCenter.NET.XPO 146 = 146 151 own counts unchanged for all 146
MainDemo.NET.XPO 17 = 17 26 Employee 27 = 15 own + Party's 5 + Person's 7, exact against source
private legacy LIMS 196 = 196 0 correct — its shared base declares lifecycle methods and an ObjectSpace field, no properties; a base holding no columns folds nothing

On every corpus the entity sets are identical to main and each entity's total minus its inherited count equals main's count — the fold only adds, never loses, never duplicates.

295 tests, 0 warnings. The five new tests fail on main for the stated reasons; the only existing test that moved was the README's test-count claim.

An entity found through its base was reported with only the properties
it declares itself: PriorityOrder listed Rank and omitted the Name and
Number it persists. The base walk already resolves every ancestor;
keeping that edge lets each ancestor' properties fold into the entity,
root down, marked with the class that declared them. A redeclared
property stays the entity' own, and the abstract base is now marked
abstract so a renderer can say which heading is a table.

Closes peopleworks#13.
Comment on lines +583 to +592
foreach (var candidate in candidates)
{
if (!accepted.Contains((candidate.Namespace, candidate.Name)))
continue;

// Only one part of a partial class declares the base list; the parts that declare
// none must not erase the edge that part resolved.
if (ResolveBase(candidate.Declaration, candidate.Scopes, accepted) is { } parent)
parents.TryAdd((candidate.Namespace, candidate.Name), parent);
}
Comment on lines +1017 to +1026
foreach (var property in parent.Properties)
{
if (own.Contains(property.Name))
continue;

var copy = property.Clone();
// The declarer, not the parent: what the parent itself inherited keeps its origin.
copy.InheritedFrom ??= parent.ClassName;
inherited.Add(copy);
}
@peopleworks
peopleworks merged commit fd7d5ff into peopleworks:main Aug 14, 2026
6 checks passed
peopleworks added a commit that referenced this pull request Aug 14, 2026
#15 landed without an entry, and it is the largest user-visible change in the
batch: every entity's property list is different. The release notes for 0.13.0
are assembled from [Unreleased], so an omission here is an omission there.

One bullet rather than two. The window where an entity's own columns fell out
of a five-slot summary existed only between the fold landing and this branch --
it was never released, and a changelog that documents it is describing a
version nobody can install.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@peopleworks

Copy link
Copy Markdown
Owner

Merged. The extraction half of this holds up under everything I could think to throw at it — throwaway fixtures rather than a reading of the diff, and the parents map survived all of them:

  • a partial class whose columns live in the part that declares no base list: Vendor folds LegalName<Party>, so the TryAdd over every candidate does exactly what its comment claims
  • a generic base closed over itself, Document : VersionedObject<Document>: folds Version<VersionedObject> and the audit columns above it, two hops and a generic in between
  • declarer marking through a chain: the audit columns under Vendor say AuditedObject, not Party

What the probe did find is one door down from where you were looking, and it is not a defect in this change — it is the consequence of the decision you handed over.

Two summaries take the first few properties of the list. The full listings read root down, the way the class does, and that is right. But a five-slot table sharing its width with a six-column audit base spends all five on the base:

Invoice — notable properties
before Number, IssuedOn, Total, Customer
after CreatedBy, CreatedOn, ChangedBy, ChangedOn, RowVersion

Not one of its own columns survived. Across a seven-entity fixture, six rows carried the identical five names — a table whose whole job is to tell entities apart, unable to. One entity kept a single column by accident, because a required property outranks the rest.

You wrote that InheritedFrom is everything a renderer needs whenever the call gets made. That was right, and it got used within hours — just not the way either of us expected: not printed, sorted by. Own columns first, then the inherited ones in their existing ranking, filling whatever slots are left. #16.

The part worth keeping is why your verification could not have caught it. Your three corpora fold about one property per entity (FeatureCenter 151 over 146), 26 over 17, and zero — the 196-entity one has a base that declares no columns at all. All thin. The shape that hurts is the one #13 itself named, an audit base wider than the entities under it, and no fixture had it. That is now AuditedXpoSolution. A fixture can contain the pattern and still miss the defect if its proportions are wrong, which is the more useful half of this for both of us.

One housekeeping note: this landed without a CHANGELOG entry, and it is the largest user-visible change in the batch — every entity's property list is different. #16 adds it, as one bullet rather than two, since the window where an entity's own columns fell out of a summary existed only between your merge and that branch and was never released.

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.

Inherited properties are not folded into the entity that inherits them

4 participants