Public claim level: concept. This article describes how deterministic static evidence can help a team understand and plan modernization of an ASP.NET Web Forms application without executing it. Every rule ID, evidence tier, coverage label, and limitation cited here comes from TraceMap's public specifications, merged pull requests, and synthetic fixtures. The Web Forms evidence rows on the public legacy modernization evidence map are currently labeled hidden pending checked-in public proof paths, so this is a concept-level explanation, not a demo claim.

The evidence for one button lives in many places.

A single user action in a Web Forms application is spread across distinct evidence sources. The control is declared in markup. The event binding is an attribute on that markup. The handler is a method in a code-behind file, wired to a partial class that a designer file also claims to know about. Navigation to the page may be registered in a global application class, a configuration file, or a hard-coded URL in another page. The handler may call a service client, a generated proxy, or a database operation defined somewhere else entirely. Whether the project even loads in a modern toolchain is itself evidence, recorded or lost.

Modernization fails quietly when a team treats any one of these sources as the whole picture. The markup alone does not name the code path. The code-behind alone does not prove the control renders. The build alone does not prove the feature works. An inventory that survives contact with a real legacy application has to keep these sources separate, cite each one, and record where the join between them is uncertain.

A button labeled “Save” is not an identity.

Labels are display text, not canonical identity. Ten pages can show a button that reads Save. The stable identifiers are the declared control ID in markup, the generated designer field, the bound event-handler name, and the page type that owns them. Those identifiers only become an identity when declared evidence links them, and every link in that chain can be stale: designer files drift from markup, handlers share names across pages, and lifecycle methods such as Page_Load depend on an explicit AutoEventWireup setting rather than any visible subscription.

TraceMap's answer is to assemble identity from declared evidence with rule IDs attached, and to refuse the join when the evidence is ambiguous. An unmatched handler name is a gap, not a guess. A convention-based lifecycle binding is only recorded when the wiring is explicit, and even then it stays at a textual tier rather than being promoted to compiler-resolved evidence.

What static evidence can observe, and at what tier.

Where evidence exists, TraceMap extracts it deterministically and labels it: pages, controls, master pages, and user controls declared in Web Forms markup (legacy.webforms.inventory.v1), declared event and property bindings such as OnClick and OnTextChanged (legacy.webforms.event-binding.v1), candidate handlers resolved against code-behind symbols (legacy.webforms.handler-resolution.v1), designer control declarations, and composed event-flow rows that carry a classification, a terminal surface kind, and supporting fact IDs (legacy.webforms.event-flow.v1).

Around the pages themselves, classic ASP.NET route registrations (legacy.aspnet.route.v1), handlers, page methods, configuration surfaces, and static navigation references are recorded under the legacy.aspnet.* rule family, and the toolchain's effect on analysis is recorded under analyzer.capability.* rules such as analyzer.capability.syntax-fallback.v1 and analyzer.capability.downstream-coverage.v1.

Every conclusion keeps its provenance: repository and commit identity, repository-relative file paths and line spans, an extractor version, an evidence tier from Tier1Semantic through Tier2Structural, Tier3SyntaxOrTextual, and Tier4Unknown, a coverage label, supporting fact IDs, and a documented limitation. Where a value is sensitive, TraceMap stores a context-separated hash or omits it; raw source snippets, raw SQL, and configuration values are not public evidence.

Handler resolution is a ladder, and the weakest rung caps the chain.

Connecting a declared binding to a handler is not one step but a bounded ladder. A Tier1Semantic match means a compiler-resolved method symbol in the linked partial class. A Tier2Structural match relies on page identity, method name, and a common event signature. A Tier3SyntaxOrTextual match is a filename-and-method-name agreement when the project cannot be loaded semantically. Anything ambiguous, missing, or unsupported becomes a Tier4Unknown gap.

The binding itself stays declared evidence at a structural or textual tier. A composed flow is classified from the resolved handler tier and the terminal evidence: StrongStaticEventFlow requires a Tier1Semantic handler and at least one Tier1Semantic terminal, a syntax-tier handler downgrades the flow to NeedsReviewEventFlow, anything between becomes ProbableStaticEventFlow, and a flow with no terminal evidence is recorded as NoBackendEvidence or, when coverage is reduced, UnknownAnalysisGap. A binding that resolves through weaker links does not disappear from the record—it keeps its own tier, its own supporting fact IDs, and its documented limitation, so a review can see where confidence drops instead of averaging it away.

A bounded example chain, with the unproven links marked.

In a synthetic fixture, a chain can be walked entirely from declared evidence: a control is declared in markup with an event attribute; the attribute names a candidate handler; the handler resolves against a code-behind symbol at a recorded tier; a static call path leads from that symbol toward a terminal surface—a data operation, a service operation, an HTTP client, or a dependency surface—recorded as a surface kind and a hash, not as raw text. The chain ends in a question a human can act on: who owns this path, and what test covers it?

Every unproven link stays marked. The control being declared is not evidence it is visible at runtime. The binding being declared is not evidence the event fires. The handler resolving is not evidence it is reachable in production. The call path being static is not evidence the operation executes. The terminal surface being identified is not evidence a specific implementation was selected or invoked. A chain with those marks is still useful for planning; the same chain without them is a plausible wrong graph.

A successful build can still mean reduced feature coverage.

Compilation success says the code the compiler can see type-checks. It says nothing about markup that declares bindings no handler satisfies, handlers no markup reaches, configuration that redirects traffic elsewhere, or project files a modern toolchain refuses to load. In legacy .NET repositories, failed or partial MSBuild project loading is a first-class input, not a dead end: syntax fallback runs over the source, capability diagnostics record what was attempted and what reduced semantic coverage, and the scan is labeled reduced rather than clean.

A loud incomplete graph is safer than a plausible wrong graph. Knowing that page event wiring is only textually resolved, or that a downstream surface has no evidence at all, is what lets a team trust the parts that are resolved and budget review time for the parts that are not. The site limitations and the legacy .NET evidence lane document these boundaries in detail.

Why this is useful before anyone rewrites code.

An evidence-backed inventory supports the work that actually gates modernization: application inventory that does not depend on one person's memory; bounded migration slices chosen from declared surfaces rather than guesswork; owner interviews that start from a cited page, control, or call path instead of a blank page; and a test-planning handoff that lists which chains need coverage before behavior moves.

It also surfaces missing evidence while missing evidence is still cheap. A gap discovered before a rewrite becomes a question; the same gap discovered after becomes a defect. The review handoff matrix turns each of these rows into a bounded packet—review question, evidence to bring, required proof field, limitation, owner, allowed wording, and stop condition—so tribal knowledge becomes a reviewable artifact instead of the only copy.

What this evidence does not prove.

Static evidence does not prove that a page or control is visible at runtime, that an event fired, or that a handler is reachable in production. It does not prove that dependency injection selected a specific implementation, that SQL or any other operation executed, or that any of it matches business intent. It does not establish feature parity, migration correctness, migration success, release safety, or complete application coverage. Absence of evidence is not evidence of absence.

TraceMap core scanning and reduction add no LLM calls, embeddings, vector databases, or prompt-based classification. See the static-versus-runtime boundary for how these claims are meant to be read.