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).
The projects wired together in CSharpCodeAnalyst.sln (the load-bearing ones):
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/).CSharpCodeAnalyst.Importers/— every import except the C# solution: doxygen (C++/Python/Java), Dart/Flutter, jdeps (Java, from bytecode) and the plain text format. UI-free apart from each importer's own configuration dialog; references onlyCodeGraphandAnalyzerSdk, never the Roslyn parser. See Importers (how to add one) below.CSharpCodeAnalyst.Mcp/— the optional MCP server: a loopback HTTP endpoint that lets an AI assistant query the loaded graph (McpServerHost), plus the tools it exposes (Tools/) and the snapshot contract the application fills (Contracts/). References onlyCodeGraph, no WPF, so the tools are unit-tested against a hand-built graph. See MCP server below andDocumentation/mcp.md.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.
ThirdParty/DsmSuite/ holds a vendored subset (7 of ~38 projects) of the GPL-licensed DsmSuite, which provides the matrix view on the DSM tab. It is foreign code with its own rules — see Vendored DsmSuite below before touching anything in there.
DartExtractor/ is a standalone Dart package (not in the .NET solution) used by the Dart/Flutter import — see Dart/Flutter import below.
TestSuite/ is a handcrafted C# solution used purely as parser input for the approval tests, and TestSuiteDart/ is its Dart equivalent for the Dart import. Do not consume either from production code — they are intentionally full of odd language constructs. TestSuiteGenerated/ is a third fixture solution, for the source-generator path only (SourceGeneratorFixtureTests): it uses [GeneratedRegex], which ships with the SDK, so it needs no package reference and neither a restore nor a build — MSBuildWorkspace's design-time build runs the generator. None of the three is part of CSharpCodeAnalyst.sln. 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.
Documentation/Roslyn/parser-architecture.md is the overview of how the parser is built — the two-phase principle and its three exceptions, the Key() bridge between Roslyn symbols and code elements, the four roles in phase 2, the resolution cascade, and a table of where to make which kind of change. Read it before a structural change; corrections-and-updates.md below is the per-decision log, not the map.
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). "Copy table as CSV" comes for free —DynamicDataGridappends it to every table's context menu and copies the rows currently on screen (after filtering and sorting) viaTableCsv; no analyzer implements its own export. - 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.
Every import except the C# solution lives in CSharpCodeAnalyst.Importers/ (one assembly, one folder per source: Doxygen/, Dart/, Jdeps/, PlainText/ — the same shape CSharpCodeAnalyst.Analyzers has). An importer implements Contracts/IImporter.cs: Id / Name / Description, IsAvailable(out reason) for the external prerequisite, and ImportAsync(IImportContext) returning a ParseResult — or null when the user cancelled, which is not an error.
Unlike an analyzer, an importer needs user input, and every importer needs different input, so it brings its own dialog. A declarative parameter model was considered and rejected: it cannot express validation like the Dart importer's "is this project resolved?". ImportAsync is therefore called on the UI thread — wrap the CPU-bound part in Task.Run yourself, or the window freezes.
IImportContext supplies IUserNotification, IProgress<string>, a scratch WorkingDirectory (created and deleted by the host) and AssetDirectory — the directory of the importer's own assembly, not the executable. An importer that ships files (the Dart one ships the extractor) must use it, or it breaks as soon as importers are loaded from a plugin folder.
Register in Features/Import/ImporterManager.cs; the import RibbonSplitButton binds to MainViewModel.ImportMenuEntries, so no XAML change is needed. Strings live in CSharpCodeAnalyst.Importers/Resources/Strings.resx and its hand-maintained Strings.Designer.cs. Features/Import/Importer.cs stays in the app: it still owns the C# solution import (which takes its options from the settings rather than a dialog) and is the runner that supplies the context and handles busy state and errors.
ParseResult lives in CSharpCodeAnalyst.CodeGraph/Contracts/ — every graph producer returns one, and the importers must not reference the Roslyn-based parser to do so.
DartExtractor/ at the repository root is a Dart package (not part of the .NET solution) that analyses a Dart or Flutter project with the analyzer package and emits the graph as JSON. CSharpCodeAnalyst.Importers/Dart/ finds the Dart SDK, deploys and resolves the tool once into %LocalAppData% (the install directory may be read-only and dart pub get writes into the package), runs it, and rebuilds the CodeGraph from the JSON. The JSON carries the literal CodeElementType / RelationshipType names, so all modelling decisions live on the Dart side and DartGraphConverter stays a pure rebuild — keep it that way, and bump format in both graph_builder.dart and DartGraphConverter.SupportedFormat when the contract changes incompatibly.
Dart has no namespaces, so the hierarchy is derived (assembly = package, namespace = path + library file). Document mapping changes in Documentation/Dart/dart-import.md — same rule as Documentation/Roslyn/corrections-and-updates.md for the C# parser.
TestSuiteDart/ is the fixture package (the Dart counterpart of TestSuite/), dependency-free so it resolves offline. Its mapping is pinned in two halves: DartFixtureApprovalTests asserts against the recorded extractor output in Tests/TestData/dart-fixture-graph.json and runs without a Dart SDK, while DartFixtureRecordingTests ([Explicit]) re-runs the extractor and compares. After changing DartExtractor, run RecordedGraphIsUpToDate, then ReRecordTheGraph, then update the expectations. DartImportEndToEndTests is a separate [Explicit] test against an arbitrary project via CSCA_DART_TEST_PROJECT.
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.
The server is opened from Home → MCP → Start MCP server; nothing listens until someone presses it. Features/Mcp/McpServerService owns the lifetime, Features/Mcp/CodeGraphSnapshotProvider supplies the data, and everything else lives in CSharpCodeAnalyst.Mcp/.
No ASP.NET, and that is load-bearing. The package is ModelContextProtocol.Core, the endpoint is a System.Net.HttpListener. The obvious host would be Kestrel via ModelContextProtocol.AspNetCore, but that package carries a FrameworkReference to Microsoft.AspNetCore.App which propagates through the ProjectReference into the application's runtimeconfig.json — and the .NET host refuses to start a process whose declared frameworks are not all installed. A missing ASP.NET runtime then costs the whole application, not just the MCP feature. Do not pull the ASP.NET package back in, not even transitively; check runtimeconfig.json after touching the package references.
The tool descriptions are the entire interface. A model never sees the code, only the [Description] attributes: the one on the method becomes the tool description, the ones on parameters become their JSON-schema descriptions. /// XML comments do not travel — no documentation file is generated, and the SDK reads the attribute. A tool method without [Description] ships with an empty description; nothing fails to compile and no test goes red, it simply never gets chosen. Write the XML comments for developers and the attribute for the model, and keep Documentation/mcp.md in sync with both.
Answers are prose for a model, not serialized objects (Tools/ToolText.cs explains why). Two habits matter more than the formatting: say what to do next rather than only what is missing, and never let an absence read as a finding — an empty result that could be misread has to state what it does not prove. The heuristic warning in find_incoming_calls and the "this element is external" note are the reference examples. Truncation always says how many entries were dropped and how to reach them.
Stateless, and answering from a copy. Every request stands on its own; CodeGraphSnapshotProvider clones the graph on the UI thread and hands out that snapshot, so tool calls never touch the live graph. Element ids are only valid while the server runs.
The accept loop must not run on the UI thread. StartAsync is called from the button handler, so it hands the loop to the thread pool with Task.Run and every await in the host uses ConfigureAwait(false). Without that, the first await captures the dispatcher and accepting connections, parsing request bodies and closing responses all move onto the thread that draws the application — Kestrel used to bring its own threads and hid this. ServingARequest_NeverReturnsToTheCallersThread pins it, because the symptom (a UI that stutters while an assistant is asking questions) is not one anybody would trace back here.
Re-importing while the server is running just works — no reconnect needed. LoadCodeGraph calls CodeGraphSnapshotProvider.SetGraph on every import, load and restore regardless of whether the server is up, and the next tool call picks up the new graph on its own; the socket, the tool list and the client connection are untouched. This is a different situation from the tool list changing (new tool, changed description), which genuinely does need /mcp → reconnect — see Documentation/mcp.md. Don't conflate the two when explaining this to a user.
CodeGraphSnapshotProvider.SetGraph and an in-flight capture can genuinely race: Capture() clones the graph on the UI thread, which takes real time for anything but a trivial graph, so a re-import can land while a previous capture is still copying. Both the capture's commit and the new SetGraph's "mark stale" take the same lock, and whichever runs last wins — if that is the capture, it stamps _isStale = false over a snapshot of the graph as it was before the swap, and the server keeps answering from the old project until an unrelated later change fixes it by accident. The fix is a generation stamp compared at commit time: a SetGraph that happened mid-capture already moved the generation on, so the capture's commit is skipped instead of overwriting it. CodeGraphSnapshotProviderTests.ALostUpdateBetweenSetGraphAndAnInFlightCapture_MustNotResurrectTheOldSnapshot reproduces it against a real Dispatcher (roughly 80% of iterations hit it without the generation check) and pins the fix at zero.
Adding a tool: a public method on one of the [McpServerToolType] classes in Tools/, carrying [McpServerTool(Name = "…")] and [Description]. Set Name explicitly — otherwise the SDK derives it from the method name, and renaming the method would silently change the wire contract. Adding a method to an existing class needs no registration; adding a whole new tool class means adding it to the list in McpServerHost.CreateOptions.
Tests are in Tests/UnitTests/Mcp/. The tool fixtures call the methods directly against a TestCodeGraph and pin the promises the descriptions make; McpServerHostTests covers transport, routing and serialization end to end over a real socket on an OS-assigned free port; CodeGraphSnapshotProviderTests covers the UI-thread/thread-pool boundary, including the lost-update race above.
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.
The matrix on the DSM tab is not ours: ThirdParty/DsmSuite/ is a modified subset of DsmSuite, pinned to a commit recorded in ThirdParty/DsmSuite/README.md, GPL-3.0-or-later (originally MIT). Our side is Features/DsmMatrix/: CodeGraphToDsmModelBuilder fills a DsmSuite IDsmModel straight from the TypeGraph — explicit parentId, no DSI file, no name splitting — and DsmMatrixView hosts their MatrixView.
The reader's guide and the upstream record are two different documents — keep them apart. The reader's guide is not in this folder: it is Appendix A of Documentation/dsm-tutorial.md, which describes what you see and how to operate it (axes — row = provider, column = consumer; the depth-coloured blocks; the row indicator bar; the presence map when zoomed out; keyboard and wheel) for an audience that does not read code. ThirdParty/DsmSuite/README.md is the record against upstream: the change table, why each change was made, and the bug list. Read that appendix before changing anything about the view's semantics and keep it in sync — none of it is discoverable from the UI — but put the reasoning in README.md.
Every change under ThirdParty/DsmSuite/ costs two things, and neither is optional: (1) a Changed <YYYY-MM> for CSharpCodeAnalyst comment at the site explaining why, and (2) a row in the change table in ThirdParty/DsmSuite/README.md. GPL §5(a) requires stating what was modified, and that table is the map for ever diffing against upstream again — an undocumented change silently becomes indistinguishable from upstream code. Same rule for the bug list in that README when you find (or fix) a defect in their code.
Two traps that are already documented there and worth knowing before you write code against their API:
DsmApplicationbindsDsmQueriesto the model it is constructed with and never rebinds. Populate the model first, then constructDsmApplication. Calling theirLoadModel(i.e. the file-based import path) leaves every query running against the previous model. This is whyDsmMatrixView.Showbuilds everything from scratch.- Their
MainViewModelis the DataContext, including its editing commands. Those mutate the DSM model only, never theCodeGraph, so they cannot corrupt a parse result — but they are live in the context menus.
Their resource dictionaries are merged in App.xaml at application scope because their controls resolve them via StaticResource. Everything in there is keyed; if you pull in more of their XAML, check for implicit (unkeyed) styles first — those would restyle the whole application.
.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.