Skip to content

Commit 7a9a98d

Browse files
peopleworksclaude
andcommitted
Name the ORM in one place, so Unknown reaches all three renderers
peopleworks#8 taught the core to answer Unknown. The agent files listened; the HTML explainer and the MCP overview kept deciding in a binary that had no third answer, so a project whose ORM could not be determined came out as XPO. The MCP one is the worse of the two. xaf_overview 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. Closes peopleworks#11. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent f5b5066 commit 7a9a98d

7 files changed

Lines changed: 106 additions & 23 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
99

1010
### Fixed
1111

12+
- **`Unknown` now reaches every place the ORM is reported.** The agent files learned it; the HTML
13+
explainer and the MCP `xaf_overview` kept deciding in a binary with no third answer, so a project
14+
whose ORM could not be determined was reported as **XPO** by both. The MCP one was the worse of
15+
the two: it prints the ORM two lines above "These lists are complete, not sampled", from a tool
16+
whose description tells the agent that anything absent does not exist in the application. All
17+
three now go through one `Orm` helper — the defect was never the wrong answer, it was that three
18+
places were each entitled to one.
19+
1220
- **The ORM is read as syntax, and is `Unknown` when nothing says.** Detection scanned raw file
1321
text for `DevExpress.Persistent.BaseImpl.EF` and fell through to XPO, so an EF Core application
1422
whose entities do not use the DevExpress EF base implementation — a legacy schema, its security

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ applications. The agent-facing surface is what is landing now, in the open.
317317
|| Pluggable publishing targets (`IDocumentationSink`) |
318318
|| **MCP server** — 10 tools, live against your source |
319319
|| **Installable Claude Code plugin** with skill and MCP server |
320-
|| **282 tests** over synthetic XPO and EF Core fixtures — no DevExpress needed |
320+
|| **285 tests** over synthetic XPO and EF Core fixtures — no DevExpress needed |
321321
|| **DevExpress ground-truth catalog**, generated locally by licensees |
322322

323323
PeopleWorks Copilot, where this tool grew up, is now one sink among several rather than the

