# Scan Best Practise

## Operational Priority

This document focuses on scan technique and quality.

For workflow, priority, interruption, and context-budget rules, always follow:
- `construction/codex-rescan-guild.md`

If any instruction here conflicts with the guild, the guild wins.

## Goal

Produce a method-level dependency map that is accurate enough to answer all of the following without reopening the source tree:

- What methods are actually exposed by this binary at runtime
- What each method calls internally and externally
- Under what exact conditions each dependency is used
- What startup, background, and side-effect paths exist outside the request/response path

## Source of Truth

Always treat source code as the source of truth. Never trust an old `approved` scan without rechecking the current entrypoints and registration code.

Old scan outputs are still useful, but only as a recall set:

- Use old `interfaces` and `service-mapping` files to detect methods or dependencies that your fresh scan may have missed.
- Diff old method keys against the runtime surface you rebuilt from source.
- Re-check every old-only item against source before keeping it.
- Never promote an old result directly back to `approved` just because it existed before.

For each binary, scan in this order:

1. Entry file: `main.go`, cobra root command, or equivalent bootstrap.
2. Registration layer: `Register*Server`, router setup, webhook setup, worker setup, reverse proxy setup.
3. Dependency injection layer: constructors, `Provide`, `Invoke`, `InitDependencies`, wire sets.
4. Method implementation layer: the concrete server methods or handler functions.
5. Background paths: eventbus, workflow, cron/period workers, cache warmers, watchers, migrations.

## Accurate Interface Enumeration

Do not derive interfaces from proto files alone.

Use the runtime registration code to decide what the binary actually exposes:

- Registered gRPC services
- Registered HTTP routes and reverse proxies
- Webhook handlers
- Health, readiness, metrics, profiling, and reflection surfaces when they are part of the binary's runtime contract
- Config-gated or flag-gated method exposure

### Important correction from `tidb-management-service`

