This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
- Windows-only WPF application (
CSharpCodeAnalyst.csprojtargetsnet10.0-windows,OutputType=WinExe). Building / running on Linux/macOS is not supported. - Requires .NET 10 SDK (build) and the .NET 10 Runtime plus MSBuild from a Visual Studio or .NET SDK install (the parser loads solutions through
MSBuildWorkspace). - Line endings: the repo is full of Windows-native WPF assets; do not reformat or convert CRLFs.
Run from the repository root.
# Restore and build the whole solution
dotnet restore
dotnet build
# Release build with explicit version (matches CI)
dotnet build --no-restore -c Release -p:Version=0.9.0 -p:FileVersion=0.9.0 -p:AssemblyVersion=0.9.0
# Run all tests
dotnet test --configuration Release
# Run a single NUnit fixture or test (filter by fully-qualified name)
dotnet test Tests/Tests.csproj --filter "FullyQualifiedName~CyclesApprovalTests"
dotnet test Tests/Tests.csproj --filter "FullyQualifiedName=CodeParserTests.ApprovalTests.CyclesApprovalTests.MethodName"
# Publish the WPF app (framework-dependent, win-x64, same flags as the release pipeline)
dotnet publish .\CSharpCodeAnalyst\CSharpCodeAnalyst.csproj -c Release -r win-x64 -o publish `
--self-contained false -p:PublishSingleFile=false -p:SatelliteResourceLanguages=enCommand-line (headless) validation mode — triggered when more than one CLI arg is passed, so App.OnStartup skips the UI and exits with a status code:
CSharpCodeAnalyst.exe -validate -sln:<path.sln> -rules:<path.txt> [-log-console] [-log-file:<file>] [-out:<file>]
Exit codes: 0 = clean, 1 = violations, 2 = validation failed. See Documentation/command-line-arguments.md.
Debug shortcut: passing a single arg -load:<project.json> auto-loads a saved project after the UI starts (see App.LoadProjectFileFromCommandLineAsync).
Five projects wired together in CSharpCodeAnalyst.sln:
CodeGraph/— pure, UI-free domain model. Contains theCodeElement/Relationship/CodeGraphgraph types (Graph/), graph algorithms (Algorithms/Cycles,Algorithms/Metrics,Algorithms/Partitioning), exporters (Export/— DGML, DSI, PlantUML, JSON), andExploration/CodeGraphExplorer(traversal queries used from the UI context menus). No WPF dependencies — reference this project from tests and tools.CodeParser/— Roslyn front-end that turns an.slnor.csprojinto aCodeGraph. Entry point:Parser.ParseAsync(path). Works in two passes:HierarchyAnalyzerfinds code elements and parent/child links, thenRelationshipAnalyzer.AnalyzeRelationshipswalks method and lambda bodies to build relationships (parallel by default; passmaxDegreeOfParallelism: 1for a single-threaded debug run).Initializer.InitializeMsBuildLocator()must be called once before any parse (bothApp.StartUiand the test fixtureInitdo this).CSharpCodeAnalyst/— WPF front-end. Organized by feature underFeatures/(CycleGroups,Graph,Tree,AdvancedSearch,Analyzers/ArchitecturalRules,Analyzers/EventRegistration,Ai,Import,Export,Metrics,Partitions,Refactoring,Gallery,Help,Info). Cross-cutting infrastructure lives inShared/(messaging, notifications, data grid, search, filter, WPF helpers).Configuration/holdsAppSettings(fromappsettings.json),UserPreferences(persisted touserSettings.json), andAiCredentialStorage. Persistence of saved projects is inPersistence/(JSON, with DTOs underDto/).Tests/(project nameCodeParserTests) — NUnit suite.ApprovalTests/parses theTestSuite/C# solution once per fixture and asserts on the resulting graph;UnitTests/covers cycles, exploration, export, search, architectural rules, etc.ApprovalTestTool/— standalone console app that clones external repos listed inRepositories.txt, parses each at a pinned commit, hashes the graph dump, and diffs against references. Used to catch parser regressions on real codebases; not part of the CI test run.
TestSuite/ is a handcrafted C# solution used purely as parser input for the approval tests. Do not consume it from production code — it is intentionally full of odd language constructs. ReferencedAssemblies/ contains the MSAGL DLLs referenced directly by CSharpCodeAnalyst.csproj and Tests.csproj (MSAGL is not on NuGet for the versions used here).
Every project inherits a Microsoft.Build.Framework PackageReference with ExcludeAssets="runtime". This is load-bearing: Microsoft.Build.Locator loads MSBuild from the installed SDK at runtime, and copying the NuGet-provided MSBuild DLLs into bin/ causes RPC_E_CALL_REJECTED and other loader failures. When upgrading Roslyn / MSBuild packages, keep the exclusion in place and bump the version comment in Directory.Build.props to match the transitive version from Microsoft.Build.Locator.
Parser.ParseSolutionInternal runs HierarchyAnalyzer → RelationshipAnalyzer → InsertGlobalNamespaceIfUsed. The global-namespace insertion normalizes assemblies that contain types directly at the root (e.g. test assemblies with generated Main) so that cycle detection always has a shared ancestor above Namespace rather than at Assembly. Preserve this invariant if you touch the post-processing.
Document parser modelling decisions: when you change how the parser maps C# to the graph — a new/changed relationship, a Roslyn quirk worked around, a deliberate "this looks like X but we model it as Y" choice — add or update a short chapter in Documentation/Roslyn/corrections-and-updates.md (English, in the existing style: the construct, why it is tricky, how we model it and the reasoning). This file is the running record of those non-obvious decisions; keep it in sync with parser changes.
The UI is not built on a DI container. App.StartUi wires up singletons manually (one MessageBus, one CodeGraphExplorer, one GraphViewer, etc.) and injects them into view models. Cross-view-model communication goes through Shared/Messages/MessageBus.cs (Publish/Subscribe on strongly-typed message records in Shared/Messages/). When adding a new cross-feature interaction, prefer defining a new message type over introducing direct view-model references.
Features/WebGraph/ replaces the former MSAGL renderer. The graph is displayed in an embedded Chromium browser (Microsoft.Web.WebView2) using Cytoscape.js. Assets (HTML, CSS, cytoscape.min.js, layout extensions) live in Features/WebGraph/Web/ and are served offline via a WebView2 virtual-host mapping (https://csharp-code-analyst.local/). The Web/ folder (incl. lib/) is copied to the output via a Content Include="Features\WebGraph\Web\**\*" glob, so new asset files are picked up automatically — no .csproj edit needed.
Adding a third-party library (web lib/ JS/CSS, NuGet package, or DLL) requires two licensing updates — do not skip these: (1) add the library to the matching ThirdPartyNotices/<LICENSE>-LICENSED-LIBRARIES.txt (grouped by license type; the license text lives once per file, so usually you only append a list entry — create a new file only for a license type not yet present), and (2) add an acknowledgement entry to the "Thank you" section of README.md (name, license, project URL). Both ThirdPartyNotices/** and the README ship with the app, so an omission is a real compliance gap. Always confirm the actual license from the package's own metadata rather than assuming.
Data flow (C# → JS): WebGraphBuilder.Build converts the current CodeGraph + PresentationState into a {nodes, edges} JSON payload. WebGraphControl calls ExecuteScriptAsync("renderGraph(<json>)") once JS signals readiness with a {type:"ready"} message.
Compound nodes: Every CodeElement.Parent link maps directly to a Cytoscape parent field, so namespaces, classes, and other containers render with their children nested inside — no extra hierarchy logic needed.
Event routing (JS → C#): Clicks, double-clicks, right-clicks, and selection changes are postMessage-ed to C# via window.chrome.webview.postMessage(...) and received in CoreWebView2.WebMessageReceived. Context menus are WPF ContextMenus built by WebContextMenuFactory from the existing command objects (ICodeElementContextCommand / IRelationshipContextCommand / IGlobalCommand), opened with PlacementMode.MousePoint over the WebView2 control.
Initialisation: WebGraphControl (a UserControl wrapping WebView2) must be in the WPF visual tree to initialise, so the web tab is tab index 0 in MainWindow (eager-init on startup). The WebView2 user-data folder goes to %LocalAppData%\CSharpCodeAnalyst\WebView2.
The ~200-element soft limit (AppSettings.WarningCodeElementLimit) still applies; Cytoscape's canvas-based renderer handles it comfortably with fcose or dagre layouts.
Analyzers are the boxes under the Analyzers ribbon button. Each implements IAnalyzer (CSharpCodeAnalyst.AnalyzerSdk/Contracts/IAnalyzer.cs): Id / Name / Description, an Analyze(CodeGraph) that does the work, plus GetPersistentData / SetPersistentData / IsDirty / DataChanged (return null / no-op / false for a stateless analyzer — only the Architectural Rules analyzer actually persists). They live in CSharpCodeAnalyst.Analyzers/<Feature>/ with the presentation VMs under <Feature>/Presentation/. The pure algorithm belongs one layer down in CSharpCodeAnalyst.CodeGraph/Algorithms/Metrics/ (UI-free, unit-tested directly).
Data flow, end to end:
- Algorithm in
CodeGraph/Algorithms/...takes theCodeGraphand returns a plain result object. Type-level analyses lift relationships to the containing type, deduplicate, and excludeIsExternalnodes (seeTypeDependencyAnalysis/SystemMetricsAnalysisas the reference); reuseType.IsDependency()to decide which edges count. Analyzeruns the algorithm; on an empty result it calls_userNotification.ShowSuccess(...NoData)and returns, otherwise it builds a table view model and publishesnew ShowTabularDataRequest(Id, Name, vm)on the message bus.- Table VM derives from
Table(AnalyzerSdk/DynamicDataGrid/Contracts/TabularData/):GetColumns()returnsTableColumnDefinitions (each binds aPropertyNameon the row VM),GetData()returns theTableRows. Optional:CanFilter/Filter,GetCommands()(context-menu / double-click actions), row-details template, and per-columnRating(anIMetricRating→ colored cell background, seeThresholdRatingandRatingToBrushConverter). - Row VM derives from
TableRowand exposes one property per column (plus aSortMemberName/RatingValuePropertyNamenumeric backer when the displayed column is a formatted string). - Register the analyzer in
CSharpCodeAnalyst/Features/Analyzers/AnalyzerManager.LoadAnalyzers(add ausing <Feature> = ...alias and an_analyzers.Add). No XAML change is needed: the ribbonRibbonSplitButtonbindsItemsSourcetoMainViewModel.Analyzers(=AnalyzerManager.All) and runsExecuteAnalyzerCommandwith the analyzerId;MainViewModelpublishes the result into aDynamicTabthat hosts aDynamicDataGrid. - Strings live in
CSharpCodeAnalyst.Analyzers/Resources/Strings.resxand its hand-maintainedStrings.Designer.cs(add the getter yourself). Convention:Analyzer_<Id>_Label/_Tooltip/_NoData,Column_<Id>_<Col>.
SystemMetrics is the smallest complete example to copy from (system-wide single values in a metric/value/description table).
Rules live in CSharpCodeAnalyst.Analyzers/ArchitecturalRules/Rules/ under a two-level hierarchy. DependencyRule constrains relationships between code elements (DENY / RESTRICT / ISOLATE / ALLOW) and its violation is a set of relationships. MetricRule constrains a measured value; it splits into SystemMetricRule (one value for the whole graph, Measure(SystemMetrics), violation carries that value) and CodeElementMetricRule (one value per element, Measure(element, MetricStore) returning null for "not applicable", violation carries the offending elements). Rules are immutable value objects parsed from one line of text — never give them a graph or a MetricStore; those belong to the run and are passed to RuleEngine.Execute.
A new metric rule costs exactly two things: a class deriving from the right base, and one entry in RuleParser.MetricRuleFactories. The parser has a single regex for the whole family (KEYWORD = value, or KEYWORD: Pattern = value for element rules), so it needs no change. The base class supplies the range check via MinThreshold / MaxThreshold, the floating-point tolerance in IsViolated, and the baseline rewrite via CreateBaselineThreshold / CreateRuleText. Implement Keyword, the bounds, Measure, FormatValue (the value with its unit) and CreateDescription. Thresholds are expressed in the rule's own unit (percent, lines) — convert from the metric's internal representation exactly once, inside Measure. When writing a threshold back (baseline), round up, otherwise the rule you just wrote is violated again.
Then wire up the edges: RuleEngine.Execute for the evaluation, RuleViolationViewModel for the table row and detail lines, ViolationsFormatter for the CLI output, RuleCleaner if the rule can be dead, BaselineGenerator.RelaxMetricRules if it can be baselined, and strings in Resources/Strings.resx plus its hand-maintained Strings.Designer.cs. Document the rule in the "Supported rules" tables of README.md. MaxCyclicityRule and MaxLinesRule are the reference implementations of the two kinds.
Features/Ai/AiClient.cs talks to any OpenAI-compatible endpoint (including Anthropic, Ollama). Credentials are stored via Configuration/AiCredentialStorage. The service is stateless and is invoked from the cycle-group UI to summarize a cycle.
Tests/ApprovalTests/ApprovalTestBase uses [OneTimeSetUp] and a static Init class to parse TestSuite.sln exactly once per test run (path is relative: ..\..\..\..\TestSuite\TestSuite.sln from the test binary). Expected values are large HashSet<string> literals inline in each fixture; when the parser legitimately changes output, dump the actual set with ApprovalTestBase.DumpRelationships / DumpCodeElements and paste it back in. Do not mock the parser — these tests are the safety net for Roslyn-version upgrades.
Features/Refactoring/ mutates the in-memory CodeGraph directly (move / delete elements, cut edges) and there is no undo. Any code path that offers these operations should save or warn first — see existing command handlers before adding new ones.
.editorconfigis authoritative and ReSharper-tuned: braces required on allif/foreach/while, max line length 199, expression-bodied accessors preferred,internalfirst in modifier order. Analyzer severities default tonone— do not add warning-as-error enforcement without discussion.- Nullable reference types are enabled everywhere; honour the annotations rather than suppressing with
!. - Namespaces match folders, rooted under
CSharpCodeAnalyst.*for every project (e.g.CSharpCodeAnalyst.CodeGraph.Graph,CSharpCodeAnalyst.CodeParser.Parser).CodeGraphis a class insideCSharpCodeAnalyst.CodeGraph.Graph— fully-qualify it (CSharpCodeAnalyst.CodeGraph.Graph.CodeGraph) in places where the namespace/type collision is ambiguous; existing code already does.