/** * API Usage Scanner * * Derives, by scanning real source (not a declaration file), how every project * relates to the api-lib projects it depends on. This is the single source of * truth for the `apiRelations` field in architecture/dependencies.json AND for * the runtime microservice graph. * * Signals (all resolved through the TypeScript checker, so re-exports resolve): * - IMPLEMENTS: `apiFactory.addRoutes(XxxApi, XxxController)` — the registration * that actually SERVES the contract over the wire. We deliberately * do NOT use `class Ctrl extends XxxApi`: a class can extend an API * as an in-process test double / simulator (e.g. Server2Simulator) * without ever serving it — only `addRoutes` proves a served route. * - USES: `factory.createRpcClient(XxxApi, ...)` → rpc client * `factory.createPubSubClient(XxxApi, ...)` → pubsub (Cloud Tasks) client * The config argument (`new ClientConfig('helper-fsdb')`) names WHICH service the * client talks to and is kept as `ApiRef.targetService` — see targetServiceOf. * An api-lib is DETECTED, not tagged: a project exporting an `abstract class` * carrying `@ApiPath` owns that API. Its transport is `@PubSub` → 'pubsub', else 'rpc'. * * Contracts are indexed from SOURCE in a pre-pass (ApiSourceIndexBuilder) rather than * from wherever the checker resolves an import to. A consumer without a tsconfig.base * `paths` entry resolves `import { XxxApi } from '@scope/xxx-api'` through node_modules * to the package's BUILT `dist/**.d.ts` — and tsc ERASES decorators when emitting * declarations, so `@ApiPath` can never be read there. Keying off the resolved * declaration therefore dropped whole services from the graph, silently. See * `recoverFromDeclaration`. */ import type { EnhancedGraph } from '../graph-sorter'; import { ProjectInfo } from '../project-info'; import { ApiClassInfo, ApiContracts, EmptiedApiContract, NonLiteralDecoratorArg, ProjectApiRelations, UndeclaredExternalCaller, UnresolvedEndpointPath } from './api-relations'; /** * An `addRoutes`/`createRpcClient`/`createPubSubClient` first argument that resolved to an * abstract class in a DECLARATION file which owns no indexed contract. Unambiguously a broken * scan (a real api-lib whose source we never indexed), never a "this isn't an API" argument — * so it is reported loudly instead of collapsing into a silent `return null`. */ export declare class UnresolvedApiCall { /** The project whose source makes the call. */ readonly project: string; /** The contract class name as written at the call site. */ readonly api: string; /** `path/to/file.ts:LINE` of the call site, workspace-relative. */ readonly at: string; /** The declaration file the checker resolved to (where decorators are erased). */ readonly declaredIn: string; constructor( /** The project whose source makes the call. */ project: string, /** The contract class name as written at the call site. */ api: string, /** `path/to/file.ts:LINE` of the call site, workspace-relative. */ at: string, /** The declaration file the checker resolved to (where decorators are erased). */ declaredIn: string); } /** The whole-workspace result of a scan. */ export interface ApiScanResult { /** projectName -> { apiLibProject -> relation }; only projects with ≥1 relation appear. */ relationsByProject: Map; /** Every project that owns ≥1 API contract class. */ apiLibProjects: Set; /** apiClassName -> where it lives + its transport. */ apiIndex: Map; /** * Projects whose production (non-test) source was actually scanned. A project with only test * files (e.g. an e2e harness), or one the compiler couldn't load, is ABSENT — callers must not * conclude "no implements/uses" for it, because its behavior was never observed. */ scannedProjects: Set; /** * Call sites naming a contract we could not map back to workspace source. Non-empty means the * graph is INCOMPLETE — callers must surface these rather than emit a green, wrong graph. */ unresolvedApiCalls: UnresolvedApiCall[]; /** * Decorator arguments that were present but could not be reduced to a string (a cross-module * constant, a computed expression). Each one costs the graph a basePath, a method, or — when it * takes out every method of a class — the whole contract, so they must be surfaced. */ nonLiteralDecoratorArgs: NonLiteralDecoratorArg[]; /** * The subset of the above that is FATAL: an `@Endpoint` path that could not be read. Every client * builds its URL as `basePath + path`, so this is missing routing, not missing metadata — * buildApiContracts throws on a non-empty list rather than shipping a contract without it. */ unresolvedEndpointPaths: UnresolvedEndpointPath[]; /** * Contract classes that declared `@Endpoint` methods and kept none — the exact shape that used to * slip out through buildApiContracts' zero-method skip, taking a whole service's queues with it. */ emptiedApiContracts: EmptiedApiContract[]; /** * `external` endpoints that did not say WHO calls them. Fatal: the inbound box on the runtime * graph exists to name that system, and with nothing to name it restates our own contract name. */ undeclaredExternalCallers: UndeclaredExternalCaller[]; } /** Statically scans every project for its api-lib implements/uses relationships. */ export declare class ApiUsageScanner { private readonly workspaceRoot; private readonly projectInfos; /** Globs of project roots whose exported `*Api` types are contracts for outside systems. */ private readonly externalApiPaths; private readonly locator; private readonly relationsByProject; private readonly scannedProjects; private readonly unresolvedApiCalls; private readonly decoratorArgDiagnostics; private sourceIndex; constructor(workspaceRoot: string, projectInfos: Map, /** Globs of project roots whose exported `*Api` types are contracts for outside systems. */ externalApiPaths?: readonly string[]); scan(): ApiScanResult; private scanProject; private visit; /** * Record a `uses` for every vendor contract this class receives by CONSTRUCTOR INJECTION — * `constructor(@inject(GMAIL_TYPES.GmailApi) private readonly gmail: GmailApi)`. * * The parameter TYPE is the signal, not the token: a token is an opaque Symbol whose name we * would have to guess at, while the type is written right there and is what the class actually * calls. Matching happens by name against the external index, so an import that resolves to a * built `.d.ts` works exactly as well as one resolving to source. * * A class that IMPLEMENTS the contract is skipped — that is the vendor adapter (`GmailClient`) * or a test double (`InMemoryFirestore`, `MockTts`), which IS the seam rather than a caller of * it. Counting those would draw an edge from every service embedding a fake to a vendor it never * actually reaches. */ private recordExternalUses; private recordCall; /** Resolve an expression to the API contract it names, or null if it is not one. */ private apiInfoFromExpr; /** * The checker landed on a BUILT declaration instead of source — the consumer has no * tsconfig.base `paths` entry for the api-lib, so the import went through node_modules to * `dist/**.d.ts`. tsc erases decorators when emitting declarations, so `@ApiPath` is simply * not there and never will be. Recover the contract by name from the source index; the graph * is then correct no matter how the consumer's tsconfig is laid out. */ private recoverFromDeclaration; /** `path/to/file.ts:LINE` for `node`, workspace-relative, for a human-readable report. */ private relativeLocation; private relativePath; /** * {api, owner, type, methods} when `cls` is an `abstract class` carrying `@ApiPath` IN SOURCE, * else null. Only the OWNER differs from the index pre-pass — here it comes from the file's * location rather than from the project being walked — so the contract test itself is delegated * to apiClassInfoFrom, keeping one definition of "this is a contract". */ private apiClassInfoFor; } /** * Run the scan and attach the derived `apiRelations` onto each graph entry in * place. Shared by `architecture:generate` (which then saves) and * `architecture:validate-architecture-unchanged` (which regenerates in memory * and must attach the SAME field, or it would see a phantom diff). Returns the * full scan so callers (validators, runtime graph) can reuse the api index. */ export declare function scanAndAttachApiRelations(workspaceRoot: string, graph: EnhancedGraph, projectInfos: Map, externalApiPaths?: readonly string[]): ApiScanResult; /** * The committed `apiContracts` table for architecture/dependencies.json, from a completed scan. * * Only contracts with ≥1 endpoint are emitted: a vendor seam has no routes, so a table entry for it * would be an empty shell, and its identity is already carried by the `external` refs in * apiRelations. Sorted by api name, methods left in declaration order, so the file is deterministic. * * THROWS on the four ways an entry can be wrong-but-green, checked root cause first (the fourth is * an `external` method that never said WHO calls it — UndeclaredExternalCallerError): * 1. an `@Endpoint` path the scan could not read (UnresolvedEndpointPathError) — the other half of * the URL a consumer computes, and the cause of most emptied contracts; * 2. a class that declared endpoints and kept none (EmptiedApiContractError), which would otherwise * leave silently through the zero-method skip above; * 3. a routed contract with no basePath (MissingBasePathError). * All three are worse than an absent entry: a consumer joining `basePath + path` computes a * confidently wrong URL with no signal that anything is off, because every other entry is complete. * Each error aggregates EVERY offender, so a developer fixing five constants sees five in one run. */ export declare function buildApiContracts(scan: ApiScanResult): ApiContracts; /** * Loud, actionable report for decorator arguments the scan could not reduce to a string. * * Same-module constants resolve, so anything reaching here is genuinely out of reach of a * parser-only pass — and every one of them silently shrinks the graph. Empty string when there is * nothing to say, so callers can test it without special-casing. */ export declare function describeNonLiteralDecoratorArgs(args: readonly NonLiteralDecoratorArg[]): string; /** * Every contract method whose declared @Endpoint kind its api kind cannot deliver — an rpc method on * a @PubSub contract (nothing calls a queue synchronously), or a cloudtasks/cron method on an @Rpc * contract (naming a queue or schedule nothing could deliver to). Mirrors core-util's * ENDPOINT_KINDS_BY_API_KIND at BUILD time, where it can name the file instead of throwing at wiring. */ export declare function describeMismatchedEndpointKinds(contracts: ApiContracts): string[]; /** * Loud, actionable report for contracts the scan could not map to source. Callers print this * instead of emitting a green graph that is quietly missing relations. Not fatal: a contract * from a genuinely EXTERNAL (published, non-workspace) api-lib legitimately has no source here. */ export declare function describeUnresolvedApiCalls(calls: UnresolvedApiCall[]): string;