- `tempo-proxy` does not necessarily expose the full `CoreService` surface. Its registration is filtered by `cfg.Server.ForwardMethods`, so the effective API surface is configuration-dependent.
- `tidb-mgmt-service` does more than register gRPC servers. `RegisterGrpcServer` also starts a reverse proxy and a background worker. A scan that only records proto services is incomplete.
- `dedicated-global-service` and `nextgen-global-service` both have startup modes such as `grpc-server-only`; these modes change whether migrations, workers, eventbus, workflow, and audit log setup happen.
- `inference-service` exposes `/metrics` from a separate HTTP server, not from the main Gin router. Scans must treat secondary runtime listeners as part of the binary surface instead of assuming everything is mounted on one router.
- `jiralert` shows the inverse failure mode: a home page can link to `/metrics` or `/debug/pprof` without registering those handlers. Count only handlers that are actually mounted in router or `http.Handle*` code, not links or README claims.
- `jiraport` shows a third failure mode: a `cmd/*` directory may still be `package main` but no longer contain a buildable entrypoint because the whole old main is commented out. Do not count a directory as a binary until you confirm there is a real runnable entrypoint.
- `auditservice` adds another runtime-surface trap: routes can be registered in source but still be absent from steady-state runtime if a blocking loop starts before the HTTP server. `cmd/subscriber` only reaches `engine.Run(...)` after `eventbus.Run()` returns, so `/healthz` and `/ping` are not effective runtime APIs during normal operation.
- `serverless-service` adds two more traps. First, a single `cmd/server` binary can expose very different surfaces by Cobra subcommand (`http`, `grpc`, `worker`, `brworker`, `exec`); scan the active subcommand runtime, not just the binary path. Second, grpc-gateway can expose the same business method through HTTP without creating a second implementation path; record the transport aliases, but do not double-count them as separate business logic.
- `serverless-service` also shows why receiver type names are not a safe grouping key. `clusterRpcService` exists in multiple packages with different registered proto services. Group methods by the runtime-registered service name and package, not by receiver type alone.
- `devops` adds three more runtime-surface traps. First, large Gin binaries may expose hundreds of handlers where Swagger `@Router` covers most business routes but misses health, proxy, MCP, or internal-only registrations; use Swagger annotations as a recall set, then reconcile them against the actual router wiring. Second, one binary can host multiple listeners and protocols at once (`HTTP API`, `gRPC`, `MCP`, metrics/debug); scan each listener separately instead of flattening everything into one HTTP surface. Third, helper agents such as `jump-pod-agent` may look like tiny HTTP binaries, but their real dependency surface is dominated by background filesystem watchers and subprocess management (`tailscaled`, `tailscale up/down/status`), which must be recorded alongside the exposed handlers.
- `devtier-store-service` adds two more traps. First, a binary that looks like a tiny HTTP service can still be dominated by startup work, cron jobs, cold-start backfills, and a second metrics listener; `cmd/billing` only makes sense when those non-handler paths are scanned together with the HTTP routes. Second, nested submodules or generated demos inside the repo (`service_gen_from_ai`, `pkg/demo`, `tools/*.go`) are not automatically part of the root runtime surface; confirm they belong to the root build and deployment path before counting them as repo binaries.
- `escalation-base` adds another monorepo trap: a repo can contain multiple independent Go modules, embedded upstream projects, and legacy copies at once. Use module path, current Makefile or Dockerfile build targets, and repo-owned import paths to decide what still belongs to the repo's runtime surface. A subtree like `jiralert` can be present in source but still be an embedded external project that should be skipped in favor of its independent repo scan.
- `cost-provider` adds a config-truth trap: deployment manifests, README env lists, or helper functions can mention credentials such as `AWS_REGION`, but that does not make them code-enforced startup requirements. Only treat an env var or config key as required when the executed bootstrap path actually reads or validates it.
- `dataapps` adds a middleware-order trap: Gin routes inherit only the middleware stack that exists at the moment they are registered. `GET /healthz` and `POST /inner/sqleditor/oplog` are wired before later auth, RBAC, request-size, and dedicated-cluster middleware, so they must not inherit those conditions from nearby routes registered later.
- `dataapps` also shows that middleware-driven cross-service calls belong to the exposed route that triggers them, not to fake standalone interface methods. `AuthApiKey`, `CheckUser`, and `CheckDedicatedClusterPermission` all call downstream gRPC services, but they should be recorded as route conditions and dependencies on the guarded handlers instead of being promoted into public runtime methods.
- `dataservices` adds a facade-chain trap: upstream binaries in the same repo can expose the public API while doing almost no business work themselves. `dataapi` mostly resolves cluster and credential context before forwarding to `dataapi_gateway`, and the real SQL execution happens in `dataapi_executor`. Keep each hop's dependencies on its own binary instead of smearing executor-side storage and SQL work onto the global facade.
- `dataservices` also adds a coverage-closure tactic for very large Gin binaries. For `cmd/central`, the reliable path was to rebuild controller groups incrementally, keep method keys canonical from the start, and repeatedly diff `interfaces` against `service-mapping` until missing keys reached zero. That is more accurate than trying to flatten a 100+ route monolith in one pass.
- `infra-api` adds a leader-election trap: a controller-runtime binary can register controllers synchronously but defer critical runtime side effects until `mgr.Elected()` fires. `infra-provider` only starts `gardenerEnv` and custom metrics collectors after leadership is acquired, so scans that stop at `SetupWithManager` under-report real runtime behavior.
- `infra-api` also adds an index-page trap: a debug or status page can render links for routes that were never registered. `cmd/debug` always prints the shared-provider log link, but the handler exists only when a kubeconfig context matching `.*f01.*` is discovered.
- `infra-api` adds a webhook-reader trap too: admission handlers may use a read-through client that first hits the manager cache and then falls back to the direct API reader on NotFound. That still counts as a Kubernetes API dependency even though no external RPC client is constructed in the webhook file.
- `account` adds a registration-drift trap: controller methods, permission maps, or path-resource regex tables can mention routes that the current proto or router registration no longer exposes. `internel/controllers/openapi/user_controller.go` still contains `GetUser`, and `gateway_openapi.go` still lists `/v1beta1/users/{id}`-style permission patterns, but the active `openapi/v1beta1` `service.proto` no longer registers that RPC. Runtime registration still wins.
- `billing` adds the same trap on HTTP routers: permission maps or old OpenAPI docs can preserve legacy path shapes that no longer exist in runtime registration. `Path2ResourceMap` still mentions `/orgs/{org_id}/v1beta1/...`, but the live binary only registers `/v1beta1/...`, `/v1beta1/msp/...`, and `/v1beta1/partners/...` groups; scans must remove those stale path variants instead of inventing fake interface methods for them.
- `account` also adds a shared-middleware trap on large admin routers: once a Gin group is guarded by one auth/RBAC middleware chain, that chain becomes a real dependency of every protected route unless a specific whitelist branch skips part of it. Rebuilding `admin/cmd/server` accurately required bulk-attaching `adminAuth -> DevOps GetUser -> optional HasAccess` to each protected route first, then layering handler-specific storage or downstream calls on top.
- `account` adds a wrapper-middleware trap too: an HTTP handler's real dependency surface may come from the outer `WrapH` / `StripPrefix` / middleware stack in `main()`, not only from the handler body. `BillingProxy` itself only resolves `GetUserFromContext`, validates the allowlist, and reverse-proxies, but the runtime path also depends on `proxyMiddlewares := server.InitProxyMiddlewares(...)` that inject authenticated user context before the handler runs.
- `account` also adds an implementation-name drift trap: the concrete Go method name can differ from the canonical proto method key, so mapping keys must follow the registered interface rather than the implementation symbol. `AuditLogService` exposes `CreateAuditLog` in the proto, but the server implementation method is still named `CreateAuditLogReq`; keeping the implementation name in `service-mapping` creates a dangling extra key and hides a missing real method.
- `account` adds a stub-surface trap as well: some registered/provider methods are intentionally present only to return a short-circuit error such as `not support yet` or a constant allow/deny result. `CreateClusterUser` and `GetClusterUser` in `central/cmd/provider` are currently hard-coded `Forbidden`, and `CheckIdentityAndPermission` only short-circuits on non-API-key callers then returns `passed` without a downstream lookup. These methods should stay empty in dependency mapping rather than being backfilled with guessed storage or service calls.
- `dataflow-service` adds a partial-override trap: registering one concrete gRPC server for a proto service does not mean that server overrides every proto method. `cmd/dataflow` wires `BackupServiceServer` and `BackupInternalServiceServer`, but `ConsoleBackupService.ModifyBackupSetting/GetBackupSetting/GetAutoSetting` and `BackupInternalService.ListRestoreOfProject` still fall through to the generated unimplemented handlers because the concrete structs embed `Unimplemented*Server` and never override those methods.
- `account` also adds a middleware-inherited metadata trap: read-only SCIM metadata endpoints are not public just because they only return schema documents. `GET /orgs/:org_id/scim/v2/ServiceProviderConfig`, `/Schemas`, and `/ResourceTypes` still inherit `scimProvisionCheckFunc -> scimAuthFunc -> scimRbacFunc`, so their `conditions` must include SCIM enablement, credential validation, and RBAC even though the handler body only reads in-memory schema metadata.
- `account` adds a startup-surface trap too: startup lifecycle methods can be real interface coverage targets when the binary model includes them. `admin/cmd/server` was only fully understandable after modeling flag parsing, config init, Auth0 JWKS warmup, eventbus startup, job queue init, Jaeger init, router construction, and final `RunHTTPServer` as first-class lifecycle entries rather than leaving them as anonymous boot code.
- `config-service` adds a dead-helper trap: a repo can keep background worker helpers, tenant fan-out logic, or extra watcher scaffolding in source, but if `main()` never starts them they are not part of the runtime surface. `ClusterConfigController` still contains tenant-worker code, yet the live binary only starts the alive-cluster watcher.
- `config-service` also adds a path-separation trap: startup, request-path, and background reconcile dependencies can look similar in names but hit different systems. Its HTTP handlers only read ClusterConfig plus infra-api CRs, while account/core service calls and PD proxy writes happen in startup warmup or the alive-cluster watcher.
- `tidb-operator` adds a controller-runtime sidecar trap: a controller binary can create its real external watch surface through a separate manager such as `PDClientManager`. `pdcm.Start(ctx)` plus member/store pollers are part of runtime truth and must be modeled separately from Kubernetes watches and `Reconcile()` itself.
- `tidb-operator` adds a helper-bug trap too: lifecycle hooks and utility binaries may contain broken flag wiring. `prestop-checker` advertises `--ca/--cert/--key`, but source binds all three to `addr`; record the actual bug and its effect instead of silently upgrading the binary to the behavior you expected.