src/XafLogicExplainer.Core/Generators/AgentContextGenerator.cs

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ private void WriteHeader(StringBuilder sb, ExtractedProject project)
126126
private static void WriteSummary(StringBuilder sb, ExtractedProject project)
127127
{
128128
var actionCount = project.Controllers.Sum(c => c.Actions.Count);
129-
var orm = OrmDisplayName(project.OrmType);
129+
var orm = Orm.DisplayName(project.OrmType);
130130

131131
sb.Append($"A DevExpress XAF application: **{project.Entities.Count} business ");
132132
sb.Append(Plural(project.Entities.Count, "entity", "entities"));
@@ -177,7 +177,7 @@ private static void WriteGroundRules(
177177
sb.AppendLine();
178178
}
179179

180-
var isEfCore = IsEfCore(project.OrmType);
180+
var isEfCore = Orm.IsEfCore(project.OrmType);
181181
var rule = 1;
182182

183183
// Rule 1: the ORM. Mixing XPO and EF Core idioms is the most frequent way generated XAF
@@ -188,7 +188,7 @@ private static void WriteGroundRules(
188188
// ORM outright, so stating it on a default would forbid whichever one the application
189189
// actually uses -- worse than silence, because the agent cannot tell a guess from a
190190
// reading.
191-
if (IsOrmUnknown(project.OrmType))
191+
if (Orm.IsUnknown(project.OrmType))
192192
{
193193
sb.AppendLine($"**{rule++}. The ORM this application uses could not be determined.**");
194194
sb.AppendLine("No `DbSet<T>` registration, `DbContext`, XPO base class or ORM `using` directive was");
@@ -738,19 +738,6 @@ private static void WriteFooter(StringBuilder sb)
738738

739739
// --------------------------------------------------------------- helpers
740740

741-
private static bool IsEfCore(string? ormType) =>
742-
ormType is not null && ormType.Contains("EF", StringComparison.OrdinalIgnoreCase);
743-
744-
private static bool IsOrmUnknown(string? ormType) =>
745-
ormType is null || ormType.Equals("Unknown", StringComparison.OrdinalIgnoreCase);
746-
747-
private static string OrmDisplayName(string? ormType) => ormType switch
748-
{
749-
_ when IsOrmUnknown(ormType) => "an undetermined ORM",
750-
_ when IsEfCore(ormType) => "Entity Framework Core",
751-
_ => "XPO",
752-
};
753-
754741
private static string PropertyMarkers(ExtractedProperty property)
755742
{
756743
var markers = new StringBuilder();

src/XafLogicExplainer.Core/Generators/HtmlExplainerGenerator.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ private static void WriteHead(StringBuilder sb, ExtractedProject project)
9191
private static void WriteHeader(StringBuilder sb, ExtractedProject project)
9292
{
9393
var actions = project.Controllers.Sum(c => c.Actions.Count);
94-
var orm = IsEfCore(project.OrmType) ? "Entity Framework Core" : "XPO";
94+
var orm = Orm.Label(project.OrmType);
9595
var rules = project.Entities.Sum(e => e.ValidationRules.Count + e.AppearanceRules.Count);
9696

9797
sb.AppendLine("<header><div class=\"wrap\">");
@@ -959,8 +959,6 @@ private void WriteFooter(StringBuilder sb, ExtractedProject project)
959959

960960
// --------------------------------------------------------------- helpers
961961

962-
private static bool IsEfCore(string? orm) =>
963-
orm is not null && orm.Contains("EF", StringComparison.OrdinalIgnoreCase);
964962

965963
private static string Describe(RelationshipType type) => type switch
966964
{
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
namespace XafLogicExplainer.Core.Models;
2+
3+
/// <summary>
4+
/// How an application's ORM is named, wherever it is reported.
5+
/// </summary>
6+
/// <remarks>
7+
/// One definition, because there were three. When <see cref="OrmType.Unknown"/> arrived, the agent
8+
/// files learned it and the HTML explainer and the MCP overview did not — each of those decided the
9+
/// question again, in a binary that had no third answer, so a project whose ORM the tool could not
10+
/// determine was reported as XPO. The defect was not the wrong answer; it was that three places
11+
/// were each entitled to one.
12+
/// <para>
13+
/// The ORM travels as a string rather than the enum because <see cref="ExtractedProject.OrmType"/>
14+
/// is what a rendered snapshot carries and what the MCP server reads back. Comparisons are made
15+
/// here so that stays an implementation detail nobody downstream copies.
16+
/// </para>
17+
/// </remarks>
18+
public static class Orm
19+
{
20+
/// <summary>The name for a heading or a table cell, where it stands on its own.</summary>
21+
public static string Label(string? ormType) =>
22+
IsUnknown(ormType) ? "Not determined"
23+
: IsEfCore(ormType) ? "Entity Framework Core"
24+
: "XPO";
25+
26+
/// <summary>
27+
/// The name for the middle of a sentence, where it needs an article.
28+
/// </summary>
29+
/// <remarks>
30+
/// A second form rather than a second decision: both are written here, so neither can drift
31+
/// into disagreeing with the other about what is known. Only the grammar differs.
32+
/// </remarks>
33+
public static string DisplayName(string? ormType) =>
34+
IsUnknown(ormType) ? "an undetermined ORM"
35+
: IsEfCore(ormType) ? "Entity Framework Core"
36+
: "XPO";
37+
38+
/// <summary>Whether the source said nothing either way.</summary>
39+
/// <remarks>
40+
/// A null is unknown too. Anything that never ran detection has not learned XPO.
41+
/// </remarks>
42+
public static bool IsUnknown(string? ormType) =>
43+
string.IsNullOrWhiteSpace(ormType)
44+
|| ormType.Equals("Unknown", StringComparison.OrdinalIgnoreCase);
45+
46+
/// <summary>Whether the application persists through Entity Framework Core.</summary>
47+
public static bool IsEfCore(string? ormType) =>
48+
ormType is not null
49+
&& !IsUnknown(ormType)
50+
&& ormType.Contains("EF", StringComparison.OrdinalIgnoreCase);
51+
}

src/XafLogicExplainer.Mcp/Tools/XafDiscoveryTools.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,7 @@ public async Task<string> OverviewAsync(
3737
var sb = new StringBuilder();
3838

3939
var actionCount = app.Controllers.Sum(c => c.Actions.Count);
40-
var orm = app.OrmType.Contains("EF", StringComparison.OrdinalIgnoreCase)
41-
? "Entity Framework Core"
42-
: "XPO";
40+
var orm = Orm.Label(app.OrmType);
4341

4442
sb.AppendLine($"# {app.ProjectName}");
4543
sb.AppendLine();

tests/XafLogicExplainer.Tests/OverReportingTests.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
using Microsoft.CodeAnalysis.CSharp.Syntax;
33
using XafLogicExplainer.Core.Analyzers;
44
using XafLogicExplainer.Core.Catalog;
5+
using XafLogicExplainer.Core.Generators;
56
using XafLogicExplainer.Core.Models;
7+
using XafLogicExplainer.Mcp;
8+
using XafLogicExplainer.Mcp.Tools;
69

710
namespace XafLogicExplainer.Tests;
811

@@ -246,6 +249,44 @@ public void DoesNotRegisterADbSetWrittenAsALocal()
246249
Assert.DoesNotContain("AuditEntry", names);
247250
}
248251

252+
[Fact]
253+
public void DoesNotNameAnOrmTheSourceNeverNamed()
254+
{
255+
// Three renderers each decided this separately, in a binary with no third answer, so a
256+
// project whose ORM could not be determined was reported as XPO by two of them. The defect
257+
// was not the wrong answer -- it was that three places were each entitled to one.
258+
Assert.Equal("Not determined", Orm.Label("Unknown"));
259+
Assert.Equal("an undetermined ORM", Orm.DisplayName("Unknown"));
260+
261+
// A null has not learned XPO either. Anything that never ran detection is unknown.
262+
Assert.True(Orm.IsUnknown(null));
263+
Assert.False(Orm.IsEfCore("Unknown"));
264+
}
265+
266+
[Fact]
267+
public void TheHtmlExplainerDoesNotClaimXpoForAnUndeterminedProject()
268+
{
269+
var page = new HtmlExplainerGenerator("0.0.0").Generate(SampleProjects.NoOrm);
270+
271+
Assert.DoesNotContain("<strong>XPO</strong>", page);
272+
Assert.Contains("Not determined", page);
273+
}
274+
275+
[Fact]
276+
public async Task TheMcpOverviewDoesNotClaimXpoForAnUndeterminedProject()
277+
{
278+
// The surface that speaks with the most authority: `xaf_overview` prints the ORM two lines
279+
// above "These lists are complete, not sampled", from a tool whose description tells the
280+
// agent that anything absent does not exist in the application.
281+
var tools = new XafDiscoveryTools(new XafProjectContext(
282+
[new XafProjectSource { Name = "SampleNoOrm", Path = SampleProjects.NoOrmPath, Language = "en" }]));
283+
284+
var overview = await tools.OverviewAsync(cancellationToken: TestContext.Current.CancellationToken);
285+
286+
Assert.DoesNotContain("**XPO**", overview);
287+
Assert.Contains("Not determined", overview);
288+
}
289+
249290
private static ControllerTargeting Read(string source) =>
250291
ControllerTargetingReader.Read(CSharpSyntaxTree.ParseText(source).GetRoot()
251292
.DescendantNodes().OfType<ClassDeclarationSyntax>().First());

0 commit comments

Comments
 (0)