diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5160a14..d2f28b7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -56,6 +56,26 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
was added for. `FeatureCenter.NET.XPO` gains `OidGenerator`, `NoKeyPropertyNamedBaseObject` and
`LayoutDemoObject`.
+- **An entity carries the properties it inherits.** A class found through a base declared in the
+ same project was reported with only the columns it declares itself: `PriorityOrder` listed
+ `Rank` and omitted the `Name` and `Number` it persists. Finding those classes at all is what
+ 0.12.1 was about, and it converted a silent omission into a stated one — the entity now appeared
+ under a heading presenting the application's tables, with two thirds of its columns absent, in a
+ document that tells an agent its inventories are complete. At scale it is the shared base that
+ hurts: an application on an `AuditedObject` lost whatever that base holds from *every* entity,
+ which is normally the audit fields an agent most needs to know it must not set by hand. Each
+ entity now folds in its ancestors' properties in declaration order from the root down, each
+ marked with the class that declared it; a property the class redeclares stays its own, and the
+ abstract base is marked as abstract. `FeatureCenter.NET.XPO` folds 151 properties over 146
+ entities, `MainDemo.NET.XPO` 26 over 17 — where `Employee` reaches `Photo` through `Person` and
+ is correctly told it comes from `Party`.
+
+ Summaries of fixed width name an entity's **own** columns first. The full listings read root
+ down, the way the class does, but a five-slot table sharing its width with a six-column audit
+ base spends every slot on the base — and then every row of the entity table names the same
+ columns and none of the ones that tell one entity from another. Rules and associations an entity
+ inherits are still listed only under the class that declares them (#14).
+
## [0.12.1] — 2026-08-13
Entities the application declares, rather than the ones that inherit from the right class.
diff --git a/README.md b/README.md
index c4afe9a..f3a8620 100644
--- a/README.md
+++ b/README.md
@@ -317,7 +317,7 @@ applications. The agent-facing surface is what is landing now, in the open.
| ✅ | Pluggable publishing targets (`IDocumentationSink`) |
| ✅ | **MCP server** — 10 tools, live against your source |
| ✅ | **Installable Claude Code plugin** with skill and MCP server |
-| ✅ | **295 tests** over synthetic XPO and EF Core fixtures — no DevExpress needed |
+| ✅ | **299 tests** over synthetic XPO and EF Core fixtures — no DevExpress needed |
| ✅ | **DevExpress ground-truth catalog**, generated locally by licensees |
PeopleWorks Copilot, where this tool grew up, is now one sink among several rather than the
diff --git a/src/XafLogicExplainer.Core/Generators/AgentContextGenerator.cs b/src/XafLogicExplainer.Core/Generators/AgentContextGenerator.cs
index 93f6080..95014af 100644
--- a/src/XafLogicExplainer.Core/Generators/AgentContextGenerator.cs
+++ b/src/XafLogicExplainer.Core/Generators/AgentContextGenerator.cs
@@ -278,9 +278,15 @@ private static void WriteEntityInventory(StringBuilder sb, ExtractedProject proj
foreach (var entity in project.Entities.OrderBy(e => e.ClassName, StringComparer.Ordinal))
{
+ // What the entity declares comes first. The full list keeps the order the class reads
+ // in, root down, but five slots shared with a shared base spend them all on the base:
+ // every row of an audited application then names the same audit columns and none of
+ // the columns that tell one entity from another. A row that cannot discriminate is
+ // what this table exists to do.
var notable = entity.Properties
.Where(p => !p.IsCollection)
- .OrderByDescending(p => p.IsKey)
+ .OrderByDescending(p => p.InheritedFrom is null)
+ .ThenByDescending(p => p.IsKey)
.ThenByDescending(p => p.IsRequired)
.ThenByDescending(p => p.IsComputed)
.Take(5)
diff --git a/src/XafLogicExplainer.Core/Generators/MarkdownDocumentationGenerator.cs b/src/XafLogicExplainer.Core/Generators/MarkdownDocumentationGenerator.cs
index 3b3273b..8db90f8 100644
--- a/src/XafLogicExplainer.Core/Generators/MarkdownDocumentationGenerator.cs
+++ b/src/XafLogicExplainer.Core/Generators/MarkdownDocumentationGenerator.cs
@@ -782,7 +782,16 @@ private DocumentSection GenerateNavigationSection(ExtractedProject project)
if (!string.IsNullOrEmpty(entity.Description))
sb.AppendLine($"*{entity.Description}*");
sb.AppendLine();
- sb.AppendLine($"- **{_l.MainProperties}:** {string.Join(", ", entity.Properties.Where(p => !p.IsCollection && p.VisibleInListView).Take(8).Select(p => p.Name))}");
+
+ // Own columns first: eight slots shared with a base the whole application derives
+ // from would otherwise name the base's columns under every entity in the menu.
+ var main = entity.Properties
+ .Where(p => !p.IsCollection && p.VisibleInListView)
+ .OrderByDescending(p => p.InheritedFrom is null)
+ .Take(8)
+ .Select(p => p.Name);
+
+ sb.AppendLine($"- **{_l.MainProperties}:** {string.Join(", ", main)}");
if (entity.Relationships.Count > 0)
sb.AppendLine($"- **{_l.Relationships}:** {string.Join(", ", entity.Relationships.Select(r => $"{r.PropertyName}→{r.RelatedEntity}"))}");
sb.AppendLine();
diff --git a/tests/XafLogicExplainer.Tests/Fixtures/AuditedXpoSolution/SampleAudited.Module/BusinessObjects/AuditedObject.cs b/tests/XafLogicExplainer.Tests/Fixtures/AuditedXpoSolution/SampleAudited.Module/BusinessObjects/AuditedObject.cs
new file mode 100644
index 0000000..dbd1102
--- /dev/null
+++ b/tests/XafLogicExplainer.Tests/Fixtures/AuditedXpoSolution/SampleAudited.Module/BusinessObjects/AuditedObject.cs
@@ -0,0 +1,52 @@
+using DevExpress.Persistent.BaseImpl;
+using DevExpress.Xpo;
+
+namespace SampleAudited.Module.BusinessObjects;
+
+///
+/// The audit base an application writes once and derives every entity from.
+///
+///
+/// Wider than the columns of the entities below it, which is the ordinary shape: audit bases
+/// accumulate. It is what makes a fixed-size summary a decision about whose columns get named.
+///
+public abstract class AuditedObject : BaseObject
+{
+ public AuditedObject(Session session) : base(session) { }
+
+ public string CreatedBy
+ {
+ get => GetPropertyValue(nameof(CreatedBy));
+ set => SetPropertyValue(nameof(CreatedBy), value);
+ }
+
+ public DateTime CreatedOn
+ {
+ get => GetPropertyValue(nameof(CreatedOn));
+ set => SetPropertyValue(nameof(CreatedOn), value);
+ }
+
+ public string ChangedBy
+ {
+ get => GetPropertyValue(nameof(ChangedBy));
+ set => SetPropertyValue(nameof(ChangedBy), value);
+ }
+
+ public DateTime ChangedOn
+ {
+ get => GetPropertyValue(nameof(ChangedOn));
+ set => SetPropertyValue(nameof(ChangedOn), value);
+ }
+
+ public int RowVersion
+ {
+ get => GetPropertyValue(nameof(RowVersion));
+ set => SetPropertyValue(nameof(RowVersion), value);
+ }
+
+ public string AuditNotes
+ {
+ get => GetPropertyValue(nameof(AuditNotes));
+ set => SetPropertyValue(nameof(AuditNotes), value);
+ }
+}
diff --git a/tests/XafLogicExplainer.Tests/Fixtures/AuditedXpoSolution/SampleAudited.Module/BusinessObjects/Invoice.cs b/tests/XafLogicExplainer.Tests/Fixtures/AuditedXpoSolution/SampleAudited.Module/BusinessObjects/Invoice.cs
new file mode 100644
index 0000000..9337a35
--- /dev/null
+++ b/tests/XafLogicExplainer.Tests/Fixtures/AuditedXpoSolution/SampleAudited.Module/BusinessObjects/Invoice.cs
@@ -0,0 +1,32 @@
+using DevExpress.Persistent.Base;
+using DevExpress.Persistent.Validation;
+using DevExpress.Xpo;
+
+namespace SampleAudited.Module.BusinessObjects;
+
+/// An entity whose own columns are outnumbered by the ones it inherits.
+[DefaultClassOptions]
+[NavigationItem("Billing")]
+public class Invoice : AuditedObject
+{
+ public Invoice(Session session) : base(session) { }
+
+ [RuleRequiredField("Invoice_Number_Required", DefaultContexts.Save)]
+ public string Number
+ {
+ get => GetPropertyValue(nameof(Number));
+ set => SetPropertyValue(nameof(Number), value);
+ }
+
+ public DateTime IssuedOn
+ {
+ get => GetPropertyValue(nameof(IssuedOn));
+ set => SetPropertyValue(nameof(IssuedOn), value);
+ }
+
+ public decimal Total
+ {
+ get => GetPropertyValue(nameof(Total));
+ set => SetPropertyValue(nameof(Total), value);
+ }
+}
diff --git a/tests/XafLogicExplainer.Tests/Fixtures/AuditedXpoSolution/SampleAudited.Module/BusinessObjects/Receipt.cs b/tests/XafLogicExplainer.Tests/Fixtures/AuditedXpoSolution/SampleAudited.Module/BusinessObjects/Receipt.cs
new file mode 100644
index 0000000..6d91220
--- /dev/null
+++ b/tests/XafLogicExplainer.Tests/Fixtures/AuditedXpoSolution/SampleAudited.Module/BusinessObjects/Receipt.cs
@@ -0,0 +1,31 @@
+using DevExpress.Persistent.Base;
+using DevExpress.Xpo;
+
+namespace SampleAudited.Module.BusinessObjects;
+
+///
+/// The same shape without a required column.
+///
+///
+/// Invoice keeps one of its own columns in a five-slot summary by accident, because a
+/// required property outranks the rest. An entity with no such column keeps none — which is the
+/// case that shows the ranking was never the thing holding the row together.
+///
+[DefaultClassOptions]
+[NavigationItem("Billing")]
+public class Receipt : AuditedObject
+{
+ public Receipt(Session session) : base(session) { }
+
+ public string Reference
+ {
+ get => GetPropertyValue(nameof(Reference));
+ set => SetPropertyValue(nameof(Reference), value);
+ }
+
+ public decimal Amount
+ {
+ get => GetPropertyValue(nameof(Amount));
+ set => SetPropertyValue(nameof(Amount), value);
+ }
+}
diff --git a/tests/XafLogicExplainer.Tests/InheritedPropertySummaryTests.cs b/tests/XafLogicExplainer.Tests/InheritedPropertySummaryTests.cs
new file mode 100644
index 0000000..33309ae
--- /dev/null
+++ b/tests/XafLogicExplainer.Tests/InheritedPropertySummaryTests.cs
@@ -0,0 +1,77 @@
+using System.Text.RegularExpressions;
+using XafLogicExplainer.Core.Generators;
+
+namespace XafLogicExplainer.Tests;
+
+///
+/// What a summary of fixed width names once an entity carries its base's columns.
+///
+///
+/// Folding inherited properties () gives every entity the
+/// columns it persists, in the order the class reads them — root down. Two summaries then take the
+/// first few of that list, and on an application with a shared audit base the first few are the
+/// base's for every entity at once: the tables that exist to tell entities apart end up naming the
+/// same columns in every row, and none of the ones that differ.
+///
+/// The full listings are unaffected and still read root down. This is only about the summaries,
+/// where the width is the whole constraint.
+///
+///
+public class InheritedPropertySummaryTests
+{
+ private static string Index =>
+ new AgentContextGenerator("0.9.0").GenerateIndex(SampleProjects.AuditedXpo, []);
+
+ [Fact]
+ public void AnEntitysOwnColumnsSurviveAFixedWidthSummary()
+ {
+ // Receipt declares two columns and inherits six. Ranked by nothing but position, the six
+ // fill all five slots and the reader learns that a receipt has a CreatedBy.
+ Assert.Equal(["Reference", "Amount", "CreatedBy", "CreatedOn", "ChangedBy"], Notable("Receipt"));
+ }
+
+ [Fact]
+ public void RequiredColumnsStillOutrankTheRestOfTheEntitysOwn()
+ {
+ // Own-before-inherited is the outer sort; the existing ranking still orders each group.
+ Assert.Equal(["Number", "IssuedOn", "Total", "CreatedBy", "CreatedOn"], Notable("Invoice"));
+ }
+
+ [Fact]
+ public void TheBaseItselfStillNamesItsOwnColumns()
+ {
+ // The base declares all six, so the rule must not be "hide inherited" -- it is "declare
+ // first". A base whose row went blank would be the same defect facing the other way.
+ Assert.Equal(["CreatedBy", "CreatedOn", "ChangedBy", "ChangedOn", "RowVersion"], Notable("AuditedObject"));
+ }
+
+ [Fact]
+ public void TheNavigationSummaryNamesTheEntitysOwnColumnsFirstToo()
+ {
+ // The same eight-slot summary, in the documents published to a Copilot rather than to a
+ // coding agent -- one fix per reader, or the reader who gets missed keeps the defect.
+ // Generated in the default language, which is Spanish; the ordering is not translated.
+ var sections = new MarkdownDocumentationGenerator().GenerateSections(SampleProjects.AuditedXpo);
+ var navigation = sections.Single(section => section.Content.Contains("#### Receipt"));
+
+ var line = navigation.Content
+ .Split('\n')
+ .SkipWhile(text => !text.StartsWith("#### Receipt", StringComparison.Ordinal))
+ .First(text => text.Contains("Propiedades principales", StringComparison.Ordinal));
+
+ Assert.StartsWith(
+ "- **Propiedades principales:** Reference, Amount, CreatedBy",
+ line.Trim(),
+ StringComparison.Ordinal);
+ }
+
+ /// The property names in one row of the agent context's entity table.
+ private static List Notable(string className)
+ {
+ var row = Index
+ .Split('\n')
+ .Single(line => line.StartsWith($"| **{className}** ", StringComparison.Ordinal));
+
+ return [.. Regex.Matches(row.Split('|')[3], "`([^`]+)`").Select(match => match.Groups[1].Value)];
+ }
+}
diff --git a/tests/XafLogicExplainer.Tests/SampleProjects.cs b/tests/XafLogicExplainer.Tests/SampleProjects.cs
index ae4b7cd..ac4ef26 100644
--- a/tests/XafLogicExplainer.Tests/SampleProjects.cs
+++ b/tests/XafLogicExplainer.Tests/SampleProjects.cs
@@ -40,6 +40,10 @@ internal static class SampleProjects
/// Path to the XPO fixture whose entities derive through a shared base.
public static string DeepXpoPath => Path.Combine(FixturesRoot, "DeepXpoSolution", "SampleDeep.Module");
+ /// Path to the XPO fixture whose audit base is wider than its entities.
+ public static string AuditedXpoPath =>
+ Path.Combine(FixturesRoot, "AuditedXpoSolution", "SampleAudited.Module");
+
/// Path to the fourteen-entity demo module.
public static string DemoPath => Path.Combine(FixturesRoot, "DemoSolution", "PharmacyDemo.Module");
@@ -96,6 +100,7 @@ private static string FixturesRoot
private static readonly Lazy LazyNoOrm = new(() => Extract(NoOrmPath));
private static readonly Lazy LazyDeepXpo = new(() => Extract(DeepXpoPath));
+ private static readonly Lazy LazyAuditedXpo = new(() => Extract(AuditedXpoPath));
/// The XPO sample: Customer, Order, OrderLine, one controller, seed data, xafml.
public static ExtractedProject Xpo => LazyXpo.Value;
@@ -122,6 +127,16 @@ private static string FixturesRoot
/// An XPO application whose entities reach BaseObject through a shared base.
public static ExtractedProject DeepXpo => LazyDeepXpo.Value;
+ ///
+ /// An XPO application on an audit base wider than the entities that derive from it.
+ ///
+ ///
+ /// Kept apart from , which asserts exact property lists on a base holding
+ /// a single column. What this fixture is for is the opposite proportion — the one where a
+ /// summary of fixed width has to choose between an entity's columns and its base's.
+ ///
+ public static ExtractedProject AuditedXpo => LazyAuditedXpo.Value;
+
///
/// The fourteen-entity demo, with a custom editor in a sibling platform project.
///