## Condition Scanning

`conditions` must describe why a dependency runs, not just that it exists.

Use `Always` only when the call is truly unconditional in the executed path. Otherwise, record the real predicate. Common condition categories:

- Request shape: specific fields, enum values, nil checks, pagination tokens, validation success
- Deployment mode: dedicated/serverless/nextgen, mock mode, local mode, dry-run mode
- Feature flags: config switches, feature gates, dynamic config toggles
- Command flags: `grpc-server-only`, `config`, explicit enable/disable flags
- Data state: cache miss, existing row found, workflow state, leader-only execution
- Provider branch: AWS/GCP/AliCloud, dev tier vs non-dev tier, tenant plan, region type
- Fallback path: primary call failed, target not found, legacy compatibility path
- Registration-time conditions: route registered only when config is present, method forwarded only when listed in config
- Persistence timing: in-memory state may be written only by periodic save loops or shutdown hooks rather than in the request path

Write conditions at the same level as the actual branch. Do not compress multiple branches into a fake `Always`.

## Method-Level Dependency Tracing

For each exposed method:

1. Find the concrete implementation function.
2. Trace all direct repository, client, and infrastructure calls.
3. Trace wrapper calls until the actual target service or storage operation becomes clear.
4. Record both the dependency and the implementation symbol that proves it.
5. Keep separate operations when a method writes and then calls another service, or calls different services under different branches.

