Releases: peopleworks/XAFLogicExplainer
Release list
v0.17.0 — the question one application cannot answer
The question one application cannot answer
Everything here so far reads one XAF application and explains it. That is the right unit for inheriting a codebase, and the wrong unit for the person who wrote forty of them.
Someone who has delivered XAF to clients for ten years has a question no single-application tool can be asked: have I built this before? Somewhere back there is the class about to be modelled again — not a similar one, the same one, thought through properly, with the two properties this time will forget.
xaflogic projects add --name Legal --project "C:\Clients\Legal\pwLegalOffice.Module"
xaflogic projects add --name Budget --project "C:\Clients\Budget\PWPresupuesto.Module"
xaflogic wiki --openOne self-contained HTML file over all of them. No server, no build step, no request to the network.
What it computes
Every shared class is placed between the applications that model it — at the average direction of those applications, at a distance set by how much they agree. A class every application has belongs to no one direction, so it falls to the centre. The picture reads before the caption does: the middle is your common ground, the rim is the work that belongs to one client.
| Classes modelled more than once | Property by property, a column per application, richest first |
| The layer you wrote yourself | Base classes carried between applications — your own framework |
| The same name, two shapes | Total a decimal here and a double there |
| Names you keep | The vocabulary nobody wrote down |
| Which two are most alike | A grid you click to hold the page to just those two projects |
| The releases you are on | Your DevExpress spread, with the one your catalog describes marked |
There is nowhere in that page to type a sentence about the collection. That is deliberate: a hand-written summary of nine applications is wrong the day the tenth is added, and nobody notices.
What running it over six real applications found
405 entities, 111 controllers. Two findings worth the release on their own.
The same property name meaning two different scalar shapes. Total a decimal in one application and a double in another — and the same for UnitPrice, Cantidad, Descuento and Latitude. Nothing is broken. Everything compiles. It is how a total ends up two cents out.
Zero shared base classes across all six. Every application rebuilt from XPO primitives. That zero is not an empty result, it is the finding, and the page says so rather than showing a heading over nothing.
Two of the quality decisions came from running against those applications rather than against fixtures, because fixtures agree with whatever the code already does. Double and double are one type, and reporting them as a disagreement is a false accusation — a tool that makes one stops being believed about the true ones. And a name holding a different collection in each entity is vocabulary rather than a conflict; leaving those in buried decimal against double under seven rows of XPCollection<T>.
A third came from opening the page instead of reading the diff: the comparison table silently cropped its last column, which is the failure that matters, because a reader would have believed the columns they could see.
What it will not claim
- Classes are matched by name. Two
Clienteclasses in two solutions may model different things — the comparison beside them is what tells you whether they share an idea. - A base class is listed as yours only when its own source was read in one of the projects. No list of DevExpress type names is involved, so nothing goes stale when DevExpress renames something — and a base class in a library you did not add to the wiki is absent, not framework.
- Report counts stay lower bounds, per application, exactly as in 0.16.0.
Also in this release
xaflogic projects add no longer requires --resource-name. It names a PeopleWorks Copilot resource — one publishing target among several, and irrelevant to wiki, explain, agents and mcp, all of which read the configured list and write locally.
Four generators carried a version number as a default: the explainer stamped 0.10.1 six releases after 0.10.1 shipped, and the writer of AGENTS.md stamped 0.9.0 eight releases on. Nothing failed, because a default every caller overrides is a default nobody rereads. They now say of unknown version, and a test refuses any generator default shaped like a version.
514 tests, over synthetic fixtures — no DevExpress licence needed to run them.
Full detail in the changelog.
v0.16.0 — what we cannot see, said out loud
What we cannot see, said out loud.
Every release so far made the same kind of promise: here is something in your application you could
not see from the code. This one keeps that promise — reports are read now, the largest gap left in
the extraction — and then does the opposite thing, which turns out to matter more.
An XAF application's reports are frequently not in its repository at all. With ReportsModuleV2
registered, users design reports at run time and they are stored as rows in a database, out of reach
of anything that reads source. An application with forty reports and none in its code is not an edge
case; it is what a reporting setup looks like once people use it. So the answer had to stop being a
list and start being three different sentences: the list is all of them, the list is a lower
bound, or the number is unknown rather than zero — and that last one is the common case, the
one where a tool that answers confidently does real damage. An agent told an application has no
reports designs as though none can exist.
The same correction landed on the framework catalog, where it had been wrong for three releases.
The newest catalog on the machine was used for every application whatever version it targeted, and
the result said "these controllers load onto this screen" in one confident sentence either way. On a
machine holding a single 26.1 catalog that sentence was produced for a 23.2 application and for a
17.1 one, unqualified. It now asks for the release the application declares, still falls back when
that catalog is absent, and says which wherever a framework fact is reported.
Both halves of the reports work are a collaboration: [@MBrekhof] extracted them, this side rendered
them and wrote the bound. Two things only the generated document could show were caught that way —
a citation printing an absolute path from the machine that ran the extraction, and one layout's
filter and GetCriteria() printed twice when two registrations shared it.
Five spellings of a DevExpress version are read now, because seven real applications were checked
instead of fixtures: PackageReference, a floating 25.2.*-*, an MSBuild property (what
DevExpress's current template generates), and a pre-NuGet assembly reference from 17.1. Three of
those seven module folders hold more than one .csproj, which had been resolved by whatever order
the file system returned.
457 tests, zero warnings.
Added
-
The reports are written down, and the list says what it is ([#37], the rendering half). They
now appear in the Markdown pages, inAGENTS.md, and through a newxaf_reportsMCP tool — each
with what it is over, the filter inside its layout, its calculated fields and bound expressions,
and the parameters dialog it opens with, including theGetCriteria()that turns the answers into
a filter.The sentence under the heading carries more than the list. With
ReportsModuleV2registered,
users design reports at run time and those are stored as database rows, so an application with
forty of them and none in its repository is the ordinary case: checked against a production
application that registers the module, setsReportStoreMode.XML, and contains no report in
source at all. Printing "no reports" there is not an incomplete answer but a wrong one, and the
more use an application makes of reports the wronger it gets. Three states are told apart — the
module absent and the list complete, the module present and the list a lower bound, and the
module present with nothing in source, where the number is reported as unknown rather than zero.
An application that registers nothing and ships no layout gets no section, because there "no
reports" is the default rather than a finding.Two things the generated document showed that reading the code would not have. A layout kept
beside the module rather than inside it was cited by its absolute path — the drive of whichever
machine ran the extraction, in a file meant to be committed; citations now fall back to the
solution root and then to the file name, never to a path that is wrong everywhere but here. And
two registrations sharing one.repxprinted the filter, the bindings and the whole of
GetCriteria()twice, burying the only thing that differs between them; the second now points at
the first. -
The reports an application declares are read ([#37], phases 1–3 — the extraction; the
Markdown and MCP rendering follow separately). Reports V2 leaves four kinds of trace in a
repository, all syntax, and none of them was read: the registration
(PredefinedReportsUpdater.AddPredefinedReport<T>, in the sameGetModuleUpdatersthe updater
already lives in), the layout (designer code, a.repx, or a constructor), and the parameters
dialog (ReportParametersObjectBase, whoseGetCriteria()is business logic in the plainest
sense).ExtractedProject.Reportsnow carries each registration with what the call says and
nothing it does not —IsInplaceReportis null when the overload is silent — plus the layout's
filter, bound expressions, grouping, calculated fields and parameters, and the dialog's fields
and criteria source.ReferencesReportsModuleis the flag the rendering needs for the sentence
that makes the list safe to believe: withReportsModuleV2in, users build reports at run time
that live only in the database, and the list is a lower bound.
Three things the real files taught, each pinned by the newReportsSolutionfixture: a.repx
parameter's type resolves through<ObjectStorage>; a layout exported from the running
application keeps its data source there too; and a shop that designs reports outside Visual
Studio keeps the exports beside the module, registered by nothing — so every.repxin the
solution is read, and the unclaimed ones are listed as unregistered rather than skipped.
Checked against three licensed applications, one of which registers through reflection over a
catalog: a syntactic reader correctly sees zero registrations there, the module referenced, and
the fourteen layouts and two parameters objects it would otherwise have hidden.
Fixed
-
The framework catalog is chosen by the version the application declares, not by file date.
LoadLatest()took the newest catalog on the machine whatever the application targeted, and the
output then reported which framework controllers load onto a screen in the same confident sentence
either way. On a machine holding a single 26.1 catalog, that sentence was produced for a 23.2
application and for a 17.1 one — three releases and nine years out — with nothing to tell the
reader. Reported by the external review at 0.12.0 and live until now.Extraction now reads the DevExpress
major.minorfrom the project file and asks for that
release's catalog. When it is not on the machine the newest is still used — most of the framework
is stable across releases, and withholding it would trade real information for a small error — but
the difference is now stated wherever a framework fact is reported: the Markdown and HTML pages,
theAGENTS.mdheader, and the MCP view detail.Both project-file spellings are read. A pre-NuGet XAF project has no
PackageReferenceat all and
names its version only inside the assembly reference (DevExpress.ExpressApp.Xpo.v17.1) — which is
exactly the case where the mismatch is widest, so reading only the modern spelling would have
missed the applications that need this most.Two further spellings were found by running this against seven real applications rather than
against fixtures, and both returned "declares nothing" until they were handled. A version written
as an MSBuild property —<DevExpressVersion>25.2.7</DevExpressVersion>with
Version="$(DevExpressVersion)"— is what DevExpress's current template generates, so it is not
an exotic case to tolerate but what a project created today looks like; properties a project file
sets are now resolved before anything reads a version, which also stops$(DevExpressVersion)
reaching the rendered package list. A floating25.2.*-*already worked and is now pinned. -
The project file a module folder is named for is the one that is read. With several
.csprojside by side — a.Net10.csprojfrom a framework migration, a hand-made
" - Backup.Module.csproj"— extraction took whichever the file system returned first, so the
target framework, the package list and the DevExpress version could come from a backup, and two
machines could describe one repository differently with nothing in the output to say why. Three
of the seven applications checked have more than one.Verified end to end against applications declaring 17.1, 23.2, 25.1, 25.2 and 26.1 — including
every case above, and one whose version matches the installed catalog, where the new sentence
correctly says nothing at all.
v0.15.0 — how it works, not only what exists
How it works, not only what exists.
Every release until now answered the same shape of question. What entities does this application
have. What does this controller do. What does that rule forbid. All of it true, all of it
declarations — and a business process is not a declaration. It is a path across several of them, and
nothing in the extractor walked a path.
xaflogic walkthrough --from ApproveOrder walks it. The scope is computed, by a bounded
breadth-first traversal, and not chosen by a language model — because a model asked what belongs in
"the approval process" answers authoritatively, in a form nobody can check, and is wrong in the
places that look exactly like the places it is right. The Mermaid diagram is emitted from that
traversal arrow for arrow, for the same reason and more sharply: a diagram is believed at a glance,
so an invented edge in one is worth less than no diagram at all.
The two halves that make it honest are the ones that took the most care. A call the walk cannot
follow is printed rather than dropped — a virtual method is followed to the declaration written
beside the call and reported with every override that may replace it, and the document says outright
that the bodies it never entered mean entities missing from the account. And --since reports what
changed in that one process against a stored snapshot, which is the question no conversational
agent can answer, because none of them has a yesterday.
--narrate is opt-in and, deliberately, the least load-bearing thing here. The model receives the
numbered steps and the code behind them; the only prose that reaches a reader is a paragraph it
managed to key to a step that exists. A document generated with an empty narration is
byte-for-byte the one generated with none.
Also in this release: every declaration now says where it is, file:line, in the extraction and in
the MCP tools — the foundation the walkthrough needed and worth having on its own, since the tools
used to hand an agent a name and leave it to search for it. Four appearance-rule defects, two of
them found by [@MBrekhof]. Any model can now answer, and a key is enough.
And the Markdown we generate is Markdown, which took two goes. A seed method's source was wrapped in
a <details> fold that collapses on GitHub and nowhere else ([#28]). Then, running the output
through a real Word converter, [@MBrekhof] found the second half of the same defect: a generic base
type was written bare, and <DetailView> is an inline HTML tag rather than a block, so an export
drops it and github.com's sanitizer strips it — every controller read as deriving from plain
ViewController ([#36], fixed in [#39]). Type names are written as code now, and the guard was
widened from lines that open with < to a tag anywhere on a line. The route from an extraction to
a Word document is written down in the README, and there is deliberately no exporter here: reading
an XAF application needs no DevExpress, and that stays true.
402 tests, zero warnings.
Added
-
Entities, controllers, actions and methods say where they are declared ([#23], first step).
The extraction knew which file a class was in and nothing narrower, soxaf_entityand
xaf_controllerhanded an agent a name and left it to search the file for it. Each now carries a
one-based line, taken from the identifier token rather than from the declaration's span — a
span begins at the first attribute, andCustomersits behind a doc comment and four of them, so
the two answers are four lines apart and only one of them is the line anybody means. Actions and
methods carry their own file as well as their own line, because a partial controller's members
need not be declared in the file the controller is cited at. The MCP tools name the file once and
then cite members by line alone; a member in a different file is given in full, which is exactly
the case where a reader would otherwise open the wrong one. Prerequisite for the walkthrough,
where every claim is supposed to carry afile:linea reader can check. -
The walk that decides what belongs to one process ([#23], phase 1).
ProcessSlice.Fromtakes
an action, a controller method, a controller or an entity by name and walks outward from it —
breadth-first, bounded by depth, syntax-only like everything else here. It follows an action to
the handler it runs, a method to the methods it calls and the entities it names, a controller to
the entities it is activated for, and an entity to the rules that govern it. Every node carries
the file and line a reader can open.
The scope is computed rather than asked for, and that is the governing decision: a model asked
what belongs in "the approval process" answers authoritatively, in a form nobody can check, and is
wrong in the places that look exactly like the places it is right.
A call it cannot follow is printed, not dropped. A virtual declaration is followed to what is
written beside the call, and reported with every override that may replace it — which is honest
about the consequence, because the bodies that were not entered mean entities missing from the
slice. So is the depth bound: a walk that ran out of things to reach is a whole process, and a
walk that stopped at its limit is a view of one, and rendering them identically is how a document
claims completeness it does not have.
Methods now recordvirtual/abstractandoverride, which is what makes that distinction
possible at all. Nothing consumes the slice yet — the Mermaid diagram, the document, the CLI
command and the MCP tool are phase 2. -
xaflogic walkthroughandxaf_walkthrough([#23], phase 2). The slice becomes a document,
and the feature is now usable: a Mermaid diagram, everything that takes part with the place it is
declared, the ordered steps each citing afile:line, the calls the walk could not follow, and
what the walk deliberately is not. In both languages, offline, with no API key and no network.
The diagram is emitted from the walk's own edge set — node for node, arrow for arrow. Nothing
decides what to draw. Ask a model for a Mermaid diagram of a process and it will produce one,
including edges that do not exist, drawn with a confidence indistinguishable from the true ones,
in a format whose whole value is that a reader believes it at a glance. A test counts the arrows
against the edges, because an invented one is exactly what a spot check of a diagram that looks
right would miss.
xaf_walkthroughis the first MCP tool that answers a question about a process. The other ten
return atoms, so an agent asked how something works has to guess which atoms to fetch and then
guess whether it has them all — and the guess that stops one atom early produces a confident
answer with a step missing from it. -
xaflogic walkthrough --narrate([#23], phase 3). Opt-in prose over a walk that has already
been computed: one paragraph on what the process is for, and a sentence or two under each step,
each sitting directly beneath the citation it belongs to.
The model narrates; it does not discover. It receives the numbered steps and the code behind
them, and the only thing that reaches a reader is a paragraph it managed to key to a step that
exists — a paragraph keyed to step 99 of a nine-step process is dropped before rendering, and so
is fluent prose attached to no step at all. The point is not that such a sentence would probably
be wrong; it is that nobody could check it, and an ordinary reader cannot tell a fluent sentence
about real code from a fluent sentence about code that is not there.
The model is also told what the walk could not follow, so it does not narrate its way over the one
gap the analysis already knows about.
Failure costs prose and not the document: no key, or a provider that does not answer, prints why
and writes the walkthrough anyway. Phases 1 and 2 stand entirely on their own, which is what makes
the model optional rather than load-bearing — and a test pins that a document generated with an
empty narration is byte-for-byte the one generated with none.
XafLogicExplainer.Corestill references nothing but Roslyn: narration arrives at the generator
as plain text keyed to steps that already exist. -
xaflogic walkthrough --since([#23], phase 4 — the last one). Re-walks the same process over
a stored snapshot and reports what is different about this process: a step added, a rule now
governing it, a branch gone, a body rewritten, and a call the trace can no longer follow. Reads
the same_Previous.jsonthatxaflogic diffdoes, or any snapshot given by path.
This is the part no conversational agent can imitate, because none of them has a yesterday. Asked
what changed in the commission calculation since the last release, a model can only re-read
today's code and describe it fluently.
Comparing two walks by their node sets alone would have missed the most ordinary change there is —
somebody edits a method body and leaves every call in it alone — so each node now carries a
fingerprint of its own substance: a method's body, a rule's condition and effect, an action's
caption and criteria. Whitespace is collapsed first, so reformatting is not reported as a change
of behaviour. A controller and an entity have no fingerprint; what matters about them is elsewhere
in the slice, and giving them one would report the same edit twice.
Three states that had to stay distinct: the process is unchanged, the process did not exist at the
snapshot, and no snapshot could be read — the last one stops the run rather than writing a
document with the section missing, because an absent section reads exactly like "nothing changed".
Fixed
v0.14.0 — everything that governs an entity, under the entity
Everything that governs an entity, under the entity.
0.13.0 gave each entity the columns it persists and stopped one door short. What is written on a property travelled with the property — a folded Number row correctly said required — and what is written on the class did not. So an entity's section could call a column required and, three headings later, document no rule requiring it: two halves of the same page disagreeing, with the property half telling the truth.
The rules were never missing from the application, only from the place a reader looks. A RuleCriteria on an audit base is enforced every time anything 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 were documented under the base alone — which, on a real application, means documented nowhere anybody reads.
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. InheritedFrom had been recorded on properties since 0.13.0 and displayed by nothing, so folding rules on top of that would have traded one defect for a quieter one: every rule visible, and no way to tell which of them changing would change the whole application.
On scale. #14 filed half of itself as genuinely debatable, and it turned out to be a question about scale rather than about relationships. An index, a count, a diagram and a search answer what does this application declare; an entity's section answers what governs this entity. Following the fold everywhere would have made every total a measurement of the class hierarchy — one rule on a base shared by two hundred entities, reported two hundred times. Each folded declaration carries the class that wrote it, and each rendering chooses.
Found while probing this. The four-argument rule attribute was misread. [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 in the suite passed its message as CustomMessageTemplate =, so all 299 tests agreed with the wrong answer. Rules now also keep their identifier and their validation contexts, which were read as arg0/arg1 and printed to the published documentation that way.
Thanks again to @MBrekhof, who filed #14 separately rather than growing the pull request it was found in.
A minor rather than a patch: ExtractedValidationRule gains Id and Contexts; ExtractedValidationRule, ExtractedAppearanceRule and ExtractedRelationship gain InheritedFrom and Clone(). Additive, and invisible to the CLI and the MCP server.
318 tests. Full notes in the changelog.
v0.13.0 — the columns those entities persist
The entities an application actually has, and all of the columns they actually persist.
Every fix here came from outside. @MBrekhof read the code before
filing, separated the reports by cause rather than by symptom, and kept finding the next one in
the review of the last — three issues and five pull requests, each of which turned out to be a
different way of asking the same question: what is this tool entitled to call an entity, and what
is it entitled to leave out.
The number that measures it, against the demos DevExpress ships with 26.1 rather than against our
own fixtures: FeatureCenter.NET.XPO 43 → 140 entities, MainDemo.NET.XPO 14 → 17,
OutlookInspiredDemo.NET.EFCore 23 → 24. Anyone evaluating this tool by pointing it at
FeatureCenter was seeing under a third of it — under an AGENTS.md telling their agent the
inventory was complete. That shape is the one thing this project exists to prevent, and it was
happening in the place a newcomer was most likely to look.
A minor rather than a patch: OrmType.Unknown is a new member on a public enum and
ExtractionOptions.BaseTypeNames changed its default. Neither affects the CLI or the MCP server;
both are breaking for code calling XafLogicExplainer.Core directly.
Changed
-
OrmType.Unknownis a new member of a public enum, returned whenever no evidence names an
ORM. Anyone consumingXafLogicExplainer.Coredirectly and switching exhaustively over
OrmTypehas a new case to handle; anyone using the CLI or the MCP server has nothing to do.
The same noteIControllerAnalyzer.AnalyzeControllerFilegot in 0.12.0, for the same reason —
on 0.x this is what a minor is for. -
ExtractionOptions.BaseTypeNamesis the single source of the list. The CLI, the MCP server
and the test harness each passed their own copy, so the default inCorewas four names while
every caller passed five — four copies with three chances to disagree about what an entity is.
The callers now use the default.
Fixed
-
Unknownnow reaches every place the ORM is reported. The agent files learned it; the HTML
explainer and the MCPxaf_overviewkept deciding in a binary with no third answer, so a project
whose ORM could not be determined was reported as XPO by both. The MCP one was the worse of
the two: it prints the ORM two lines above "These lists are complete, not sampled", from a tool
whose description tells the agent that anything absent does not exist in the application. All
three now go through oneOrmhelper — the defect was never the wrong answer, it was that three
places were each entitled to one. -
The ORM is read as syntax, and is
Unknownwhen nothing says. Detection scanned raw file
text forDevExpress.Persistent.BaseImpl.EFand fell through to XPO, so an EF Core application
whose entities do not use the DevExpress EF base implementation — a legacy schema, its security
tables in another project — was reported as XPO. That is not a hole in the document: ground rule
1 then tells the agent thatDbContext,DbSet<T>and EF migrations "do not exist in this
application and must never be suggested", which forbids the only correct answer. Signals are now
ranked by what it costs to be wrong about them — aDbSet<T>registered on a context first,
thenusingdirectives and base classes — and where neither ORM leaves a trace, the rule is
omitted rather than guessed. Reading text also counted a mention: a comment naming the
namespace was enough, which is how the fixture for this fix first passed against the old code. -
Entities are found through a base class the project wrote itself. Classification matched a
class's own base list against the root names and stopped there, so an application with a shared
base — auditing, a key convention, a display-name property — lost every business object below it.
The inversion is what makes it severe: the abstract base is matched, so the inventory reported
the one class that is not a table and omitted the ones that are. Selection now repeats until a
round changes nothing, exactly asSelectControllersdoes, and resolves a base name through the
deriving file's own scope rather than by simple name, so aContracts.Orderbeside a
BusinessObjects.Orderstill resolves to the base it actually named. On the demos shipped with
26.1:FeatureCenter.NET.XPO43 → 140 entities,MainDemo.NET.XPO14 → 17, and
OutlookInspiredDemo.NET.EFCore23 → 24 — the last of which is an EF Core application, where an
entity that is not registered as aDbSet<T>had no fallback either. -
PersistentBaseandXPBaseObjectare recognised as persistent bases. The XPO hierarchy is
PersistentBase→XPBaseObject→XPCustomObject→XPObject, withXPLiteObjectalso under
XPBaseObject. The list held the three leaves and neither of the classes above them, so a hole
sat in the middle of a documented API — DevExpress names all five as bases a persistent class may
derive from, and recommendsPersistentBase. Deriving from the higher bases is what you do when
the table brings its own key, which is the same population as the legacy schemas the DbSet roster
was added for.FeatureCenter.NET.XPOgainsOidGenerator,NoKeyPropertyNamedBaseObjectand
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:PriorityOrderlisted
Rankand omitted theNameandNumberit 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 anAuditedObjectlost 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.XPOfolds 151 properties over 146
entities,MainDemo.NET.XPO26 over 17 — whereEmployeereachesPhotothroughPersonand
is correctly told it comes fromParty.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).
Install
dotnet tool install -g XafLogicExplainer.Cli
dnx XafLogicExplainer.Mcp --yes -- --helpFull changelog: v0.12.1...v0.13.0
v0.12.1 — entities the application declares
Entities the application declares, rather than the ones that inherit from the right class.
The first release that came from outside. @MBrekhof pointed this tool at an XAF application of 221 entities over a legacy LIMS schema. It found three.
Not a rare shape, either: an application mapped onto tables that already exist rarely derives from BaseObject. The tables bring their own keys, so the project writes its own base class or maps a plain POCO — and EntityAnalyzer was matching class declarations against four XPO-era base type names, none of which such a project will ever use.
What makes that the worst kind of error this project can make is what happens next. The generated AGENTS.md goes on to state that its inventories are complete, and instructs the agent to answer that anything absent does not exist in this application. So the failure is not a gap in a document. It is an agent confidently wrong about which tables exist.
Under EF Core the application already declares what it persists, in the one place that has to be correct for it to run at all: the DbSet<T> properties of its DbContext. That roster is now read, and it needs no compilation and no new heuristic. On the reporter's application, extraction goes from 3 entities to 210 — the remaining 11 being framework tables the DbContext also registers, correctly excluded.
A name is not an identity
Three of the five fixes here were found reviewing that change, and they are all the same rule read in the other direction. A roster of bare names finds the classes a base list misses — and then also finds a DTO that merely shares a name with an entity, every half of a partial class, and a type mentioned in a method body.
So registrations now carry the namespaces they could have been naming: the registering file's usings, its own namespace, and the namespaces enclosing it. That is ordinary C# lookup, the part of it syntax can see. What it cannot see — aliases, using static — finds no class and is dropped, which is the safe direction, and it is written into the code rather than left to be discovered.
The fifth fix is not about names at all. Which half of a partial class won depended on the operating system: NTFS compares file names without case and ext4 by byte, so Shipment.Generated.cs sorts after Shipment.cs on one and before it on the other. CI caught it on Linux while Windows was green. A document that cannot be regenerated identically on two machines is one nobody can diff, which is most of what regenerating it is for.
Full detail in CHANGELOG.md.
Install
dotnet tool install -g XafLogicExplainer.Cli
xaflogic agents --project "C:\MySolution\MyApp.Module"Already installed:
dotnet tool update -g XafLogicExplainer.Cli10 MCP tools · 279 tests · .NET 10 · MIT · no DevExpress licence required
v0.12.0 — what runs when you open this screen
What runs when you open this screen
Two things an XAF repository does not contain, and this release extracts both.
The screens are in no file. XAF generates a list, a detail and a lookup view for every business class, plus a list view for every collection — and the Model Editor stores only the ones somebody changed. The demo application has fourteen business classes and fifty-four views, none of which appear in any file. The inventory is derived from the framework's own id generators, not guessed.
Which controllers run there is decided at run time. ViewController.IsFitToView ANDs four conditions together: nesting, view type, object type and view id. Each is unrestricted when unset, so a controller that sets none loads onto every screen in the application. This transcribes all four the way the framework evaluates them and records why each one matched.
Two layers, kept apart: what your team wrote in full, what XAF provides folded behind one line — and with a ground-truth catalog the framework's are named too, scoped to the modules you actually register.
New MCP tool xaf_view, a Screens section in the explainer, and a _Screens.md file beside the other agent documentation.
Most of this release is corrections
They came from an audit rather than a bug report: three reviewers on deliberately disjoint axes — one against the installed DevExpress sources, one against the new code, and one shown only the generated output and never the generator. The third found a category the other two structurally could not.
About thirty findings, one false alarm. The worst had been there far longer than the release: extraction returned only the first controller class per file and recognised only classes deriving directly from ViewController and its two siblings, so real XAF code — which extends shipped controllers and its own base classes — was being dropped silently. A probe with five controllers across three files reported one.
The rule the audit produced, now enforced by tests:
Under-reporting is bad, over-reporting is worse, and "unknown" must never be spelled the same way as "unrestricted".
Full detail in CHANGELOG.md.
Install
dotnet tool install -g XafLogicExplainer.Cli
xaflogic agents --project "C:\MySolution\MyApp.Module"10 MCP tools · 270 tests · .NET 10 · MIT · no DevExpress licence required
v0.11.0 — the behaviour outside the business classes
Everything that lives outside the business classes.
Read every business object in an XAF solution and you can still be confidently wrong about the
application. This release extracts the two places that knowledge hides.
Custom property and list editors
A property rendered by one does not show the control its type implies, and the business class says
nothing about it. They also live in the platform project (*.Blazor.Server, *.Win) beside the
module, so nobody reading the business objects ever meets them.
- Detected from
[PropertyEditor],[ListEditor]and[ViewItem], and from editor base types. - Alias constants are resolved across the solution — the constant is declared in the module
while the editor sits in the platform project, so reading either alone resolves nothing. - Client assets are recorded — the JavaScript an editor cannot work without: behaviour in
neither C# nor XML, and the reason a control breaks when somebody renames a file. - Also finds built-in editors reconfigured at run time through
View.CustomizeViewItemControl<T>(),
where no custom editor class exists to find at all. isDefault: truemeans the editor replaces the default for that type everywhere;falsemeans
it is merely selectable in the Model Editor. Only the first is reported as used by an entity.
Version-gated data migrations
The blocks guarded by CurrentDBVersion < new Version("1.1.0.0"). Each ran once, on somebody's
production database, and never again — which is why an agent asked "why does this column contain
that?" reasons from current code and invents a cause.
Records the version, the "existing databases only" bound, which schema phase it ran in, the
methods it calls, the code, and the comment above the block — usually the only surviving record
of why.
Kept separate from seed data throughout: seed data says what a fresh database contains, migrations
say what happened to every database that was not fresh.
xaflogic explain
One self-contained HTML page explaining the application to a person — the reader who has just
inherited a ten-year-old XAF application, or has to hand one over. A map of the domain model drawn
from association attributes scattered across the codebase, every entity and property, every action
with the code it runs, an index of every criteria expression, and no request to the network.
Also
New MCP tools xaf_editors and xaf_migrations. A fourteen-entity demo application in the
repository, so the diagrams and screenshots show a realistic application that belongs to nobody.
Explicit interface implementations no longer appear as duplicate properties.
Full detail in CHANGELOG.md.
v0.10.1 — MCP registry ownership
Packaging fix that unblocks the MCP registry listing.
XafLogicExplainer.Mcp packed the repository README, which does not carry the mcp-name: line the registry reads out of the published package to verify ownership. The package now carries its own README. Nothing else changed — the tools, the CLI and the extraction engine are identical to v0.10.0.
dotnet tool install -g XafLogicExplainer.Cli{ "mcpServers": { "xaf": { "command": "dnx", "args": ["XafLogicExplainer.Mcp", "--yes"] } } }v0.10.0 — first packages on NuGet
First release published to NuGet. Three packages, one version:
| Package | What it is |
|---|---|
XafLogicExplainer.Cli |
The xaflogic command — dotnet tool install -g XafLogicExplainer.Cli |
XafLogicExplainer.Core |
The Roslyn extraction engine |
XafLogicExplainer.Mcp |
MCP server — dnx XafLogicExplainer.Mcp |
An agent that has read every page of the DevExpress documentation still knows nothing about your application. This reads it: entities, controllers, actions, business rules, navigation and Model Editor customizations, straight from source with Roslyn — no compilation, and no DevExpress licence required.
Highlights
xaflogic agentswritesAGENTS.md,CLAUDE.mdand Copilot instructions, tiered so the always-loaded index stays around 11 KB- An MCP server with seven tools, installable on its own or through the Claude Code plugin
- An optional DevExpress ground-truth catalog, generated locally by licensees, that tells your code apart from the framework's
- 129 tests over synthetic XPO and EF Core fixtures — no DevExpress installation needed to run them
Still 0.x on purpose. The extraction engine is production-proven, but this release changed its behaviour in six places and has been verified against one real application. 1.0.0 is earned once the extractor has read codebases we did not write.