Independent engineering project / Assembly Archaeologist

Assembly Archaeologist

A .NET IL analyzer examining assembly dependencies, compatibility concerns and migration ordering. A generated assembly estate makes static-analysis coverage and cycle-breaking decisions reproducible.

Contribution
Independent design, implementation and evaluation
Languages
C# / PowerShell
Engineering focus
IL-level static analysis / Migration sequencing / Dead code detection

Scope. Self-directed modernization study measured in a local or simulated environment; not a customer engagement, and not evidence of production migration outcomes.

Implementation & evaluation

The project README is reproduced below, with links to the implementation, design records and operating instructions. Scenario narratives describe an independent project, not a customer deployment.

Read on GitHub

Assembly Archaeologist

Metadata-driven migration analysis for .NET Framework estates without source code: portability constraints, dependency structure and migration sequencing.

C# / .NET 10 / Mono.Cecil. 206 tests. No source code, no reference assemblies, no resolver: 24 compiled assemblies and the metadata inside them.


The problem

Assembly-reference graphs describe packaging dependencies, but migration planning also requires call-site portability constraints, type-level coupling and an explicit model of migration units. Assembly-level counts alone do not resolve those questions.

This project analyzes compiled IL from a generated .NET Framework corpus without requiring application source or reference assemblies. It evaluates the following migration questions and records the supporting evidence:

  • Which API calls actually block the move, at the call site, not the reference.
  • Which dependency cycles are real entanglement and which are packaging accidents.
  • What the cheapest way out of each cycle is, exactly, not approximately.
  • What is safe to delete -- and how much that answer depends on what you assume about reflection.
  • What the migration order is, how wide it is, and which single chain sets the schedule.

The one-sentence finding

Assembly-level dependency counts are insufficient for the migration decisions evaluated in this corpus.

Portability is a property of a member. Entanglement is a property of a type. Difficulty is a property of a migration unit. Deletability is a property of a type qualified by a string constant. The assembly is the unit the packaging happens to use, and reasoning at that level produces answers that are confidently, measurably wrong.

docs/results.md is the evidence. It is generated by the code, it makes twelve predictions before it measures anything, and nine of them turned out to be wrong.


Selected findings

A manifest scanner cannot see the estate's worst problem. Correlating assembly reference counts against actual blocker severity gives Kendall tau 0.567 -- the sort of number that looks like agreement. But two assemblies in this estate reference nothing except mscorlib and still contain BinaryFormatter, which is the most severe class of blocker there is. BinaryFormatter lives in mscorlib. Every assembly already references mscorlib. A scanner that reads manifests is structurally blind to it.

A five-assembly cycle was caused by two types. The estate has three multi-assembly strongly connected components covering 10 of 24 assemblies. At type level, only 5 of the 19 types in those components are in any cycle at all -- 26%. One of the three components contains no type-level cycle whatsoever: it is three assemblies that were split along the wrong seam, and moving a single type dissolves it with no code change at all.

Recovering one string prefix is worth more than the entire sound analysis. Asked "what can I delete?", call-graph analysis says 6 types. Honouring literal reflection says 4. A fully sound analysis -- one that admits it cannot predict where Type.GetType(prefix + name) will land -- says zero, and it is right. But recovering prefix from two adjacent IL instructions takes that answer from 0 deletable types back to 3. Partial soundness is not a compromise here; it is the only thing that makes the sound answer usable.


How it works

There is no source code anywhere in this project's analysis path. That is the point.

The corpus is authored, not collected. CorpusBuilder uses Mono.Cecil to emit 24 real .NET Framework assemblies -- a 2005-era claims platform, with a Web Forms front end, a WCF service host, a COM+ component and an MSMQ integration. They reference System.Web 4.0.0.0, System.ServiceModel, System.EnterpriseServices and System.Messaging, with correct public key tokens. None of those are installed on the machine that builds them. Metadata does not require the thing it names to exist, which is exactly why IL analysis works on estates whose dependencies you cannot obtain.

The reader has never seen the spec. IlReader opens a directory of DLLs with a resolver that refuses every request, and rebuilds the call graph, the type graph and the assembly graph from IL alone. CorpusSpec declares what was planted; CorpusIntegrityTest re-derives every planted fact from the emitted bytes using machinery that has no access to the spec. The corpus does not mark its own homework.

Resolution would mislead, not fail. On this machine, four of the seven Framework assemblies the estate names do resolve -- System.Web, System.Data, System.Drawing and System.Configuration all exist in the .NET 10 shared framework as empty type-forwarding facades containing none of the types the estate uses. An analyser that resolved its references would not error out; it would quietly conclude the wrong thing. IlReaderTest pins this.

Source availability is a PDB, not an assumption. Three assemblies are emitted without symbols. That is the signal for "nobody can rebuild this", and it changes the plan far more than it changes the analysis: all three analyse perfectly, but 21 of 24 assemblies transitively depend on one of them.


Running it

.\demo.ps1     # emit the estate, analyse it, show the findings
.\test.ps1     # six stages: build, 206 tests, report freshness, determinism x3,
               # source mutation (7/7 killed), secrets scan

The CLI on its own:

dotnet run --project Archaeologist.Cli                    # writes docs/results.md
dotnet run --project Archaeologist.Cli -- --stdout        # to the console
dotnet run --project Archaeologist.Cli -- --keep C:\tmp\estate   # keep the DLLs

--keep leaves you 24 real assemblies you can open in ILSpy, dotPeek or ildasm.


What is in here

Archaeologist.Core/CorpusSpec.cs The estate, and the ground truth about it
Archaeologist.Core/CorpusBuilder.cs Cecil emitter; two passes, so cycles are emittable
Archaeologist.Core/IlReader.cs IL -> call graph, blockers, reflection sites
Archaeologist.Core/BlockerRules.cs 15 porting rules, written from the docs not the corpus
Archaeologist.Core/Graphs.cs Iterative Tarjan, topological order, waves
Archaeologist.Core/FeedbackArcSet.cs Greedy Eades-Lin-Smyth, and an exact subset DP
Archaeologist.Core/Reachability.cs Four answers to "what is dead?"
Archaeologist.Core/CycleDissolver.cs Exact minimum type extraction
Archaeologist.Core/MigrationPlanner.cs Units, condensation, waves, critical path
Archaeologist.Core/Experiments.cs The report, as a program
docs/results.md The output. 12 predictions, 3 held
docs/adr/ Five decisions and why
docs/portfolio/ Why this, what broke, what the tests caught
docs/known-limitations.md What it does not do, stated plainly

Things it deliberately refuses to do

The exact feedback-arc-set solver refuses above 20 nodes. It does not fall back to the heuristic. An exact solver that silently degrades is worse than no exact solver, because its output is still labelled "exact". The measured cost of that honesty is in section 4 of the report: on graphs of 12 nodes the heuristic loses to the exact answer 33 times out of 40, by an average of 3.5 edges and a worst case of 11.

The report will not render while a prediction is unsettled. Nor if it contains a non-ASCII character, nor if a table row has the wrong number of cells. The generator throws. This is the mechanism that makes twelve honest predictions possible: there is no code path that quietly drops the ones that came out badly.

The reachability analysis will tell you nothing is deletable. When asked for a sound answer, it gives one, and the sound answer on this estate is zero. Section 6 is about what it costs to buy a better one.

Source & documentation

Continue into the implementation.

The source repository contains setup instructions, design decisions, evaluation guidance and the project's stated limitations.

Browse supporting documentation 11 documents / 5 decision records