Good scans distinguish:

- Direct DB access vs repository wrapper
- Internal in-repo service call vs cross-repo RPC
- Optional side effects vs mandatory execution
- Stubbed or unimplemented methods vs real implementations

### Important correction from `tidb-management-service`

- A method registered in proto may still be effectively absent or configuration-gated at runtime.
- A registered concrete server may implement only a subset of the proto surface. Always diff the generated service interface against the concrete receiver methods before assuming every proto method is live.
- A registration function may start workers or reverse proxies that are not tied to a single RPC method but still belong to the binary surface.
- Old aggregate summaries such as `"ClusterService": {"CreateCluster": ...}` are not enough. Convert them into canonical method keys such as `ClusterService.CreateCluster`.
- Handlers may do an optimistic local gate first and write the real side effect later. For example, quota checks can happen before the final provider cost is known; scans should document both the gate condition and the later persistence path instead of collapsing them into one fake synchronous dependency.
- A rate limiter or queue is not automatically a remote dependency. `jiralert` uses an in-memory `rate.Limiter` for per-summary throttling and only touches Redis for optional distributed locks. Separate local throttling from real cross-process locking, and keep asynchronous worker-side effects attached to the original exposed method.
- OpenAPI or gateway wrappers are not valid stopping points for dependency tracing. `dataflow-service`'s openapi `Import` and `ChangefeedService` methods mostly delegate to in-binary controller singletons, but an accurate scan must keep following that chain until it reaches the real MySQL write, regional RPC, RBAC call, or other external effect.
- Event-driven binaries need two layers of method tracing: the transport ingress and the event-name fan-out. `jiraport`'s resolver `POST /` only accepts `lambda.Event`, but the real dependency map lives under `JiraIssue`, `NotificationAck`, `NotificationAction`, and `HookEvent`, with `NotificationAction` then branching again by `cmd[0]`.
- Fan-out aggregators need their own explicit method surface. `cost-provider` has only one public business endpoint, but the real dependency and condition depth lives in `cost.Manager.QueryBill`, which concurrently invokes multiple provider clients, chooses provider-specific SQL templates, and fails the whole request when any provider branch returns an error. If you flatten everything directly onto the HTTP handler, you lose the true branching and failure semantics.
- When middleware performs real downstream RPCs, keep those RPCs attached to the protected route and preserve registration-order differences. In `dataapps`, the same Gin engine hosts public routes, an inner oplog route, an EDA proxy, metrics, and health checks, but each surface inherits a different middleware stack depending on when it was registered.
- When a controller-runtime binary starts secondary managers, workers, or collectors only after winning leadership, model that work as explicit lifecycle methods or background workers instead of hiding it inside a generic `StartManager`.
- When one binary mixes `cmux` for primary HTTP + gRPC, a separate controller-runtime metrics listener, and an optional nested debug listener started by a feature-gated subsystem, model each listener as a separate runtime surface. `aws-shared-provider/tidb-worker` needed all three surfaces (`api-server-addr`, `metric-bind-addr`, and the resource-manager `:9292` debug server) to avoid undercounting exposed methods.
- When one binary multiplexes HTTPS and gRPC on the same `http.Server` instead of `cmux`, still model the HTTP handlers and gRPC methods as distinct runtime methods. `aws-shared-provider/cmd/manager` serves both through one TLS listener and dispatches by `ProtoMajor == 2` plus `Content-Type: application/grpc`, while metrics and webhook still live on separate controller-runtime listeners.
- When manager runnables expose a small control HTTP surface but do most work in `Start()` loops, split those paths explicitly. In `aws-shared-provider/tidb-worker`, admin, GC, GCV2, TTL, auto-analyze, and background-task runnables all expose tiny HTTP or gRPC control methods while their real PD/MySQL/Kubernetes dependency depth lives in polling, recycle, jammed-worker, and autoscale loops.
- When a binary mixes request handlers with periodic reconciliation loops, separate the dependency maps by execution path. Do not smear background `core-service`, `gardener`, or `pd-proxy` calls onto HTTP endpoints that never execute them.
- When a controller binary has no public listener, it still needs a non-empty runtime surface. `autoscaler/balancer` only exposes lifecycle and reconcile paths, but those still belong in `interfaces` and `service-mapping` instead of collapsing to an empty file.
- When a controller repo exposes only a tiny debug HTTP surface, do not let that crowd out the real runtime. `cluster-autoscaler` needed leader election, pod observers, startup loops, and autoscaling iterations modeled alongside `/metrics` and `/health-check`.
- Client initialization is not a downstream call. `aitestsuit` calls `openai.InitGPT(cfg.ChatGPTToken)` during startup, but no runtime path ever reaches `openai.GetReplyFromAI` or `CreateCompletion`; scans should keep that as local setup unless a request or worker path actually uses the client.
- Queueing is not the end of the dependency trace. `aitestsuit`'s `POST /api/v1/task` only writes rows and enqueues onto `msgCh`, but an accurate scan must follow that queue into `StartWorker -> runTask` so the real MySQL, `aipower`, and `dataservices` dependencies are attached to the background execution path instead of being lost behind a local channel send.
- Imported package `init()` functions can create real startup dependencies before `main()` runs. `api-gateway/ui/internal/dao.init()` opens MySQL eagerly, so startup scans must include package-init side effects when imported packages establish DB handles, caches, or other live resources.
- `NoRoute` or framework fallback handlers are part of the runtime surface when they serve real content. `api-gateway` exposes its frontend through `r.NoRoute(func(c *gin.Context) { c.File(...) })`, so a scan that only counts explicit `/api/*` routes under-reports the binary.
- Cobra `PersistentPreRunE` belongs to every subcommand mode it guards. `auth-server` loads local config, remote oneconfig state, and starts a background Viper watch loop before `http`, `grpc`, and `migrate`, so scans must attach those startup and background effects to all reachable modes rather than only the default command.

