Skip to content

Releases: peopleworks/XAFLogicExplainer

v0.17.0 — the question one application cannot answer

Choose a tag to compare

@peopleworks peopleworks released this 25 Aug 00:14
4a25038

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 --open

One self-contained HTML file over all of them. No server, no build step, no request to the network.

The corpus map

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 Cliente classes 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

Choose a tag to compare

@peopleworks peopleworks released this 23 Aug 23:04
0a7bc0e

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, in AGENTS.md, and through a new xaf_reports MCP 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 the GetCriteria() that turns the answers into
    a filter.

    The sentence under the heading carries more than the list. With ReportsModuleV2 registered,
    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, sets ReportStoreMode.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 .repx printed 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 same GetModuleUpdaters the updater
    already lives in), the layout (designer code, a .repx, or a constructor), and the parameters
    dialog (ReportParametersObjectBase, whose GetCriteria() is business logic in the plainest
    sense). ExtractedProject.Reports now carries each registration with what the call says and
    nothing it does not — IsInplaceReport is 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. ReferencesReportsModule is the flag the rendering needs for the sentence
    that makes the list safe to believe: with ReportsModuleV2 in, 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 new ReportsSolution fixture: 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 .repx in 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.minor from 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,
    the AGENTS.md header, and the MCP view detail.

    Both project-file spellings are read. A pre-NuGet XAF project has no PackageReference at 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 floating 25.2.*-* already worked and is now pinned.

  • The project file a module folder is named for is the one that is read. With several
    .csproj side by side — a .Net10.csproj from 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

Choose a tag to compare

@peopleworks peopleworks released this 23 Aug 06:37
b058d35

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, so xaf_entity and
    xaf_controller handed 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, and Customer sits 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 a file:line a reader can check.

  • The walk that decides what belongs to one process ([#23], phase 1). ProcessSlice.From takes
    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 record virtual/abstract and override, 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 walkthrough and xaf_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 a file: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_walkthrough is 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.Core still 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.json that xaflogic diff does, 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

  • An appearance rule keeps its condition however the attribute was written ([#22], reported by
    [@MBrekhof]). `AppearanceAttrib...
Read more

v0.14.0 — everything that governs an entity, under the entity

Choose a tag to compare

@peopleworks peopleworks released this 17 Aug 05:18
f33a79b

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

Choose a tag to compare

@peopleworks peopleworks released this 14 Aug 16:22
bef0b40

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.Unknown is a new member of a public enum, returned whenever no evidence names an
    ORM. Anyone consuming XafLogicExplainer.Core directly and switching exhaustively over
    OrmType has a new case to handle; anyone using the CLI or the MCP server has nothing to do.
    The same note IControllerAnalyzer.AnalyzeControllerFile got in 0.12.0, for the same reason —
    on 0.x this is what a minor is for.

  • ExtractionOptions.BaseTypeNames is the single source of the list. The CLI, the MCP server
    and the test harness each passed their own copy, so the default in Core was 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

  • Unknown now reaches every place the ORM is reported. The agent files learned it; the HTML
    explainer and the MCP xaf_overview kept 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 one Orm helper — 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 Unknown when nothing says. Detection scanned raw file
    text for DevExpress.Persistent.BaseImpl.EF and 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 that DbContext, 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 — a DbSet<T> registered on a context first,
    then using directives 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 as SelectControllers does, and resolves a base name through the
    deriving file's own scope rather than by simple name, so a Contracts.Order beside a
    BusinessObjects.Order still resolves to the base it actually named. On the demos shipped with
    26.1: FeatureCenter.NET.XPO 43 → 140 entities, MainDemo.NET.XPO 14 → 17, and
    OutlookInspiredDemo.NET.EFCore 23 → 24 — the last of which is an EF Core application, where an
    entity that is not registered as a DbSet<T> had no fallback either.

  • PersistentBase and XPBaseObject are recognised as persistent bases. The XPO hierarchy is
    PersistentBaseXPBaseObjectXPCustomObjectXPObject, with XPLiteObject also 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 recommends PersistentBase. 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.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).


Install

dotnet tool install -g XafLogicExplainer.Cli
dnx XafLogicExplainer.Mcp --yes -- --help

Full changelog: v0.12.1...v0.13.0

v0.12.1 — entities the application declares

Choose a tag to compare

@peopleworks peopleworks released this 13 Aug 21:11
67b0f14

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.Cli

10 MCP tools · 279 tests · .NET 10 · MIT · no DevExpress licence required

v0.12.0 — what runs when you open this screen

Choose a tag to compare

@peopleworks peopleworks released this 11 Aug 06:31

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

Choose a tag to compare

@peopleworks peopleworks released this 10 Aug 16:44

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: true means the editor replaces the default for that type everywhere; false means
    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

Choose a tag to compare

@peopleworks peopleworks released this 10 Aug 14:48

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

Choose a tag to compare

@peopleworks peopleworks released this 10 Aug 14:20

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 agents writes AGENTS.md, CLAUDE.md and 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.

See the site and the changelog.