## Canonical Output Rules

Keep one canonical schema per repo. Mixed schemas make later review and automation unreliable.

Preferred rules:

- Interfaces file uses one stable top-level field for services and methods.
- Service-mapping file uses one stable top-level `dependencies` object keyed by method.
- Every method key in `service-mapping` must exist in `interfaces`.
- No dangling mapping-only keys.
- No service-level aggregate blobs once method-level output exists.

If an old file uses legacy fields such as `grpc_services`, mixed summaries, or aggregate service blobs, rebuild it instead of layering more patches on top.

## Quality Checks Before Approval

Do these checks every time:

- Registered service count in source matches interface file.
- Method count in interface file matches the registered proto or route surface after runtime filtering.
- Every documented method has a mapping entry, even if the entry is intentionally empty.
- No mapping entry points to a missing method.
- No unconditional `Always` remains where a branch, mode, flag, or feature gate exists.
- Startup/background/runtime side effects are represented when they materially affect dependencies.
- Reflection, metrics, pprof, healthz, readyz, reverse proxy, and webhook surfaces are included when they are intentionally exposed by the binary.
- Unimplemented, stub, or pass-through methods are marked as such instead of being inferred as full business logic.

## How To Improve Old Scan Quality

When reviewing an old scan, actively look for these failure modes:

- Old approved result still uses legacy schema.
- Interface file and mapping file use different naming conventions.
- Method counts look right, but registration-time conditions were ignored.
- Only proto services were scanned, but runtime side effects were omitted.
- `Always` was used where the source clearly has branches.
- Proxy binaries were treated as if they exposed the full upstream proto instead of a filtered subset.
- Background workers, migrations, eventbus startup, or audit log setup were omitted because they do not sit inside an RPC handler.
- Swagger-annotated handler sets were trusted as the full HTTP surface even though runtime registration also added undocumented proxy, MCP, health, or internal routes.
- Background watchers or subprocess lifecycles were dropped because the binary looked request-driven at first glance.
- Startup backfills, cron registration, or separate metrics listeners were omitted because the old scan only followed HTTP handlers.
- Route conditions were copied from shared middleware files without checking registration order, so early-registered health or inner routes incorrectly inherited later auth, RBAC, or request-size guards.
- Middleware-auth or RBAC helpers were promoted into fake interface methods even though the runtime only exposes the guarded business routes.
- Wrapper or delegation methods were left empty because the controller only forwarded once, even though the downstream singleton/controller/service implementation still performed real DB writes, Auth0 calls, email delivery, or event publication.
- Startup helper functions were treated as local setup only, even though a nested helper such as `InitMeasure(...)` also launched a scheduler, metrics loop, or other steady-state background worker.
- Controller scans stopped at `SetupWithManager` and missed leader-gated startup work that begins only after `mgr.Elected()` or similar runtime hooks fire.
- Controller binaries with no network listener were collapsed into empty interface files instead of lifecycle plus reconcile/runtime methods.
- Index or status pages were trusted as route inventories even though some linked handlers were registered only under optional config or context conditions.
- Admission webhooks were treated as pure local validation even though their read-through helper touched the Kubernetes API via cached and direct readers.
- Helper watchers or tenant fan-out workers were counted as live runtime behavior even though the entrypoint never started them.
- Background reconcile traffic to external services was incorrectly assigned to user-facing HTTP handlers because both touched the same domain objects.
- Upstream facade handlers and downstream executor workers inside the same repo were collapsed into one fake dependency path, hiding which binary actually touches SQL, storage, or tenant-regional routing.
- Nested submodules, demos, or standalone tools were counted as if they were part of the root service deployment.
- Embedded upstream projects or duplicate subtrees inside a monorepo were counted as first-class binaries even though their module path, build wiring, and ownership point to another repo.
- Env or config requirements were inferred from deployment YAML, README text, or dead helper functions instead of the executed startup path.
- Separate cache and poller runtimes such as `PDClientManager` were collapsed into generic controller logic, so external PD-driven watch surfaces disappeared from the scan.
- Lifecycle hooks or build/test tools were summarized by README intent while source-level flag bugs, TLS/file preconditions, and real transaction loops were left undocumented.
- Multi-mode entrypoints were flattened into one fake union surface even though flags or roles produced materially different steady-state runtimes. `core-portal/cmd/server` can either expose the full gRPC + gateway + proxy stack or only `/healthz` and `/version` depending on `CENTRAL_ROLE`; those modes must be modeled separately.
- Handlers were recorded as public interfaces just because they were registered on a mux, even though no stable listener ever served that mux. `core-svc/svc/cmd/server` currently builds a throwaway mux in `setupMetricsAndPprof()` and attaches pprof handlers to it, but only the TiInf metrics listener is configured as a steady-state listener; this means `/debug/pprof/*` should not be modeled as stable interfaces until listener wiring proves they are really served.
- Framework-provided builtin routes were ignored because the binary only showed a custom request handler. `VictoriaMetrics/app/vminsert` uses `lib/httpserver`, so `/health`, `/ping`, `/metrics`, `/-/healthy`, `/-/ready`, `/debug/pprof/*`, and `*/favicon.ico` belong to the main listener even though they are not declared inside `requestHandler`.
- Secondary protocol listeners were collapsed into one generic bucket such as `TCPListeners.*`, which hid transport-specific conditions and dedicated HTTP listeners. `VictoriaMetrics/app/vminsert` needs separate runtime methods for cluster-native TCP, Graphite, Influx, shared OpenTSDB, and the dedicated OpenTSDB HTTP listener.
- Proxy binaries with an optional internal listener were flattened into one fake endpoint surface, which hid where builtin and control routes actually lived. `VictoriaMetrics/app/vmauth` serves `/-/reload`, `/metrics`, `/health`, and `pprof` on the main listener only when `httpInternalListenAddr` is unset; once that flag is set, those routes move to the internal listener and the main listener becomes proxy-only.
- Sibling binaries in the same repo were assumed to share admin routes, so old scans copied endpoints such as `/-/reload` into binaries that never registered them. `VictoriaMetrics/app/vmstorage` uses the same `lib/httpserver` builtin health/metrics/pprof surface as other binaries, but it does not define a reload endpoint; route inventories must come from the concrete request handler and dispatch code, not from adjacent binaries or docs.
- Single-tenant and multitenant routes were collapsed into one fake endpoint family because they shared parser code, which hid how tenant identity actually enters the dependency chain. `VictoriaMetrics/app/vmagent` reuses the same insert handlers for both modes, but multitenant routes first parse `/insert/{tenant}/...` into an auth token while single-tenant routes pass `nil`; scans should model those as separate method families even when the downstream `TryPush(...)` implementation is shared.
- Entry points with an early `handleStaticAndSimpleRequests(...)` stage were treated as if every route flowed through the later concurrency-limit, tracing, and slow-query branch. `VictoriaMetrics/app/vmselect` is a counterexample: builtin routes, VMUI/static assets, vmalert proxy paths, relabel-debug helpers, and placeholder metadata/buildinfo endpoints return before the expensive query path runs, so scans must not inherit query-execution conditions such as `search.maxConcurrentRequests`, slow-query logging, or vmstorage fan-out onto those early-return routes.
- UI and API aliases were duplicated into separate fake methods even though both paths hit the same handler and local state. `VictoriaMetrics/app/vmalert` serves both `/api/v1/*` and `/vmalert/api/v1/*` for the same alerts, rules, rule-detail, alert-detail, and notifiers handlers; the scan should keep one canonical method with alias notes instead of inventing duplicate method keys and then pretending they are distinct runtime surfaces.
- `admin_cli` binaries were treated as automatically skippable even when the repo-level task explicitly required the whole deployable surface to be rechecked. `VictoriaMetrics/app/vmctl` is still an `admin_cli`, but its subcommands have real cross-system migration edges to OpenTSDB, InfluxDB, Prometheus remote-read, VictoriaMetrics import/export, and local snapshot/block files; when such a binary remains in-scope, model the command actions as methods instead of leaving a coarse `CLICommands.*` blob.
- Do not infer binary ownership from route names or file names alone. `billing` is a counterexample: `/api/v1/billing/worker/run` and `/api/v1/billing/worker/subtask/run` sound like worker-binary endpoints, but only `cmd/server` mounts `InitializeWorkerRunActionRouter`; `cmd/worker` itself serves just `/version`, `/metrics`, pprof, and the gocron scheduler loop.
- Shared handlers reused by stress or debug binaries must be re-registered from the concrete entrypoint, not copied from the main server surface. `billing/cmd/stress` reuses `measureAPI.CostUnits`, but the runtime path is `POST /stress/billing/tenants/:tenant_id/measures` under a fixed port 16688 listener, while eventbus/payment/marketplace startup stays disabled because those lines are commented out in the stress entrypoint.
- When one binary mounts several HTTP groups with different base paths, derive the final route from the concrete router group plus `Handle*` registration, not from Swagger host/basepath comments or from which package defines the handler. `billing/cmd/server` registers generic `/v1beta1` billing routes, `/v1beta1/msp/*`, and `/v1beta1/partners/*` separately; trusting handler names or OpenAPI docs can easily mis-bucket `/v1beta1/tenants/...` routes under the MSP group or invent partner cluster routes that were never registered.
- Before using coverage counts, normalize old mapping keys to the same canonical naming scheme as the interface file. `billing/cmd/server` still had dozens of legacy bare HTTP keys like `GET /v1beta1/partners/bills/:billedMonth` after the interface had moved to `HTTP_API.GET ...`; until those keys were normalized, coverage numbers overstated missing methods and hid the real remaining gaps.
- When one handler is registered twice with and without an explicit scoping parameter, keep the dependency chain aligned unless the code truly branches. `billing/cmd/server` registers many pairs like `/tenants/:tenant_id/...` and `/tenants/:tenant_id/billing_config_id/:billing_config_id/...` against the same handler; scans should reuse the same method-level dependency logic and only tighten conditions around how `getBillingCfgFromURI(...)` resolves the billing config, instead of leaving the scoped variant empty or inventing a separate business flow.
- Do not assume tenant-scoped routes always operate on the tenant from the path. Some flows first resolve local payment state and then redirect the business lookup to a different tenant or org. `billing/cmd/server` support-plan reads are the counterexample: once the active payment method is `MSP`, the handler parses `PaymentCode` into the reseller tenant and reads that tenant's active binded plan instead of the caller's own row. Scan conditions should record this identity handoff explicitly.
- When a handler can synthesize a default response after a not-found branch, keep the storage dependency conditional on the persisted branch. `billing/cmd/server` `GetActiveTenantSupportPlan` still returns `marketing.DefaultBindedSupportPlan(...)` when no active binded plan exists; the scan should show the attempted lookup, but it should not invent a phantom `support_plans` row read for the fallback object.
- Route names, Swagger summaries, and response-model names do not override the concrete handler body. `billing/cmd/server` has two counterexamples: `GET /tenants/:tenant_id/plans/ondemand/effective-discounts` currently just calls `GetTenantDiscount(...)` instead of `ListEffectiveDiscounts(...)`, and the global create/update discount handlers are still explicit offline stubs. Scans must record the runtime truth even when the route name implies a richer behavior.
- A scoped path parameter is not proof that the implementation actually uses it. `billing/cmd/server` registers `.../billing_config_id/:billing_config_id/discounts/:discount_id` update/delete routes, but the concrete handlers only re-bind `tenant_id` and `discount_id` before calling `UpdateDiscount(...)` / `DeleteDiscount(...)`. Keep the scoped route in the interface, but do not invent nonexistent billing-config validation or branching in the mapping.

In practice, the fastest way to improve an old scan is:

1. Re-read the binary entrypoint and registration code.
2. Compare registered runtime surface with both the current interface file and the previous approved output.
3. Diff method keys between interfaces and service-mapping until coverage reaches zero missing keys.
4. Search for suspicious `Always` conditions.
5. Search for startup side effects after registration.
6. Rebuild any legacy aggregate file into canonical method-level form.
7. For large controller trees, harvest Swagger `@Router` annotations as a coverage baseline, then diff that list against the actual router registration to catch undocumented runtime routes before approval.
8. In monorepos, verify service ownership before scanning deeply: check `go.mod` module path, current build targets, and whether the subtree is already represented by a different standalone repo.
9. When a public method is just a wrapper, scan through the delegated controller/service method and record the downstream dependencies there instead of leaving the wrapper empty.
10. Follow startup helpers transitively: if `main()` calls a helper that internally starts a worker or scheduler, include that background runtime in the scan even when `main()` does not invoke it directly.
11. If one deployable binary registers multiple proto packages that reuse the same service name, qualify the interface service name by runtime surface or package before building method keys; otherwise canonical `Service.Method` keys will collide and silently corrupt coverage accounting.
12. Do not assign deferred worker dependencies to the request that merely schedules or records future work. `billing/cmd/server` `RunTenantClose` and `CancelTenantClose` only create or update `tenant_close_records`; the real cleanup, support-plan cancellation, infra cleanup, and close-email fan-out happen later in `TenantCloseModel.ProcessTenantClose`, so those downstream dependencies must stay on the processor/runtime method rather than the kickoff HTTP route.

## Review Standard

A scan is ready for approval only when:

- The runtime surface is correct
- The schema is canonical
- Method keys line up across files
- Conditions reflect the real code branches
- Side effects outside the handler path are not silently dropped

If any of these fail, downgrade the repo from `approved` and record the concrete reasons in `deep-scan-state.json`.
