import { Effect, MessageBusSemantics, DeployableUnit, CodeUnitKind, RequestSpellingMetadata, BehavioralSummary, Predicate, TypeShape, RenderNode, BoundaryBinding, WrapperReference, GraphqlDeclaredContract, ConfidenceInfo, Transition, Gap, Input, Output } from '@suss/behavioral-ir'; import { z } from 'zod'; /** * What a unit reads out of what it was given. * * A summary already describes this, but only as a chain of `derived` nodes * nested one inside the next, which anyone who wants the list has to walk * themselves. The `path` on an input reference is empty, so querying the * field that looks like the answer gives nothing back. * * This module flattens that into the list directly: everything a unit reaches * for through its inputs, once each, in the order somebody would say them out * loud. It goes on the summary as `inputReads`. */ /** One thing a unit read, and the way it reached it. */ interface InputRead { /** The input it came through, by the name the inputs table uses. */ input: string; /** The properties walked to reach it, outermost first. */ path: string[]; } /** * Reading a function the project wrote in front of a library, before * anything is extracted. * * A service writes `registerCrud(app, "users", handlers)` and puts the * library call one hop away, inside a helper of its own, where the path * is a parameter. The call site has the literals and no library; the * helper body has the library and no literals. * * A pack asks here for the helper to be read once, over the whole * project, before any file is walked. What comes back says what the * body does in terms of the helper's own parameters, and the pack turns * that into the patterns and recognizers it used to take as config. */ /** * One value inside a helper's body, said in the helper's own terms. * * Every variant is something a call site can fill in. Anything else is * `unread`, which a pack drops rather than guesses at. */ type HelperValue = /** * A string, with `{N}` standing where parameter N was interpolated. * A literal with no interpolation is the literal itself. */ { as: "text"; text: string; } /** Parameter N, or one named property of it. */ | { as: "parameter"; position: number; property?: string; } /** An object literal, each property read the same way. */ | { as: "object"; properties: Record; } /** A call, so a pack can see through `JSON.stringify(request)`. */ | { as: "call"; callee: string; arguments: HelperValue[]; } /** Something none of the above covers. */ | { as: "unread"; }; /** One call a helper's body makes, read in the helper's own terms. */ interface HelperSink { /** The property the call was made through, or null for a bare call. */ method: string | null; /** What the call was made on. */ receiver: HelperValue; arguments: HelperValue[]; } /** A function the project declares, as the index read it. */ interface ProjectHelper { /** What the declaration calls it, which is what a call site writes. */ name: string; /** Absolute path of the file declaring it. */ file: string; /** Its parameters, in order, by name. */ parameters: string[]; /** * Parameters some caller handed this pack's own value to. Empty when * the helper was found by its body rather than by a call site. */ subjectParameters: number[]; /** Every call its body makes that the search asked about. */ sinks: HelperSink[]; } /** * How the index picks out which of the project's functions to read. * * `subject` starts at a call site: a function the project hands one of * this pack's own values to is a helper of this pack's, whatever the * helper's file imports. `text` starts at the body, for a pack whose * library is reached over the wire and has no import to look for. Both * come from the library itself, which is the bar `requiresImport` meets. */ type HelperSearch = { by: "subject"; } | { by: "text"; contains: string[]; }; /** What a pack contributes to a run once its helpers have been read. */ interface HelperDeclarations { discovery?: DiscoveryPattern[]; invocationRecognizers?: InvocationRecognizer[]; } /** A pack's standing request to have the project's helpers read. */ interface ProjectHelpers { find: HelperSearch; /** * Turned into patterns and recognizers before the first file is * walked. It is handed data, never an AST, so a pack that declares * one still runs on any adapter that implements the reading. */ declare(helpers: readonly ProjectHelper[]): HelperDeclarations; } /** * The `PatternPack` interface, which is what a framework pack gives a language * adapter. The pack says WHAT to look for (which import, which call, which * decorator); the adapter knows HOW to find that in its language's AST. * * Everything here is data, never code. A pack describes a library once and any * adapter that understands these patterns can apply it. If you want a pack to * compute something, the answer is usually another declarative field here. * * The sections run in the order an adapter uses them: discovery finds candidate * code units, terminals describe how a unit finishes, contract reading and * input mapping describe what it declares and takes in, and response property * semantics say which property is the body and which is the status. */ type DiscoveryMatch = { type: "namedExport"; names: string[]; } | { type: "registrationCall"; importModule: string; importName: string; registrationChain: string[]; } | { type: "fileConvention"; filePattern: string; exportNames: string[]; } | { type: "clientCall"; /** Module the client is imported from, or "global" for built-ins like fetch */ importModule: string; /** Named export or identifier, for example "initClient" or "fetch" */ importName: string; /** If set, only match calls to these methods on the client (e.g. ["getUser"]). * Unset means any method call (or bare call for globals). */ methodFilter?: string[]; /** * Method names on the import that produce a client-equivalent instance, * so variables initialized from those calls also act as discovery * subjects. axios uses `axios.create({...})` to build a baseURL-bound * instance; declaring `factoryMethods: ["create"]` lets the adapter * treat `api.get(...)` (where `api = axios.create(...)`) the same as * `axios.get(...)`. */ factoryMethods?: string[]; /** * The property of the factory call's config object that every * request through the instance is sent under, `baseURL` for axios. * Both sides of a boundary have to read one route the same way, * and a spec's `servers[0].url` already goes in front of the * provider's paths, so a base written here goes in front of the * consumer's. */ basePathOption?: string; /** * The import and every instance built from it can be called as a * function, so `axios(config)` and `api(config)` are requests the * same as a call through a method in `methodFilter`. */ callable?: boolean; } | { /** * A constructor or factory call that takes a configuration object * containing a resolver map, which is how code-first GraphQL servers * are usually written. The map is two levels deep: outer keys are * GraphQL type names (`Query`, `Mutation`, `Subscription`, or * object-type names like `User`), and inner keys are field names * whose values are resolver functions. * * Example (Apollo Server v4): * ```ts * new ApolloServer({ * typeDefs, * resolvers: { * Query: { users: async () => {...} }, * Mutation: { createUser: async (_, {input}) => {...} }, * User: { fullName: (parent) => `${parent.first} ${parent.last}` }, * }, * }); * ``` * * Each inner function becomes one discovered unit whose binding * semantics is `graphql-resolver(typeName, fieldName)`. Both * `new Ctor(cfg)` and `ctor(cfg)` match, because Apollo's standalone * server uses `new` and yoga uses a bare call. */ type: "resolverMap"; importModule: string; importName: string; /** * The property on the config object that contains the resolver map. * This is the library's own config key, so the pack has to give it and * the adapter ships no default. Apollo, yoga, and graphql-tools all * spell it `"resolvers"`. */ mapProperty: string; /** * GraphQL types whose fields we DON'T treat as resolvers. This is the * opt-out for meta-types like `Subscription` that we may want to handle * differently later. Leave it unset to discover every type. */ excludeTypes?: string[]; } | { /** * A consumer-side GraphQL hook call, the way Apollo Client and urql * are normally used. Each call to one of the listed hooks becomes * a `client`-kind code unit whose binding semantics is * `graphql-operation(operationType, operationName?)`. * * The document argument can be written several ways: an inline * `gql`-tagged template, a const binding (in this module or imported * from another one), a `.graphql` or `.gql` file import, or a * generated `TypedDocumentNode` object literal from graphql-codegen * client-preset. When the document body cannot be read statically, * the operation header falls back to the `TypedDocumentNode` type * arguments. A document that still cannot be resolved shows up on the * summary as `metadata.graphql.unresolvedDocument`, so the boundary is * kept rather than dropped. * * Example: * ```ts * import { gql, useQuery } from "@apollo/client"; * const GET_USER = gql`query GetUser($id: ID!) { user(id: $id) { id } }`; * function UserPage({ id }) { * const { data } = useQuery(GET_USER, { variables: { id } }); * ... * } * ``` * * The adapter records the operation name and type on the * DiscoveredUnit's `operationInfo`, and binding construction uses that * to emit `graphql-operation(...)`. The per-hook `operationType` * wins when the document header cannot be read, the same way * `graphqlImperativeCall.methods` does. */ type: "graphqlHookCall"; importModule: string; /** * Hooks to match on that import, each mapped to the operation type it * performs (`useQuery` to query, `useMutation` to mutation, * `useSubscription` to subscription). That mapping supplies the * operation type when the document body cannot be read statically. * Each hook is reported as `kind = "client"` unless a pack overrides * that through the enclosing `DiscoveryPattern.kind`. */ hooks: Array<{ hookName: string; operationType: "query" | "mutation" | "subscription"; }>; } | { /** * An imperative Apollo-Client-style call: `client.query({ query })`, * `client.mutate({ mutation })`, `client.subscribe({ query })`. This is * separate from hook calls because the document is on a config-object * property rather than the first positional argument. * * Discovery only fires when the named constructor (usually * `ApolloClient`) is imported, because otherwise any object at all * with a `query` method would look like a match. * * Each entry in `methods` specifies the method called on the client * (`"query"`, `"mutate"`, or `"subscribe"`) and the config-object * property that contains the gql document * (`"query"`, `"mutation"`, and `"query"` respectively). The method * name decides the operation type when the gql document's header is * anonymous. When the document has a name, its header wins. */ type: "graphqlImperativeCall"; importModule: string; importName: string; methods: Array<{ methodName: string; documentKey: string; operationType: "query" | "mutation" | "subscription"; }>; } | { /** * Treats a TypeScript package's public export surface as a * boundary. The adapter reads `package.json` at `packageJsonPath`, * resolves each reachable entry point (root `.` and any sub-path * `exports`), follows barrel re-exports, and emits one discovered * unit per exported function. Those units are the provider side of * an in-process `function-call` boundary. * * The bindings this produces have the identity * `{ transport: "in-process", * semantics: { name: "function-call", * package: , * exportPath: [...] }, * recognition: }`. * * A sub-path export is identified as, for example, * `@suss/behavioral-ir/schemas::BehavioralSummarySchema`, giving * `exportPath = ["schemas", "BehavioralSummarySchema"]`. A root export * leaves the sub-path segment out. * * As of v0 this resolves the `types`, `default`, and `import` * conditions on `exports`, and falls back to `types`, `main`, `module` * when there is no `exports` field. Pattern exports (`./utils/*`) and * `development` conditions are not handled yet. */ type: "packageExports"; /** * Absolute path to the package's `package.json`. Left out when * `workspaces` is set, since the workspace decides the list. */ packageJsonPath?: string; /** * One pattern for every package the workspace declares. The pack * cannot list the packages, because they belong to the project * rather than to any library, so the adapter reads the workspace * manifest and applies this pattern once per package it finds. */ workspaces?: true; /** * Restrict to these `exports` keys (without the leading `./`). The * root export is keyed `"."`. Leave it unset for every sub-path that * resolves. */ subPaths?: string[]; /** * Export names to skip, usually `["default"]` when a pack wants to * treat default exports separately or ignore them. */ excludeNames?: string[]; } | { /** * Class methods with a particular decorator, on classes with a * particular class-level decorator. NestJS-style frameworks work this * way: resolvers, handlers, and controllers are declared by decorator * rather than by registering a function in an object literal. * * Discovery only fires when `classDecorator` and each * `methodDecorators` entry come from `importModule`, so a user-defined * decorator that happens to share a name will not match. * * For a NestJS GraphQL pack: * `{ importModule: "@nestjs/graphql", * classDecorator: "Resolver", * methodDecorators: ["Query", "Mutation", "ResolveField", * "Subscription"] }` * * The adapter fills in `DiscoveredUnit.resolverInfo` so the binding * comes out as `graphql-resolver(typeName, fieldName)`. * `typeName` resolves from `methodDecoratorTypeMap` when the * method decorator is in it, and otherwise from the class * decorator's first argument (`@Resolver(() => User)` gives * `"User"`). `fieldName` comes from the method decorator's `{ name }` * option when that is set, and otherwise from the method name. */ type: "decoratedMethod"; /** * The module a decorator has to be imported from before discovery will * fire. Codebases sometimes re-export a framework decorator wrapped * with extra metadata of their own, and checking one module would miss * those. Pass an array of acceptable modules and any one of them * matching is enough. */ importModule: string | string[]; /** * Class decorators to recognise. The first one that appears on a class * is the one typeName is read from; the rest are fallbacks for * codebases with several wrapper styles. A pack ships only what its own * framework declares here, and takes a project's own wrappers through * its options instead. */ classDecorators: string[]; methodDecorators: string[]; /** * Maps a method decorator to the type its field belongs to, for the * decorators that settle it. NestJS puts `@Query` on the root * `Query` type and `@Mutation` on `Mutation` no matter what the class * says, so an entry here wins over the class decorator's argument. * * When the map leaves a decorator out and the class decorator gives no * type either, nothing here works out which type owns the field. The * binding then goes out with no type and pairs with nothing, instead * of claiming a field the schema does not have. */ methodDecoratorTypeMap: Record; } | { /** * NestJS-style REST controller discovery: a class decorated with * `@Controller(pathPrefix?)`, and methods decorated with * `@Get(subpath?)`, `@Post`, `@Put`, `@Delete`, and so on. The * decorator's NAME is what determines the HTTP method, through * `methodDecoratorRouteMap`. The route path is the class decorator's * first argument joined with a slash to the method decorator's first * argument, and both of those are optional. * * Wrapper decorators are tolerated the same way as in * `decoratedMethod`: at least one method-route decorator has to come * from the framework module, but class decorators * are matched by name alone, so a project's own wrapper around the * framework's decorator matches once the project lists it in the * pack's options. * * The adapter fills in `DiscoveredUnit.routeInfo` so the binding comes * out as `rest(method, path)`. */ type: "decoratedRoute"; importModule: string | string[]; classDecorators: string[]; /** * Maps a decorator to an HTTP method. NestJS uses one decorator per * verb (`@Get`, `@Post`, `@Put`, `@Delete`, `@Patch`, `@Options`, * `@Head`, `@All`), and other frameworks may do the same. The values * become the `method` field on the REST binding, and `"*"` is fine for * a catch-all decorator. */ methodDecoratorRouteMap: Record; } | { /** * Loop expansion: a `for-of` loop over a literal array of * route specs is treated as if each element were an inline * registration. Used for patterns like: * * const routes = [ * { method: "get", path: "/users", handler: getUsers }, * ... * ]; * for (const r of routes) app[r.method](r.path, r.handler); * * `elementShape` declares which keys on each element give the method, * the path, and the handler. The loop body has to contain at least * one call expression that references the * loop variable, which filters out unrelated loops. Nothing else * about that call is checked. * * An iterable that resolves to an `ArrayLiteralExpression`, inline or * bound to a `const` one hop away, gets expanded. Cross-file and * computed iterables are outside v0. * * Pack-author docs: `design/proposals/dynamic-registration.md`. */ type: "registrationLoop"; elementShape: { methodKey: string; pathKey: string; handlerKey: string; }; /** * The routable the loop registers on. When set, the loop's body * must call a method on a variable constructed from one of these * imports, the same resolution a registration call's subject * gets. Without it, any loop over objects with the three keys * above matches, and a file that imports the library can contain * an unrelated one. */ receiver?: { importModule: string; importNames: string[]; }; } | { /** * Helper-call expansion: one function call at the user's site is * treated as if it were N inline registrations, with the call's * arguments substituted into a template per registration. Used * for calls like `registerCrud(app, 'users', userHandlers)` that * `registrationCall` discovery cannot see today. * * Each entry in `registrations` describes one virtual route * the helper produces. `pathTemplate` and `handlerArg` use * `{N}` placeholders that resolve to the call's positional * arguments. `{N}` substitutes the argument's literal value * (for string-literal args) or its source text (for * non-literal args, with the slot marked opaque). `{N}.prop` * reads `prop` from the argument's resolved object. * * `importModule` optionally narrows matches to helpers imported from * one specific module, which helps when two packages happen to export * a function with the same name. * * Pack-author docs: `design/proposals/dynamic-registration.md`. */ type: "registrationTemplate"; helperName: string; importModule?: string; /** * Which argument is the routable, so a route the helper writes * keys on the same app as one written beside it and the * middleware registered there covers it too. */ subject?: { argument: number; importModule: string; importNames: string[]; }; registrations: Array<{ method: string; pathTemplate: string; handlerArg: string; }>; } | { /** * Routes declared as JSX elements, the way client-side routers * write them: an element imported from the router library whose * attributes give a URL path pattern and the element it * renders. Covers the tree form (route elements nested inside * one another, child paths joining the parent's, index routes * taking the parent's path) and the object-array form (a * factory call whose first argument is an array of route * objects using the same property names). * * The pack says what its library exports: the route element, the * path, element, and index attributes, and any factories that * take an array of route objects. The adapter walks JSX and arrays, * and knows none of those names itself. * * Each route with a readable path becomes one unit whose target * is the component the element attribute references, resolved * only when the reference is a single identifier. A route whose * component cannot be read is still reported, as a boundary with * nothing behind it. A route whose path cannot be read gets no path * and reports that in a gap instead of guessing at one. */ type: "jsxElementRoute"; /** * Module(s) the route element and factories must be imported * from. Exact module specifiers, matched against the file's * import declarations; aliased imports are followed. */ importModule: string | string[]; /** The route element's exported name. */ routeElement: string; /** The attribute with the route's path pattern on it. */ pathAttribute: string; /** The attribute with the JSX the route renders on it. */ elementAttribute: string; /** * The attribute that marks an index route, which renders at its * parent's path. Leave it unset if the library has no index routes. */ indexAttribute?: string; /** * The property with the routes nested under a route object on it, * which is how the object form expresses what the JSX form expresses * by nesting. Paths compose the same way in both. Leave it unset if * the library's route objects do not nest. */ childrenAttribute?: string; /** * Factory functions whose first argument is an array of route * objects keyed by the same three attribute names. The array is read * where it is written, or through the same value resolution the rest * of discovery uses, so a `const` binding one hop away works the * same as an inline literal. */ routeObjectFactories?: string[]; /** * Factory functions that turn JSX route elements into the route * objects the library consumes. The elements themselves are read * by the JSX walk wherever they appear, so a route-object factory * handed one of these calls has nothing more to add. Listing them * here is what stops that case from being reported as an * unreadable route array. */ elementsFactories?: string[]; /** * The HTTP method recorded on each route binding this produces. A page * route serves navigations, and the pack has to say which method those * use rather than the adapter assuming one. */ method: string; } | { /** * Consumer side of the package-export boundary. Scans source files for * imports of the listed packages and records every call site, * emitting one `caller`-kind unit per enclosing * function. The bindings this produces are * `function-call { package, exportPath }`, which match the provider * summaries `packageExports` produces. * * `packages` lists exact package names to track imports of, possibly * with a sub-path such as `"@suss/behavioral-ir/schemas"`. Pass * several package names to track a family at * once. Imports of any other package are ignored. * * As of v0 this covers named and default imports. Namespace imports * (`import * as X from`) are not tracked yet. Re-imports within * the consumer repo (consumer A imports from consumer B which * re-exports from pkg) produce units against the intermediate rather * than the original, because full symbol resolution is not built yet. */ type: "packageImport"; /** Left out when `workspaces` is set. */ packages?: string[]; /** * Track imports of every package the workspace declares. A file * inside one workspace package importing another is the consumer * side of the package-export boundary, whichever two they are. */ workspaces?: true; }; type BindingExtraction = { method: { type: "fromRegistration"; position: "methodName" | number; /** * Registrations whose recorded method is something other than their * own name uppercased, the way `.all` registers every method and is * recorded as `"*"`. Anything missing from the map is recorded as * its own name uppercased. */ nameMap?: Record; } | { type: "fromExportName"; } | { type: "fromContract"; } | { type: "fromClientMethod"; } | { type: "fromArgumentProperty"; position: number; property: string; default?: string; } | { type: "literal"; value: string; }; path: { type: "fromArgument"; position: number; } | { type: "fromArgumentProperty"; position: number; property: string; } | { /** * The route path comes from where the file is on disk, which is how * Next.js and React Router describe their routes. The pack spells * out its own convention here, because the adapter knows * about files and the pack knows what the framework does with their * names. * * `app/api/orders/[id]/route.ts` under `{ root: "app", * dropBasenames: ["route"], dynamic: "brackets" }` comes out as * `/api/orders/{id}`, which pairs with an Express provider * writing `/api/orders/:id`. */ type: "fromFilename"; /** * Where a route path starts. The directories below it become the * path, and everything above it belongs to the project's own layout * and gets dropped. */ root: string; /** * Filenames that say what kind of file it is rather than adding a * path segment: `route`, `page`, `index`, `_index`. */ dropBasenames?: string[]; /** * How the framework writes a parameter in a filename. Next.js * uses `[id]`, React Router uses `$id`. */ dynamic?: "brackets" | "dollarPrefix"; /** * Whether a directory in parentheses organises files without * appearing in the URL, as `app/(marketing)/about` does. */ dropParenthesized?: boolean; /** * Whether one filename contains the whole path with dots between the * segments, the way `routes/orders.$id.tsx` does. */ flat?: boolean; } | { type: "fromContract"; } | { type: "fromClientMethod"; }; }; /** * Where a declared channel's spelling comes from. * * `decoratorArgument` reads the argument off the same decorator the * match selected the handler by, so `@EventPattern("order.placed")` * gives "order.placed". `literal` is for a wire whose channel the * library fixes. `unstated` says the wire is known and the channel is * not, which pairs the way a null channel always has. */ type ChannelSource = { from: "decoratorArgument"; position: number; } | { from: "literal"; value: string; } | { from: "unstated"; }; /** * A binding the pattern states outright, for a boundary the match * cannot read from the source. * * `bindingExtraction` speaks REST and nothing else, so a declarative * pack whose boundary is a queue or a topic had nowhere to say so and * was pushed into a callback, which pack health then reports as an * ast-link. This is the same vocabulary `DiscoveredCustomUnit` already * has, declared instead of returned. Message bus only for now; the * design note in the proposals directory says what comes next and why * the rest stays put. */ type DeclaredBinding = { semantics: "message-bus"; messageBus: MessageBusSemantics["messageBus"]; channel: ChannelSource; }; /** * How a framework takes a function that runs around a handler rather * than as one. Middleware and an error handler are registered through * a method on the routable, `app.use(fn)` or `app.onError(fn)`. A * validation hook is handed to the routable's constructor instead, * `new OpenAPIHono({ defaultHook })`, and runs for every route on it. */ type WrapperRegistration = WrapperMethodRegistration | WrapperOptionRegistration; /** What every wrapper registration says about the function it registers. */ interface WrapperFunctionShape { /** * Parameter position of the continuation inside the wrapper, the one * it calls to hand control on. Absent when the wrapper never * continues, which is how an error handler that always responds is * written. */ continuationParam?: number; /** * Parameter position that receives what the wrapped unit threw. A * wrapper that declares one runs only when the wrapped unit's path * ended by throwing, which is a fact about how the framework invokes * it rather than anything its body says. Absent means the wrapper * runs on the ordinary path. */ throwParam?: number; /** * Parameter position that receives a value the framework worked out * before calling the wrapper, the outcome of validating the request * for a hook. The pack's terminals read one parameter further along * from there, as they do past `throwParam`, and nothing else changes: * the wrapper still runs on the ordinary path. */ resultParam?: number; } /** A wrapper registered through a method on the routable. */ interface WrapperMethodRegistration extends WrapperFunctionShape { /** Method name that registers a wrapper, e.g. "use" or "onError". */ method: string; /** Argument position of the wrapper function. */ targetPosition: number; /** * Argument position of a path pattern narrowing which routes the * wrapper runs for, when the method takes one. Absent means the * wrapper runs for every route on the subject. */ scopePosition?: number; /** * Only match a registered function declared with exactly this many * parameters. Express tells its error handlers apart from its * middleware by arity alone, both being `app.use(fn)`. */ arity?: number; } /** A wrapper handed to the routable's constructor as an option. */ interface WrapperOptionRegistration extends WrapperFunctionShape { /** The option the wrapper is under, e.g. "defaultHook". */ constructorOption: string; /** Argument position of the options object in the constructor call. */ targetPosition: number; } interface DiscoveryPattern { /** The kind of code unit this discovers: "handler", "loader", "action", "component", etc. */ kind: string; match: DiscoveryMatch; bindingExtraction?: BindingExtraction; /** A binding the pattern states outright. See `DeclaredBinding`. */ binding?: DeclaredBinding; /** * How the routable this pattern discovers (Express's `Router()`, * Hono's `new Hono()`, and similar) can itself be mounted onto * another one under a path prefix, as in Express's * `app.use(prefix, router)` or Hono's `app.route(prefix, sub)`. * This only means anything when `match.type` is `"registrationCall"`, * because mount discovery reuses that match's `importModule` and * `importName` to work out which variables in a file are the routable * that a mount call is being made on. * * When set, the adapter composes the mount's prefix into the path * of every route discovered on the mounted value, whether it is * declared in the mounting file or, by following the mounted value * through an import, in whichever file declares it. A mount whose * prefix is not a string literal, or whose target the resolution store * cannot follow to a concrete value, contributes nothing, and the routes * under it keep the path they were written with. */ mount?: { /** Method name that registers a sub-router at a prefix, e.g. "use" or "route". */ method: string; /** Argument position of the prefix string. */ prefixPosition: number; /** Argument position of the mounted router/sub-app value. */ targetPosition: number; }; /** * How this framework registers a function that runs around a * handler rather than as one: middleware, an error handler, a * validation hook. Like `mount`, this only means anything when * `match.type` is `"registrationCall"`, because wrapper discovery * reuses that match's `importModule` and `importName` to work out * which variables in a file are the routable being registered on. * * When set, the registered function becomes a unit of its own and is * summarized like any other, and every unit registered on the same * routable records a reference to it. A registration whose function * the resolution store cannot follow contributes nothing. */ wraps?: WrapperRegistration; /** * This pattern only runs against files that import one of these module * specifiers, or a sub-path of one. An empty array means no gate at all * (the pattern is dispatched against * every file). Leaving it undefined does the same, but pack authors * SHOULD write it out, because `[]` is the deliberate * "match every file" choice, usually because the pattern keys on * something other than imports. The fetch runtime does that, since it * matches global `fetch(...)` calls. * * Matching is by prefix on the import module specifier. An entry of * `"@nestjs/graphql"` matches `from "@nestjs/graphql"` and * `from "@nestjs/graphql/dist/foo"` and any other sub-path. * * This pre-filter is only there for speed. The closure walk and the other * post-passes can still reach every loaded file through symbol * resolution. */ requiresImport?: string[]; } type TerminalMatch = { type: "returnShape"; requiredProperties?: string[]; } | { type: "returnStatement"; /** * Skip ReturnStatements whose returned expression is a CallExpression * (or NewExpression). For frameworks where `return reply.send(...)` * also lands as a `parameterMethodCall` match on the inner call, * this stops the same `return reply.send(...)` producing two * terminals, one from the wrapping returnStatement and one from * the inner method-call chain. Bare returns (`return user`, * `return { id }`, `return await fn()`) still match. */ excludeCallReturns?: boolean; } | { type: "parameterMethodCall"; parameterPosition: number; methodChain: string[]; /** * Methods that return the response itself and change nothing the * terminal reads, so they may appear anywhere in the chain. * `res.set(h).status(201).json(b)` matches `["status", "json"]` * when `set` is listed here. */ passThroughMethods?: string[]; } | { type: "throwExpression"; constructorPattern?: string; } | { type: "functionCall"; functionName: string; /** * Only match when the name was imported from one of these modules. * The field works exactly like a DiscoveryPattern's gate, matching by * prefix: "react-router" also matches "react-router/server". * * Set it whenever the function belongs to a library, because matching * on a bare name picks up every function with that name in the * user's project too. `json` is a common name for a project's own * response helper, and reading a library's argument order into one * of those gives you a confident wrong answer. * * Leave it unset only when the function belongs to no library at all. * A pack should generally not target a project's own helper. Declare * the envelope structure instead, with a `returnShape` terminal, and * the adapter follows a returned call into the project and reads * the helper's parameters. That covers a helper whatever it is called * and whatever order its arguments come in. */ requiresImport?: string[]; } | { /** * A return statement whose value is a JSX element or fragment. The * root element or component name is recorded in * `RawTerminal.component`. React, and any other JSX-based framework * pack, uses this to classify component output as a `render` terminal. */ type: "jsxReturn"; } | { /** * A synthetic terminal for the implicit fall-through at the end of a * function body. It fires when the function's last statement is * neither a `ReturnStatement` nor a `ThrowStatement`, which * covers the common case of handler and effect bodies that run * side effects and return `undefined` implicitly. Without this, * handler summaries come out with `transitions: []` because * `findTerminals` has nothing to match. A pack that always expects * explicit returns (HTTP handlers) should leave this out of * its terminals. A pack for callback bodies (React handlers, * `useEffect` bodies, Node `.on(...)` callbacks) should include it. */ type: "functionFallthrough"; } | { /** * A call to the parameter at this position, `next()` inside a * middleware. Nothing declares this in a pack: the adapter builds * it from `DiscoveryPattern.wraps.continuationParam`, so a * wrapper's path that hands control on ends in a `delegate` * output and a path that responds first does not. That is how * composition tells the two apart. */ type: "parameterCall"; parameterPosition: number; }; interface TerminalExtraction { statusCode?: { from: "property"; name: string; } | { from: "argument"; position: number; minArgs?: number; } | { from: "constructor"; codes: Record; } | { from: "argumentProperty"; position: number; name: string; } | { from: "argumentConstructor"; position: number; codes: Record; }; body?: { from: "property"; name: string; unwrapJsonStringify?: boolean; } | { from: "argument"; position: number; minArgs?: number; }; /** Fallback status code when none is extracted. e.g. Express res.json() defaults to 200. */ defaultStatusCode?: number; } interface TerminalPattern { /** What kind of output this terminal produces: "response", "throw", "return", "render", "delegate" */ kind: "response" | "throw" | "return" | "render" | "delegate"; match: TerminalMatch; extraction: TerminalExtraction; /** * On a throw terminal: the framework turns the thrown status into * the wire response, so a resolved status makes the output a * response. HTTP packs state this; a pack reading a non-HTTP code * space off throws leaves it off and the throw stays a throw (#149). */ producesResponse?: boolean; } interface ContractPattern { /** How to find the contract object. A contract is a data structure rather * than a code unit, so this needs less than a DiscoveryPattern does. */ discovery: { importModule: string; importName: string; registrationChain: string[]; }; responseExtraction: { /** The property on the contract object with the responses map on it */ property: string; }; /** * The properties an endpoint states its HTTP method and path under. * Both ts-rest and zod-openapi happen to spell them `method` and * `path`, but they are the library's words, so the pack says them * and the adapter reads whatever it is told. */ methodProperty: string; pathProperty: string; paramsExtraction?: { property: string; }; /** * Where the reader finds one endpoint's contract object. Left out, * the ts-rest shape applies: one contract object contains every * endpoint keyed by handler name, and the reader walks up from the * handler to the enclosing router call. With `registrationArgument`, * the zod-openapi shape applies instead: `app.openapi(route, handler)` * passes the endpoint's own contract as the handler's sibling * argument. */ endpoint?: { from: "registrationArgument"; position: number; }; } type InputMappingPattern = { /** Positional parameters, e.g. Express (req, res, next) */ type: "positionalParams"; params: Array<{ position: number; role: string; }>; } | { /** * One object parameter whose properties are the inputs, the way * ts-rest passes `{ params, body, query }` and React Router passes * `{ params, request }`. A handler that destructures it gets one * input per name it binds; a handler that takes it whole gets a * single input, since the source does not say which properties it * reads. */ type: "objectParam"; /** Defaults to the first parameter. */ paramPosition?: number; /** Property name mapped to the role it takes, e.g. `{ params: "pathParams" }`. A name not here keeps the name it was bound under. */ knownProperties: Record; /** The role for a parameter taken whole. Defaults to "request". */ wholeParamRole?: string; } | { /** * Component props, React / Vue / Svelte-style: one parameter that * the caller destructures at will, with prop names only visible at * the call site. When the parameter is destructured, each bound * name becomes its own Input with the name as its role. When it is * not destructured (`function X(props) {...}`), a single Input comes * out with `wholeParamRole`, which defaults to `"props"`. * * This differs from `objectParam` in two ways. The pack declares no * prop names up front, since they are whatever the component author * wrote, and each input records the type text of the prop, which a * component's shape comparison reads. */ type: "componentProps"; paramPosition: number; /** Role for the single Input when the param is not destructured. Defaults to "props". */ wholeParamRole?: string; } | { /** * Emit one `Input` per declared parameter, in source order, using the * parameter's name as its role, or `defaultRole` when set. Used by the * reachable-closure pass for internal library functions, where no * framework declares a set of roles, so the name a caller sees IS * the role. Destructured parameters are captured the * same way `objectParam` captures them, so `(ctx, { userId })` * gives two inputs, `ctx` and `userId`. */ type: "allPositional"; defaultRole?: string; } | { /** * Decorator-driven parameter mapping, NestJS-style. For each declared * parameter, the adapter reads the parameter's first decorator and * looks its name up in `decoratorRoleMap`. * A decorator that matches gives the parameter that role. One that * matches nothing falls back to `defaultRole`, or is skipped when * `defaultRole` is unset. * * For `@nestjs/graphql` resolvers: * `{ "Args": "args", "Parent": "parent", * "Context": "context", "Info": "info" }`. * * Decorators are matched by name alone, so if several frameworks * define `@Args`, all of them map. Packs that need to * tell them apart by import module can add that later, once there * is a use case worth the cost. */ type: "decoratedParams"; decoratorRoleMap: Record; defaultRole?: string; }; /** * What a property on the API response object means. The pack declares this so * the adapter can work out a derived property at extraction time, the way * `.ok` means a status somewhere in 200 to 299. */ type ResponsePropertyMeaning = { type: "statusCode"; } | { type: "statusRange"; min: number; max: number; } | { type: "body"; } | { type: "headers"; }; /** Whether a refused request comes back as a response or as an exception. */ type FailureDelivery = "response" | "exception"; interface ResponsePropertyMapping { /** Property or method name on the response (e.g. "ok", "status", "json") */ name: string; /** How this member is accessed: property read or method call */ access: "property" | "method"; /** What the value means */ semantics: ResponsePropertyMeaning; } /** * A library wrapper that returns the function it was handed. For a factory * declared inside the project, the adapter works this out on its own by * reading the body. A library's body is not there to read, so the pack has to * say so. */ interface TransparentWrapper { /** Callee text as written, e.g. "Sentry.wrapHandler". */ callee: string; /** Which argument the wrapped function is passed as. */ argument: number; /** * The module the callee has to have been imported from. Without it a * local object spelled the same way would be taken for the library. */ module: string; } interface PatternPack { name: string; /** * Pack version stamp, which feeds the cache invalidation key. Bump on * any change that affects discovered units / extracted summaries. * Format is opaque to the adapter, so semver or a content hash both * work. * * Optional, because whoever loads the pack knows more about it than * the pack does. The CLI folds a hash of the file it loaded and of * the config it passed into this stamp, so a pack run through the CLI * invalidates on an edit whether or not it declares a version. A host * that builds packs some other way takes on that responsibility itself. * A pack with nothing to stamp comes out as `"unset"`, and a warm cache * will then serve results for code that has since changed. */ version?: string; /** * Files under the project this pack reads that are not source files, * given the files the run is about to walk. Their content feeds the * same cache key the pack's own config does, so a run made after * somebody edits one reads the project again instead of handing back * the previous answer. * * The aws-lambda pack is the case this exists for: a SAM template * decides which handlers there are and what invokes them, and no * source file changes when that template does. A pack that reads only * the code it is handed leaves this out. */ discoveryInputs?: (files: readonly string[]) => string[]; languages: string[]; discovery: DiscoveryPattern[]; terminals: TerminalPattern[]; contractReading?: ContractPattern; inputMapping: InputMappingPattern; /** * For a REST pack, where in the handler each part of the request is * read, in the same words `inputMapping` gives the parameters: an * Express handler reads a header at `request.headers`, a Lambda * handler at `event.headers`. The adapter stamps this on every route * the pack recognizes, and the intent pass rewrites the route's reads * from it into the sections an author writes under `receives`. * * Leave it out on a pack whose handlers do not take a request, and * leave a section out when the framework has no path for it. */ requestSpelling?: RequestSpellingMetadata; /** * Transport (wire protocol) used in the `BoundaryBinding.transport` * of discovered units. Every pack has to say what its transport is * rather than falling back on a hardcoded HTTP default. "What transport * does this pack cover?" is a question every pack should have to * answer, and requiring the field stops a later pack (React, GraphQL, * Lambda-invoke, queues) from quietly inheriting an HTTP-shaped default * that does not fit it. * * The pack's `name` separately fills in `BoundaryBinding.recognition` * on the summaries, so `{ transport, recognition }` come from * the pack directly and the adapter derives `semantics` from the * discovery pattern's binding-extraction rules. */ protocol: string; /** * What the properties on the API response object mean, consumer side. * This tells the adapter how to turn a derived property like `.ok` or * `.json()` into a structured IR construct instead of leaving it opaque. */ responseSemantics?: ResponsePropertyMapping[]; /** * How this client hands back a response the server refused. `fetch` * returns one and the caller reads the status off it. axios and ky * reject instead, so every non-2xx reaches the caller through a * `catch` and there is no status for a guard to read. Defaults to * `"response"`. */ failureDelivery?: FailureDelivery; /** * Synthesize extra code units out of a parent unit's body, for when one * construct the user wrote implicitly spawns several units the runtime * schedules. Used when a framework's runtime * schedules callbacks that aren't visible as top-level declarations: * React event handlers on JSX elements, React `useEffect` bodies, * Node `emitter.on("event", handler)`, class-component lifecycle * methods, and similar. * * `ctx` is typed `unknown` here because the extractor has no * knowledge of which adapter is driving it; each language adapter * defines its own context type (`TsSubUnitContext` in * `@suss/adapter-typescript`, say) with the primitives a pack needs to * walk the parent's AST. Packs import and cast to the adapter * context they were written against, and that cast is how a pack says * out loud that it requires the TypeScript adapter. * * Returned units are fed through the adapter's extraction pipeline * the same way top-level discovered units are, so each becomes its * own `BehavioralSummary`. Put per-unit `terminals` and `inputMapping` * on the `DiscoveredUnit` when a sub-unit is written differently from * the parent pack's defaults. */ subUnits?: (parent: DiscoveredSubUnitParent, ctx: unknown) => DiscoveredSubUnit[]; /** * A top-level discovery callback the pack supplies. It is to discovery * what `subUnits` is to sub-units: when a framework's convention does * not fit one of the data-driven `DiscoveryMatch` variants (REST * registration, decorator-based controllers, named-export shapes, * etc.), the pack ships its own walker here. The adapter calls it * once per source file alongside the data-driven dispatch. * * Use this for framework-specific patterns that do not generalize: * React's component-export heuristic (PascalCase plus a JSX return), * Vue's `.vue` SFC slots, Solid's component conventions, Storybook's * `.stories.tsx` file convention. Those are all legitimate conventions, * but baking each one into the central `DiscoveryMatch` union forces * every unrelated pack to know about them. Callbacks leave the central * union for the generic primitives and let each pack own its own * conventions. * * `ctx` is typed `unknown` for the same reason as in `subUnits`: each * adapter ships its own context primitive (`TsDiscoveryContext` in * `@suss/adapter-typescript`) and a pack casts to whichever one it was * written against. That cast is how the pack says it requires the TS * adapter. * * The units you return go through the adapter's normal pipeline. They * get their terminals and effects extracted, sub-units synthesized, and * summaries assembled exactly as units from data-driven discovery * do. Per-unit `terminals` and `inputMapping` overrides on * `DiscoveredUnit` work the same way too. * * **Cross-pack dedup.** When this callback discovers a unit at the * same `(func, kind)` as a unit from another pack's data-driven * discovery, the adapter's cross-pack claim dedup keeps whichever * claimed it first. The order packs appear in the framework list is * what decides precedence. */ discoverUnits?: (sourceFile: unknown, ctx: unknown) => DiscoveredCustomUnit[]; /** * Per-call-site recognizers that emit typed `Effect`s alongside the * generic `invocation` effect the adapter already captures. * * **Scope contract.** The adapter walks every CallExpression in * the function body and dispatches to every registered recognizer * for each call. Walking skips nested function bodies (those are * their own units with their own recognizer dispatch). The walk is * INDEPENDENT of the existing invocation-effect walker, which is * deliberately narrow (it only captures * `invocation` effects from bare expression statements and container * composition, to avoid double-counting calls that already become * terminals). Recognizers do not have that problem, so they fire on * every call regardless of position, including * `const x = await fn(...)` initializers and nested call args * (which the invocation walker skips). This independence means * recognizer authors can rely on seeing every call in scope. * * **Cross-pack visibility.** Recognizers fire regardless of which pack * discovered the enclosing function, so * `@suss/framework-prisma`'s recognizer can fire on Prisma calls * inside an `@suss/framework-express` handler. Pack authors don't * need to coordinate. * * **Emission contract.** Returning effects ADDS them to the enclosing * default-branch transition, and the generic `invocation` effect is * kept either way (typed effects live alongside the raw * call capture, so inspect can still render the callee text and * arguments while the checker pairs on the typed form). Return `null` * or `[]` for no match. * * **Dedup is the recognizer's responsibility.** The dispatcher does * not dedupe across calls. A recognizer that wants to fire * once per identifier, to collapse reads bound to a const used N * times, has to track that state itself across invocations. * * **Exceptions are caught and logged.** A recognizer that throws gets * logged to stderr with the file path and line number, and is skipped * for that one call while the extraction carries on. A buggy * recognizer will not crash the run. * * `call` is the language adapter's call-expression handle (opaque * here; ts-morph `CallExpression` in `@suss/adapter-typescript`). * `ctx` is the adapter's recognizer context (source file, an * `extractArgs()` helper that reuses the adapter's own EffectArg * builder). A recognizer casts both to the adapter context it was * written against, which is the same way `subUnits` says a pack * requires the TypeScript adapter. */ invocationRecognizers?: InvocationRecognizer[]; /** * Optional pack-level import gate. When set, the adapter's * pre-filter only considers this pack applicable to source files * whose imports include at least one of the listed modules * (matched by prefix, so `"@aws-sdk/client-sqs"` matches that module * and any `"@aws-sdk/client-sqs/sub-path"`). * * Useful for recognizer-only packs that target a specific library: * `@suss/framework-aws-sqs` declares `["@aws-sdk/client-sqs"]`, * `@suss/framework-prisma` declares `["@prisma/client"]`. Without * a gate, a recognizer-only pack walks every file in the project. That * is correct but wasteful in a large monorepo where most files never * import the library. * * A discovery-pattern pack already has a per-pattern `requiresImport` * on `DiscoveryPattern`. This is the pack-level version of that, for a * pack whose ONLY mechanism is recognizers and which has no discovery. * * Empty or undefined means no gate, so the pack walks every file (the * default for universal recognizers like `@suss/runtime-node`'s * process-surface and env-var recognizers, since `process.*` is * available without importing anything). */ requiresImport?: string[]; /** * Files this library's code generator writes beside the module it * generates, e.g. `["schema.prisma"]`. A project can point the * generator at a directory of its own, and then every consumer * imports the module by relative path and `requiresImport` matches * nothing. A directory containing one of these files counts as the * gated package, so those consumers reach the pack the way an * ordinary import would. */ generatedModuleMarkers?: string[]; /** * Functions the project itself wrote in front of this library, read * once across the whole project before any file is walked. What the * pack makes of them joins its own patterns and recognizers for the * rest of the run. */ projectHelpers?: ProjectHelpers; /** * Environment variables the pack's library reads from inside * node_modules, where no walk ever looks. Declaring them keeps the * checker from telling a template that a variable is unused when the * library reads it on every invocation. The adapter emits one marker * summary per entry whose `module` some project file imports, and * the runtime-config pairing consults the markers before it accuses. * The module match is a specifier prefix, so one entry covers a * scoped family like `@aws-lambda-powertools/`. */ libraryEnvVars?: Array<{ /** Module-specifier prefix the library's imports start with. */ module: string; /** Env-var name prefixes the library reads, e.g. "POWERTOOLS_". */ prefixes?: string[]; /** Exact env-var names the library reads. */ names?: string[]; }>; /** * Library wrappers that return the function they wrapped. The adapter * works this out on its own for a factory inside the project by reading * its body. A library wrapper's body is not there to read, so the * pack has to say it: a call to `callee` resolves to its * `argument`-th argument. * * `callee` matches the call expression text as written, e.g. * `"Sentry.wrapHandler"`. */ transparentWrappers?: TransparentWrapper[]; /** * Objects whose properties are the process environment, written as * the dotted path the code spells, e.g. `"process.env"`. The adapter * states a fact for a read off one of these whose index is not a * literal, which is how a helper that takes the variable's name as a * parameter gets its reads reported at the calls that named them. */ environmentObjects?: string[]; /** * How this library's client object is constructed, so an operation * summary can say which endpoint its calls go to. Each entry is a * constructor or factory imported from `importModule`, with * `uriProperty` the option key whose value is the endpoint. The * adapter reads every construction in the project and stamps the * client on each operation summary when exactly one distinct client * exists; two or more distinct clients abstain, since a hook call * does not say which one it goes through. */ graphqlClients?: Array<{ importModule: string; importName: string; uriProperty: string; /** * How this constructor's cache option installs a fragment * registry, for the library whose client can supply fragment * definitions at run time. `cacheProperty` is the construction * option the cache is passed in, `cacheConstructor` the cache * class, and `registryProperty` the cache option that installs * the registry. The adapter reads every construction and records * whether a registry is configured, absent, or unreadable; a * construction it cannot read counts as unreadable, never as * absent. */ fragmentRegistry?: { cacheProperty: string; cacheConstructor: { importModule: string; importName: string; }; registryProperty: string; }; }>; /** * Which service a client talks to, keyed by the endpoint the * construction was read with: the uri literal, or the written * expression when the value is computed. The value is the provider * workspace name. This is deployment knowledge, so it comes from the * pack's own per-project config rather than from the library. */ graphqlClientBindings?: Record; /** * Which service the operations in a set of files talk to, for a * project whose one frontend uses two clients. A hook call does not * say which client it goes through, so when the sole-client rule * cannot decide, these globs do: an operation whose file matches * gets the entry's workspace. First matching entry wins. */ graphqlOperationScopes?: Array<{ files: string[]; workspace: string; }>; /** * Per-property-access recognizers, the counterpart to * `invocationRecognizers`. Use these for patterns that read a value * through property access without invoking it: `process.env.X` * env-var reads, `Date.now()`-style time reads (which is actually a * call, see invocationRecognizers), bare `module.constant` reads. * * A recognizer here is handed a property access, a call, or a tagged * template, and guards its own shapes. The tagged template is there * for a library that takes its whole argument as one, the way * `prisma.$queryRaw` and `gql` do. * * The scope rules are the same as for invocationRecognizers: it fires * on every such node in the function body and skips nested function * bodies. The emission contract is the same, so effects land on the * enclosing default-branch transition. * * The arguments are opaque here and narrowed by the adapter, for the * same reason as in invocationRecognizers. */ accessRecognizers?: AccessRecognizer[]; /** * What the pack wrote as data rather than as code, for the health * report. Absent for a pack written as a hand-rolled walk, which is * itself the thing the report says. */ declarations?: PackDeclarations; } /** * The price a pack paid for what it matches, so the migration onto the * declared surface can be measured rather than asserted. * * Expressiveness is bought link by link: a link answered with data is * inspectable, serializable and runs on any adapter, while a link * answered with a function is code that only its own language runs. * Both are allowed, and the report says which is which. */ interface PackDeclarations { declarations: DeclaredMatch[]; } /** One thing a pack declared it matches. */ interface DeclaredMatch { /** What it matches, in the pack's own words. */ name: string; /** Links whose answer is data. */ dataLinks: number; /** Links answered with a function, by the question each one asks. */ functionLinks: string[]; /** * Links whose function reaches the adapter's own syntax tree, by the * question each one asks. Reaching the tree needs a separate import, * so a pack cannot arrive here without saying so. */ astLinks: string[]; /** A line of code the pack says this matches, or null when it says none. */ example: string | null; } /** * Per-call-site recognizer hook. See `PatternPack.invocationRecognizers` * for the contract and threading model. */ type InvocationRecognizer = (call: unknown, ctx: TCtx) => Effect[] | null; /** * Per-property-access recognizer hook. See * `PatternPack.accessRecognizers` for the contract and threading model. */ type AccessRecognizer = (access: unknown, ctx: TCtx) => Effect[] | null; /** * The bare minimum a `subUnits` hook needs to know about the parent code * unit it is working inside. `func` is left opaque here because each * language adapter brands its own FunctionRoot type. This interface lives in * the extractor only so `PatternPack` can refer to it, and the adapter-level * context types like `TsSubUnitContext` narrow `func` to a concrete AST * handle. */ interface DiscoveredSubUnitParent { /** Handle to the parent's function body. Opaque at extractor level. */ func: unknown; /** Discovered name of the parent (e.g. "Counter"). */ name: string; /** Kind of the parent (usually "component", "handler", etc.). */ kind: string; } /** * What a pack's `discoverUnits` hook returns for each top-level unit it * finds. It is to discovery what `DiscoveredSubUnit` is to sub-units. The * adapter widens these into its own internal `DiscoveredUnit` type, which has * adapter-specific fields like `routeInfo` and `packageExportInfo` on it, and * then runs them through the normal extraction pipeline. * * Pack authors only ever see opaque handles: `func` is whatever the adapter's * primitive returned, and the adapter narrows it to its concrete * function-root type (`FunctionRoot` in `@suss/adapter-typescript`). */ interface DiscoveredCustomUnit { /** Function body handle, opaque here. */ func: unknown; /** IR code-unit kind (e.g. "component", "handler"). */ kind: string; /** Discovered name (e.g. "UserCard"). */ name: string; /** * The unit's callable identity, when the pack states one: the module * it lives in and the name it goes by there. A server action is the * case, so intent and the keyed pairing pass can refer to it. The * adapter puts both on the unit's function-call binding. */ functionCallInfo?: { module: string; exportName: string; }; /** * Terminal patterns to extract from this unit's body. Defaults to * the pack-level `terminals` when unset. */ terminals?: TerminalPattern[]; /** * Input mapping for this unit. Defaults to the pack-level * `inputMapping` when unset. */ inputMapping?: InputMappingPattern; /** * REST route identity for units a callback discovers against an * external manifest (a SAM/CFN template's `Events` block, an infra * routing declaration, and so on) rather than an in-code registration. * When set, the adapter builds a `rest` binding from `(method, path)`, * the same binding a NestJS controller gets from decorator-derived * `routeInfo`, and the discoverUnits callback never has to reach into * the adapter's binding machinery. Either half is null when the * source does not state it, and a binding missing one pairs with * nothing. * * One function bound to several routes emits one DiscoveredCustomUnit * per route. The adapter's per-file claim dedup keys on * `(func, kind, method, path)`, so all of those variants survive. */ routeInfo?: { method: string | null; path: string | null; }; /** * GraphQL field identity for units a callback discovers against an * external manifest rather than an in-code resolver map. AppSync * routes a field to a Lambda in the deploy template, so the field is * the boundary that code serves. When set, the adapter builds a * `graphql-resolver` binding from `(typeName, fieldName)`, which * pairs with the operations a client sends. */ resolverInfo?: { typeName: string; fieldName: string; }; /** * Message-bus channel identity for consumer units a callback discovers * against a subject the code itself gives (a handler factory whose * config states the subject it expects). When set, the * adapter builds a `message-bus` binding from `(messageBus, channel)`, * which pairs with producers sending on the same channel. */ channelInfo?: { messageBus: MessageBusSemantics["messageBus"]; /** Null when the pack knows the wire but not the channel on it. */ channel: string | null; }; /** * The deployed unit this one is, for a unit nothing else routes to. * A Lambda with no event source in its template is reached by being * invoked by name, so its own platform and name are the boundary, and * the adapter builds a `unit-invocation` binding from them. Set it * where a unit would otherwise fall back to a keyless function-call. */ invocationInfo?: DeployableUnit; /** The thing that gets deployed and runs this unit, when known. */ deployableUnit?: DeployableUnit; /** * Metadata merged onto the resulting summary's `metadata` field. */ metadata?: Record; } /** * What a pack's `subUnits` hook returns per synthesized child. The * adapter pipes each of these through the same extraction + assembly * pipeline used for top-level-discovered units. */ interface DiscoveredSubUnit { /** Function body handle, opaque here. */ func: unknown; /** IR code-unit kind (e.g. "handler"). */ kind: CodeUnitKind; /** Qualified name (e.g. "Counter.button.onClick"). */ name: string; /** * Terminal patterns to extract from this sub-unit's body. When unset it * defaults to `return` and `throw`, which suits handlers and effects. */ terminals?: TerminalPattern[]; /** * Input mapping for this sub-unit. When unset it defaults to an empty * positional mapping, so an event handler with one argument should pass * `{ type: "positionalParams", params: [{ position: 0, role: "event" }] }`. */ inputMapping?: InputMappingPattern; /** * Metadata merged onto the resulting summary's `metadata` field. * Packs use this to stamp provenance (`metadata.react = { kind: "handler", ... }`). */ metadata?: Record; } /** * The language-neutral form the path engine walks: statement kind, condition * handle, children, and exit kind. Each language's adapter lowers its own AST * into this, and the path enumeration never touches a language-specific node * again. * * The `Cond` type parameter is the language's own handle for a condition * expression (a ts-morph Expression for TypeScript, a tree-sitter node for a * language added later). The engine passes it through untouched for a caller * to parse afterwards, and never looks inside it. */ /** Where a condition came from, in terms of what that branch's body does. */ type ConditionSource = "explicit" | "earlyReturn" | "earlyThrow" | "catchBlock"; /** Whether a statement's own subtree leaves the unit by returning or throwing. A throw anywhere beats a return anywhere. Never set for break or continue. */ type ExitKind = "return" | "throw" | null; /** * A test the engine passes through without looking inside: its display text * plus the language's own expression handle. The handle is null for a * synthetic condition, such as a loop's "some iteration of" marker or a * switch group's disjunction text. */ interface ConditionHandle { readonly sourceText: string; readonly expression: Cond | null; } /** One condition attached to a caller-visible transition. */ interface ConditionInfo { readonly sourceText: string; readonly polarity: "positive" | "negative"; readonly source: ConditionSource; readonly expression: Cond | null; } /** A statement list: an if arm, a loop body, a try/catch/finally block. */ type StatementBlock = readonly StructuredStatement[]; /** * One switch or match case group, already merged out of the language's own * grammar (TypeScript stacks several empty-bodied labels on top of the * clause that finally has a body, and the lowering step folds that * into one group with a joined condition). `condition` is null for the * default or wildcard group. `body` has any language-specific * statement that ends a fallthrough (a trailing break) already stripped * out, and `hasTrailingBreak` records whether one was there. */ interface CaseGroup { readonly condition: ConditionHandle | null; readonly hasTrailingBreak: boolean; readonly body: StatementBlock; } /** * What every lowered statement has on it, whichever construct it is. * * The lowering works `exitKind` out by scanning the statement's own * subtree for a return or a throw, skipping every nested function body. * * `callbacks` is the bodies of the functions this statement passes to * calls it makes, as far as the language counts those as running for the * enclosing unit. The engine walks them on the same path as the * statement, so their branches are the unit's branches. Their `return` * is not the unit's, so a path that ends inside one continues past the * statement. A lowering that leaves the field out behaves as it did * before there was one. */ interface LoweredStatementParts { readonly exitKind: ExitKind; readonly callbacks?: readonly StatementBlock[]; } /** * One statement in the unit's control flow, already lowered out of the * source language. `kind` says which construct it is, `condition` (where * one applies) is the opaque test, and the block and group fields are its * children. `LoweredStatementParts` describes the rest. */ type StructuredStatement = (LoweredStatementParts & { readonly kind: "if"; readonly condition: ConditionHandle; readonly thenBody: StatementBlock; /** null when the source has no else/elif tail at all. */ readonly elseBody: StatementBlock | null; }) | (LoweredStatementParts & { readonly kind: "switch"; /** Source order; at most one group has a null (default) condition. */ readonly groups: readonly CaseGroup[]; }) | (LoweredStatementParts & { readonly kind: "loop"; /** Display text for the loop header. The engine builds two synthetic conditions out of it: "some iteration of: ..." and "loop exited via ...: ...". */ readonly condition: ConditionHandle; readonly body: StatementBlock; }) | (LoweredStatementParts & { readonly kind: "try"; readonly tryBody: StatementBlock; readonly catchBody: StatementBlock | null; /** Here only so it can be validated. A finally that exits, or that contains a terminal the caller gave us, is not modeled, and its own conditions are never enumerated. */ readonly finallyBody: StatementBlock | null; }) | (LoweredStatementParts & { readonly kind: "exit"; readonly exit: "return" | "throw" | "break" | "continue"; }) | (LoweredStatementParts & { /** Anything else: expression statements, declarations, and any statement the enumeration does not branch on. */ readonly kind: "opaque"; }); /** * `Reading`: what a reader found, in the four states a source can leave a * value in. * * A reader that returns `T | null` makes null mean three things at once: the * source left the value out, the source states it and the reader could not * evaluate it, or several candidates matched and the reader picked none. * Each needs something different said about it on the summary, and the type * does not show which the reader meant, so every new reader decides it again * and some of them decide wrong. * * The rule that turns a reading into a claim, a library default, or a gap * lives in the summary builder in this package and is not exported. */ /** Where in a file a value is written, as byte offsets into that file. */ interface SourceRange { start: number; end: number; } /** * What a reader found where a value could be. `written` is the only state * with a value a summary may claim, and the other three each say something * different about why there is no claim to make. */ type Reading = { kind: "written"; value: T; range: SourceRange; } | { kind: "absent"; } | { kind: "unreadable"; reason: string; range: SourceRange; } | { kind: "ambiguous"; candidates: readonly T[]; reason: string; range: SourceRange; }; /** The source gives this value, here, and the reader read it. */ declare function writtenReading(value: T, range: SourceRange): Reading; /** * The source says nothing where this value could be. A library that defines * a default for it may still supply one, and the summary builder applies * that default only when a pack declares it as data. */ declare const absentReading: Reading; /** * The source gives the value and the reader could not evaluate it. The * reason is the sentence somebody reading the summary will see, so write it * about what could not be read rather than about the code. */ declare function unreadableReading(reason: string, range: SourceRange): Reading; /** * Several values could be right and the reader picked none. Keeping the * candidates leaves what was found available to whoever later teaches the * reader how to choose, and every step afterwards keeps them too. * * The range is where somebody should look to see the ambiguity. An ambiguity * often spans more than one place (two mounts of one router, in two files), * so give the site the reading was taken at, the same one an `unreadable` * reading here would have given. */ declare function ambiguousReading(candidates: readonly T[], reason: string, range: SourceRange): Reading; /** * A reading paired with what the library does when the source says nothing. * Only a pack may supply that default, so the value a summary claims for an * unstated field is library-defined and lives in the pack alongside * everything else the pack already declares. */ interface DefaultedReading { /** What the source said. */ reading: Reading; /** * The value the library applies when the source gives none, as the pack * declared it. Leave it out when the library defines no default, and then * a source that says nothing gets no claim. */ libraryDefault?: T; } /** * The same reading with its value in another form. A written value goes * through `f` and keeps its range. An ambiguous reading's candidates go * through `f` too, so the alternatives end up in the same form as the * chosen value would. Absent and unreadable readings pass through * unchanged, since neither one has a value to convert. */ declare function mapReading(reading: Reading, f: (value: T) => U): Reading; /** * Read further from what this reading found. `f` runs on a written * value with the range it was written at, so a step that turns out to * be unreadable can say so against the same syntax. * * `f` runs on each of an ambiguous reading's candidates too, and the ones * that do read come back as the candidates of an ambiguous reading with the * same reason. That way the alternatives stay next to the chosen value all * the way to the summary, instead of being dropped at the first step * that reads further. */ declare function andThenReading(reading: Reading, f: (value: T, range: SourceRange) => Reading): Reading; /** * Which of several readings a claim comes from, for a value a library lets * a source give in more than one place. */ interface ChosenReading { /** * The reading a claim comes from: the first that was written, or if none * was, the first that failed to read, or absent when all of them were * absent. */ reading: Reading; /** * The readings the choice passed over that nobody could resolve. A later * reading supplying the value does not settle what an earlier one said * and could not be read, so these still go to the * builder and their reasons still reach the summary. */ passedOver: readonly Reading[]; } /** * Choose among readings of the same value, taking the first that was * written. Nothing is thrown away: whatever the choice passed over and * could not read comes back next to it in `passedOver`, so a * `response_model` nobody could read still lands as a gap even when the * return annotation after it supplies the type. */ declare function firstWrittenReading(readings: readonly Reading[]): ChosenReading; /** * What a written reading found, for a reader that has to read further from * it before anything is claimed. A route's path template gives the * parameters that decide what each of the handler's parameters is, and * that has to be settled before there is a summary field to fill in. * * This applies no default and gives no reason, so most of what a summary * says must not be written from it. Hand the reading to the * builder instead, and the fixed rule gets applied to it once, somewhere * review can see it. * * The identity fields of a boundary binding are the exception, and the path * this reads is one of them. A binding either says where a unit is or says * nothing and pairs with nothing, and no pack declares a * default for what a boundary is called, so the value the builder would * put there is the value this gives back. Hand the reading over as well and * the reason still becomes a gap. */ declare function valueToReadFurtherFrom(reading: Reading): T | null; /** * composeWrappers.ts: what a unit does once the code registered around * it is folded in. * * A wrapper is a meta-function: it takes a unit and returns a unit. The * call to its continuation comes through as a `delegate` transition, so * a path that ends before that call is a response the caller gets * instead of the unit's own, and a path that reaches it hands the * request on without responding. The responses go beside the unit's own * outcomes and the pass-throughs go nowhere, because the unit's * outcomes already say what happens on them. The package README works * the example through and says what composition does not read. */ /** * Whether to keep gaps at all. A run that asked for none has none on * the summaries coming in, and composition adds none either. */ interface ComposeOptions { gapHandling?: "strict" | "permissive" | "silent"; } /** * Every summary, with the ones that record wrappers replaced by their * composition. A summary with no wrappers, and one whose wrappers this * run has no summaries for, comes back untouched. */ declare function composeWrappers(summaries: readonly BehavioralSummary[], options?: ComposeOptions): BehavioralSummary[]; /** * Which branch an effect belongs to. * * An adapter finds the calls a body makes once, then has to say which * of the branches it also found each call runs on. Two facts settle it, * and both are already extracted: the guards the call is written under, * and where the branch's terminal is in the file. Every adapter that * attributes effects to branches reads from here, so they all decide * it the same way. * * Neither test is enough alone, and the README beside this file says * why with the two shapes that go wrong. */ /** * Whether the branch leaves room for every guard on the effect. A guard * the branch wrote down the other way around rules the effect out. A * guard it says nothing about does not, because a branch out of a loop * or a catch says nothing about what happened inside. */ declare function guardsHoldOn(preconditions: readonly RawCondition[] | undefined, conditions: readonly RawCondition[]): boolean; /** * Whether a call on `effectLine` was written by the time the terminal * ending on `terminalEndLine` is reached. The terminal's last line and * not its first, because a call written inside the terminal's own * expression, `return new Promise((resolve) => resolve(read()))`, runs * as part of producing it. */ declare function runsBefore(effectLine: number, terminalEndLine: number): boolean; /** * moduleImports.ts: writing `metadata.moduleImports` on a summary set. * * Every adapter records the project files a summary's own file depends * on, and a checker rebuilds the module graph from that field to find * what runs inside a deployable unit. The adapters differ in where the * edges come from (TypeScript resolves import declarations, Python * resolves them through facts, Ruby has `require_relative` and constant * references), so the stamp takes a lookup and leaves the source to them. * * The paths a lookup returns must be spelled the way `location.file` is, * so both ends of the graph match. An empty list is stamped as one, which * makes the file a leaf of the graph; undefined leaves the summary alone. */ declare function stampModuleImports(summaries: BehavioralSummary[], importsOf: (file: string) => Iterable | undefined): void; /** * moduleInit.ts: the raw structure for what a module does when it loads. * * A module's top-level statements run once, when it is first imported, * and a service that reads its configuration there reads it there for * good. Walking unit bodies alone never sees that read, so the read gets * a unit of its own, one per file, named after the file, with no * boundary. Attributing it to each handler would report one read as * several. The calls those statements make go on the same unit, so the * closure can reach the functions behind them. * * The one default branch has a `void` terminal, because module * initialization returns to nobody. Every adapter builds it the same way. */ interface ModuleInitOptions { /** The unit name, which is the file's base name in every adapter. */ name: string; file: string; range: { start: number; end: number; }; effects: Effect[]; /** The calls the top-level statements make while the module loads. */ calls?: RawEffect[]; } declare function moduleInitStructure(options: ModuleInitOptions): RawCodeStructure; /** * Small helpers for pattern packs that are built the same way. * * The pack interface is deliberately declarative: a PatternPack is a data * object the adapter interprets, and most differences between frameworks are * best expressed that way. A few patterns, though, repeat word for word * across packs, and this module collects those so they are written once. */ /** * Build the `discovery` entries for an HTTP-server framework whose handlers * are registered with `app.get(path, handler)`, `router.post(...)`, and the * like. * * Each `importNames` entry produces one DiscoveryPattern, because a library * usually exposes both a default export and a named export that each produce * the routable instance (Express has `express()` and * `Router()`, Fastify has `fastify()` and `Fastify`). The binding * extraction, method from the registration and path from position 0, is * the same for every HTTP server framework we support. * * Callers still pass the `methods` list themselves, because frameworks * support different HTTP verbs. Fastify includes `.head` and `.options`; * Express historically does not by default. * * @example * discovery: httpRouteDiscovery({ * importModule: "express", * importNames: ["Router", "express"], * methods: [".get", ".post", ".put", ".delete", ".patch"], * }) */ declare function httpRouteDiscovery(opts: { importModule: string; importNames: readonly string[]; methods: readonly string[]; /** Defaults to "handler". Override for packs that want a different kind. */ kind?: string; /** * How this framework's routable can itself be mounted onto another * one under a path prefix, so a route declared on the mounted value gets * summarized with the prefix built into its path. See `DiscoveryPattern`. */ mount?: DiscoveryPattern["mount"]; }): DiscoveryPattern[]; /** * Discovery entries for the functions a framework registers around its * handlers: middleware, error handlers, validation hooks. * * A wrapper becomes a unit of its own, summarized like any other, so * these entries carry no `bindingExtraction` and no registration chain. * Their `match` is there for the import and the imported name, which is * how wrapper discovery works out which variables in a file are the * routable. One entry per (import name, wrapper shape) pair, since a * framework can register more than one kind of wrapper on a routable. */ declare function wrapperDiscovery(opts: { importModule: string; importNames: readonly string[]; wraps: ReadonlyArray>; /** Defaults to "middleware". Override for packs that want another kind. */ kind?: string; }): DiscoveryPattern[]; /** * One route a project helper registers when called, spelled with `{N}` * placeholders for the call's positional arguments. */ interface RegistrationHelper { /** What the helper is called, as the project's code writes it. */ helperName: string; /** The file declaring it, so a same-named function elsewhere is left alone. */ importModule?: string; /** Which argument is the app, so middleware on it covers these routes. */ subject?: { argument: number; importModule: string; importNames: string[]; }; registrations: Array<{ method: string; pathTemplate: string; handlerArg: string; }>; } /** * Discovery patterns for a project's own registration helpers. * * A helper like `registerCrud(app, "users", handlers)` registers routes * the call site never spells out. What each one registers comes from * the project helper index, which reads the helper's body before * extraction, so a call site expands per call and a helper called twice * gives two routes rather than none. */ declare function registrationHelperDiscovery(helpers: readonly RegistrationHelper[], kind?: string): DiscoveryPattern[]; /** * The standing request an HTTP pack makes to have the project's own * route helpers read, so what each one registers is a fact about the * code rather than something the project restates in config. */ declare function routeHelperIndex(opts: { importModule: string; importNames: readonly string[]; methods: readonly string[]; /** Defaults to "handler", to match `httpRouteDiscovery`. */ kind?: string; }): ProjectHelpers; /** * The payload behind a `JSON.stringify(...)` call, or the argument * unchanged when it is anything else. A producer serializes its message * before sending it, and the shape worth comparing across the boundary * is what went in, not the string that came out. */ declare function unwrapJsonStringify(body: EffectArg | null): EffectArg | null; /** * Option declarations more than one pack takes. * * A pack exports `optionsSchema` beside its factory, and the CLI parses * a `-f pack=config.json` file against it before the factory runs, so a * key nobody declared is refused by name instead of read as nothing. * * Four packs accept `storageSystem` and three accept `scope`. A copy * per pack is how one of them ends up allowing a value the others * reject, which is what happened when two doc comments named * `"postgres"` and the union allowed only `"postgresql"`. */ /** * Which database is behind the connection, spelled the way the * provider summaries spell it. Both sides build a pairing key from * this, so a value only one side would write stops the two pairing * with nothing said about why. */ declare const storageSystemOption: z.ZodEnum<{ postgresql: "postgresql"; mysql: "mysql"; sqlite: "sqlite"; }>; /** * Which of a project's connections an access belongs to, matched * against the scope the provider summaries carry. Packs default it to * `"default"`, so a project with one connection never sets it. */ declare const scopeOption: z.ZodString; /** * A call the project has declared to be its own dispatcher, which both * message-bus packs take under `producers`. * * `receiver` is the type rather than the variable: a service keeps its * dispatcher in a field, a closure or a constructor parameter, and the * type is the only thing stable across all three. */ declare const configuredCallOption: z.ZodObject<{ module: z.ZodString; receiver: z.ZodString; method: z.ZodString; subjectArg: z.ZodNumber; bodyArg: z.ZodOptional; }, z.core.$strict>; /** * The path engine, written against `StructuredStatement` rather than any one * language. Every control-flow path from entry to a terminal contributes its * own conjunction of conditions, so a terminal you can reach along several * paths becomes several entries instead of one branch with an invented * conjunction, or none at all. * * Nothing here touches a language-specific AST node. A language adapter * lowers its own tree into `StructuredStatement` and hands that over. * * Constructs a lowering declines to model come back as thrown * `PathBudgetExceeded` or `UnmodeledFlow`, and the caller falls back to * enclosure conditions plus an opaque conjunct instead of guessing. */ /** The cap on how many paths to enumerate. Past it the caller falls back to degraded conditions. */ declare const MAX_PATHS = 256; /** A construct the lowering step will not model safely, such as switch and case rules the enumeration does not cover. Callers catch this and degrade. */ declare class UnmodeledFlow extends Error { } /** The path budget was exceeded. Callers catch this and degrade. */ declare class PathBudgetExceeded extends Error { } interface StructuredPathConditionsInput { /** The unit's top-level statements, in source order. */ statements: StatementBlock; /** * Every StructuredStatement reachable from `statements`, mapped to the * terminals the caller gave us that sit directly at that node: its own * position (a return, throw, or opaque leaf) or, for a branching * construct, its header or test expression. A lowering builds this at the * same time as the tree, so the engine never walks a raw source AST to * find where a terminal lives. */ terminalsByStmt: ReadonlyMap, readonly Terminal[]>; } interface StructuredPathConditionsResult { /** The paths to each terminal, keyed by the caller's own terminal handle. */ byTerminal: Map[][]>; /** Condition lists for paths that fall through the end of the body. */ fallthrough: ConditionInfo[][]; } /** * Work out the conditions on each path, for every StructuredStatement * reachable from `input.statements`. Throws `PathBudgetExceeded` when the * path count crosses the cap, or `UnmodeledFlow` when a switch or match * turns up that the enumeration does not cover (a stray break, an unsafe * fallthrough). The caller catches both and degrades to enclosure conditions * plus an opaque conjunct, rather than claiming something it did not read. */ declare function enumerateStructuredPaths(input: StructuredPathConditionsInput): StructuredPathConditionsResult; /** * Enumerate, and when the engine gives up say so instead of throwing. Each * terminal comes back reachable under one condition nobody can read, which * keeps everything the caller already knew about it. A caller that can do * better, by walking the terminal's own ancestors for the conditions that * enclose it, should. */ declare function enumerateOrDegrade(input: StructuredPathConditionsInput, terminals: Iterable): StructuredPathConditionsResult & { /** Why the engine gave up, or null when it read the whole body. */ degraded: string | null; }; /** * The conditions every path to a terminal agrees on. A terminal reached more * than one way keeps only the shared ones, so a condition is never claimed * for a path without it. * * `readPredicate` turns the language's own expression handle into a * predicate. Without one every condition stays opaque, which is what a * caller that has no reader gets. */ declare function sharedGatingConditions(paths: readonly (readonly ConditionInfo[])[] | undefined, readPredicate?: (expression: Cond) => Predicate): RawCondition[]; /** Anything with a stable id, which is what these key on. */ interface Identified { readonly id: number; } /** A set of nodes, compared by id. */ declare class IdSet implements Iterable { private readonly byKey; constructor(nodes?: Iterable); add(node: T): this; has(node: T): boolean; /** The node this set was built with, which is the caller's own handle for it. */ get(node: T): T | undefined; get size(): number; [Symbol.iterator](): Iterator; } /** A map keyed by node, compared by id. */ declare class IdMap implements Iterable<[T, V]> { private readonly entries; set(node: T, value: V): this; get(node: T): V | undefined; has(node: T): boolean; get size(): number; [Symbol.iterator](): Iterator<[T, V]>; } /** * nodeWalk.ts: the one recursion the adapters use to read a declaration * wherever the language allows it to be written. * * A declaration goes where a statement goes, and every language has more * places for a statement than a reader remembers to list. So an adapter * does not list the containers it descends into. It walks every named * child and says only which nodes keep their own body, because that set * is short and a grammar's own vocabulary already names it. */ /** A parse tree, however a grammar spells one. */ interface WalkableNode { readonly namedChildren: ReadonlyArray; } /** * Returned by `into` for a node whose children the walk should leave * unread, such as one whose body belongs to what it declares. A symbol, * so it cannot collide with whatever a caller carries. */ declare const SKIP_CHILDREN: unique symbol; /** What a walk does at each node it reaches. */ interface NodeVisitor { /** Called once per node, in source order, before its own children. */ at(node: T, carried: C): void; /** What to carry into this node's children, or `SKIP_CHILDREN` to leave them unread. */ into(node: T, carried: C): C | typeof SKIP_CHILDREN; } /** * Every named node under `root`, depth first and in source order, with a * value the visitor updates as the walk descends. `root` itself is not * visited, because a caller starts from the body it is reading. */ declare function walkDescendants, C>(root: T, carried: C, visitor: NodeVisitor): void; /** * The project root when nothing declares one. * * Resolution from anywhere inside a tree finds the same `node_modules`, * and a recursive reader pointed at the directory every file shares * reaches all of them, so the deepest common directory serves both. */ /** The deepest directory containing every file, or undefined when they share no absolute root. */ declare function commonDirectoryOf(files: ReadonlyArray): string | undefined; /** * The small surface every language adapter implements, whatever that * language's own tooling looks like underneath. * * Anything TypeScript-specific, such as the ts-morph Project a caller needs * for corroborate's sandboxed execution, belongs on that adapter's own type * rather than here. This interface never gets a field that mentions one * particular language's tooling. */ interface LanguageAdapter { /** * Extract summaries from a specific list of files. Returns a * Promise so an implementation can do concurrent I/O during * discovery without bottlenecking on synchronous reads. */ extractFromFiles(filePaths: string[]): Promise; /** Extract summaries from every source file the adapter's project knows about. */ extractAll(): Promise; } /** * adapterStamp.ts: the language-neutral half of an adapter's cache key. * * An adapter's own version.ts calls `createAdapterStamp` with its own * `import.meta.url` and its hand-bumped version, and keeps the result for * the life of the process. The stamp hashes the adapter's own dist file * plus every analysis package it ships beside (this package, * `@suss/resolution`, `@suss/datalog`, `@suss/behavioral-ir`), so a * release that changes any of them invalidates an older cache, and a * dev rebuild invalidates on its own with no version bump by hand. * Running from source has no dist file to hash, and that mode declines * to cache. The extraction cache's design is in this package's README. */ /** * Whether this process can see the adapter's own code. `bundle` includes * a hash that changes whenever the code does. `source` means nothing * here could find it, so a cache key built from this stamp says nothing * about the code that will produce the results. */ type AdapterCodeStamp = { kind: "bundle"; hash: string; } | { kind: "source"; }; /** What `createAdapterStamp` hands back to an adapter's own version.ts. */ interface AdapterStamp { /** The stamp for the running adapter, computed once per process. */ codeStamp(): AdapterCodeStamp; /** * Cache-friendly identity for this adapter plus its packs. Stable * across processes given the same inputs; bumps when any pack arrives * with a new version stamp, the adapter version changes, or the * loaded adapter dist file changes (dev-mode rebuild auto-invalidation). */ packsDigest(packVersions: ReadonlyArray<{ name: string; version?: string; }>): string; /** * `cacheDir` unless this process loaded the adapter from source, where * nothing can tell one build of it from another; then this returns * null and, once per process, says why on stderr. */ declineWhenRunFromSource(cacheDir: string | null): string | null; } /** * Build the code stamp and packs digest for one adapter. `moduleUrl` has * to be the calling module's own `import.meta.url`, so the stamp finds * the dist file the adapter itself was loaded from rather than some * other package's. */ declare function createAdapterStamp(config: { moduleUrl: string; version: string; }): AdapterStamp; /** * The hash for a bundle directory: the bundle itself plus every analysis * package that can be located. Empty when the directory has no bundle in * it, which is what running from source looks like. */ declare function computeDistHashFrom(dir: string): string; /** * Content hash of the given files, in the order they arrive. Empty when * the list is empty or a file cannot be read, so a caller that could not * locate a file gets the same "no stamp" result as a run from source * rather than a hash of a shorter list. * * The caller resolves the paths, because where a specifier resolves to * depends on which package is asking. */ declare function computeContentHash(paths: readonly string[]): string; /** * Stamp for a set of project files a run reads without walking them: a * SAM template, a workspace package.json. Both the paths and the * content go in, so a file that moves counts as a change even when * every byte in it stayed the same. * * A file that cannot be read stamps as absent rather than voiding the * whole stamp, which is what `computeContentHash` does. These files * belong to the project rather than to the installed tool: one of them * being gone is a fact about the project the next run should notice, * not a reason to stop telling runs apart. */ declare function projectFileStamp(paths: readonly string[]): string; /** * The digest a run looks its cache entry up under. A pack may read * project files that are not among the ones a run walks, a SAM template * that decides which handlers exist for instance, so those belong in * the key next to the pack's own config. Which files they are depends * on the files this run walks, so the digest is settled per run rather * than once per adapter. */ declare function runDigest(packsDigest: string, packs: ReadonlyArray<{ discoveryInputs?: (files: readonly string[]) => string[]; }>, files: readonly string[]): string; /** * The on-disk extraction cache, shared by every language adapter. * * A run reuses the previous one's summaries whole when nothing * changed, and per file when some files did. The entry directory is * named after everything that has to agree before any reuse is sound: * schema version, adapter version and code hash, pack versions, the * extraction config stamp and the config file path. The manifest * inside records a stamp and content hash per file, each summary's * owning files, and per owning file the other files its walk read. * An adapter attaches whatever it needs to a cached file's record * through `meta`, which the cache stores and returns opaquely. The * full design is in this package's README. */ /** * How many keys' worth of entries a cache directory keeps. Two lets a * pair of builds alternate (a branch switch, an adapter rebuild and a * revert) without either losing its entry, and bounds a directory at * twice one manifest. */ declare const MAX_ENTRIES = 2; /** * What one walked file contributed to the run, and what its walk read. * `deps` are the other files whose content went into this file's * summaries; a change to any of them re-extracts this file. `claims` * are the units this file's walk claimed, replayed before a partial * run walks anything so precedence comes out the same. `meta` is * whatever else the adapter needs to revalidate this file on a * partial run (a route's mount prefixes, for instance); the cache * never reads it. A file marked `cacheable: false` recorded a * dependency the cache cannot pin to files, and is re-extracted on * every partial run. */ interface RootRecord { path: string; cacheable: boolean; deps: string[]; claims: { key: string; pack: string; }[]; meta: Meta; /** Packs that applied to the file, re-checked on a partial run. */ packs: string[]; } /** * Which files each summary belongs to. `owners[i]` lists the walked * files whose reuse keeps `summaries[i]` alive; an empty list marks a * run-level summary that a partial run always recomputes. */ interface CacheAttribution { roots: RootRecord[]; owners: string[][]; } /** Reported by `lookup`: what the cache decided, and why. */ interface CacheDiagnostic { kind: "hit" | "miss" | "partial"; /** * Reason the lookup missed (only set when kind === "miss"). * `key-changed` means the cache directory contains entries, but none * under this run's schema, adapter, packs and config path. * `files-changed` means the include set is not the one the entry * was written from. */ missReason?: "no-manifest" | "key-changed" | "config-changed" | "files-changed"; /** Set when kind === "partial". */ partial?: { filesChanged: number; filesRemoved: number; rootsReused: number; rootsReextracted: number; rootsDeclined: number; summariesReused: number; }; } /** * Result of a cache lookup. A hit gives back the whole summary set; a miss * says why, so a caller can render the reason. */ type CacheLookup = { kind: "hit"; summaries: BehavioralSummary[]; diagnostic: CacheDiagnostic; } | { kind: "miss"; diagnostic: CacheDiagnostic; }; /** * What a `files-changed` miss can still reuse. `validRoots` is the * cache's own verdict from hashes and recorded dependencies; the * caller may demote further (a mount prefix that no longer matches) * before calling `reuse`. `reuse` returns the summaries owned by at * least one surviving root, in stored order, with their owners, so * the caller can merge them and write the result back. */ interface PartialPlan { /** Paths whose content hash differs, plus paths new to the set. */ changed: Set; removed: Set; roots: Map>; validRoots: Set; rootsDeclined: number; reuse(valid: ReadonlySet): { summaries: BehavioralSummary[]; owners: string[][]; }; allSummaries(): BehavioralSummary[]; /** The stored attribution decoded, for a write that changes nothing. */ attribution(): CacheAttribution; } interface CacheLayer { /** The summary list on a hit, null on a miss. */ tryHit(input: CacheInput): Promise; /** * The lookup behind `tryHit`, with the reason a miss missed. Costs * stats alone: no file reads, no AST work. */ lookup(input: CacheInput): Promise; /** * After a `files-changed` miss: hash what the stats said moved and * work out which files' summaries survive. Null when the entry has * no per-file layer to reuse, or no entry matches the key at all. */ plan(input: CacheInput): Promise | null>; /** * Persist a fresh extraction's summaries to the cache, keyed * against the same file list. Subsequent `lookup` calls with the * same files return them. Without `attribution` the entry can only * ever be reused whole. */ write(input: CacheInput, summaries: BehavioralSummary[], attribution?: CacheAttribution): Promise; } interface CacheInput { /** Absolute paths of every file the run walks. */ files: ReadonlyArray; adapterPacksDigest: string; /** * One file whose stamp guards the whole entry alongside the file * list, such as a tsconfig or a project manifest. Optional because * not every adapter has one. */ configPath?: string; } /** * Construct a cache layer rooted at `cacheDir`. Pass `null` to * opt out of caching entirely, the returned layer's `tryHit` * always misses and `write` is a no-op. Useful for one-shot * extracts where caching adds latency without payoff. */ declare function createCacheLayer(cacheDir: string | null): CacheLayer; /** * The extraction funnel, shared across languages. * * "Why did this run produce nothing" is always "at which stage did the * count reach zero", so the report is a funnel: files in the tsconfig, * files the import gates selected, units discovered, summaries built. * Each row is recorded by the stage that owns it, because the * alternative is the CLI re-deriving the pre-filter's decisions from a * second copy of its logic, and a second copy drifting from the first * is what made an entire pack family extract nothing in silence. * * TypeScript, Python and Ruby all build one of these, though only * TypeScript gates files by import specifier today. A pack with no gate * concept reports it the way TypeScript reports an ungated pack. */ /** One pack's path through the funnel. */ interface PackFunnel { pack: string; /** * What the pack calls this build of itself, or null when it declares * nothing. The cache keys on it, so a pack that never changes its * stamp can serve a later run with an earlier build's results. */ version: string | null; /** * Whether the pack looks for units of its own at all. A pack made only * of recognisers contributes effects to units other packs found, so it * always discovers nothing, and that tells you nothing about whether * it is working. */ discovers: boolean; /** * Whether the pack has any recognisers. Those fire inside units other * packs discovered, so a pack made only of them contributes effects * and never a summary. */ recognizes: boolean; /** * Import specifiers that make a file relevant to this pack. Empty * for an ungated pack, which walks every file. */ gates: string[]; /** * Gate specifiers that do not resolve from the project. Non-empty * here usually means the target project's dependencies are not * installed, which stops symbol-resolution packs while leaving * textual-gate packs working, so the run half-succeeds in a way * nothing else surfaces. */ unresolvedGates: string[]; /** Files the pre-filter selected for this pack. */ candidateFiles: number; /** Units discovered across those files. */ unitsDiscovered: number; /** * Unit bodies any pack walked in the files this pack's gate selected. * * This is what a recogniser pack had the chance to fire on. Its own * discovery count tells you nothing, since it discovers nothing by * design, and neither does its candidate-file count, because a * recogniser only runs where some pack found a unit to walk. */ unitsInGatedFiles: number; /** Effects this pack's recognisers returned. */ effectsRecognized: number; /** * Units this pack kept. A unit an earlier pack already claimed is * dropped here, so a pack can discover plenty and keep none when it * comes after a pack that recognises the same code. */ unitsClaimed: number; /** Summaries built from those units. */ summariesProduced: number; /** Summaries in the finished run that credit this pack for finding them. */ summariesBound: number; /** * Bound summaries on the provider side of their boundary. * * Kept apart from the count below because a provider that produced * no transitions is a different problem from a client that produced * none. */ providerSummaries: number; /** * Summaries with at least one transition, of any role. A provider's * transitions say what it does with a request and a client's say what * it expects back, so a pack whose summaries have none has bound * something and described nothing either way. */ summariesWithBehavior: number; /** * Where one of this pack's hooks threw. Every count above is a floor * while this is non-empty. */ failures: PackFailure[]; /** * Registration helpers this pack's config asked for that no call in * the run matched. The helper belongs to the project, so a spelling * that matches nothing is a config mistake with no other symptom: the * routes go missing and every count reads the same as a project * without them. */ helpersUnmatched: string[]; /** * What the pack wrote as data rather than as code, or null for a pack * written as a hand-rolled walk, or for a language whose packs have * no declared-pattern system at all. This is the one thing in the * funnel that no run produces: it is the pack's own shape, and it is * here so the migration onto the declared surface can be measured. */ declarations: PackDeclarations | null; } interface ExtractionReport { /** Files in the project's include set, or null when nothing states one separately from the walk. */ filesInProject: number | null; /** Files the adapter loaded and the extract walked. */ filesWalked: number; packs: PackFunnel[]; summaries: number; /** * Files whose exports the checker could not follow, so the run read * them as exporting nothing. * * Without this the artifact cannot tell the two apart: a module whose * barrel chain outran the call stack and a module that really does * export nothing both produce no summaries and exit 0. * Anything reachable only through these files is missing, and every * count below is a floor while this is non-empty. */ filesWithUnreadableExports: string[]; /** * The first stage whose count was zero, when the run produced * nothing. Null when the run produced summaries. */ emptyStage: EmptyStage | null; /** * Reassigned names the run stated nothing for, because control flow * decides which write a reader sees. Each is a value resolution the * facts decline; the count across a large corpus says whether scoped * reaching definitions is worth writing. */ reassignedNamesUnstated: number; } type EmptyStage = "tsconfig" | "gateResolution" | "candidateFiles" | "discovery" | "assembly"; /** * A pack's hook throwing on one file. * * The run continues with the other files, so every count for that pack * afterwards is a floor rather than a total. Somebody reading those * counts has to be told, or a pack that broke looks the same as a pack * that looked and found nothing. */ interface PackFailure { /** The hook that threw, called what a pack author would call it. */ hook: string; /** The file the pack was reading. */ file: string; message: string; } /** Per-pack running counts, filled as the extract proceeds. */ interface PackTally { candidateFiles: number; unitsDiscovered: number; unitsInGatedFiles: number; effectsRecognized: number; unitsClaimed: number; summariesProduced: number; failures: PackFailure[]; /** Registration helpers from this pack's config that produced a unit. */ helpersMatched: Set; } declare const emptyTally: () => PackTally; /** * Record that a pack's hook threw, and phrase it in one sentence a caller * can print. Both callers want the same wording, and a failure that only * reached stderr left the counts looking like an empty pack. */ declare function recordPackFailure(tally: PackTally | undefined, failure: { pack: string; hook: string; file: string; error: unknown; }): string; declare function createPackTallies(packs: ReadonlyArray<{ name: string; }>): Map; /** Credit the pack whose name a raw unit's boundary binding gives as `recognition`. */ declare function tallyUnit(tallies: Map, recognition: string | undefined): void; /** * The summary-side funnel counts, per pack. * * These are read back off the finished run rather than tallied during * it, because the summaries a pack is responsible for are not all built * where the pack is in scope: wrapper expansion and sub-unit synthesis * both add summaries after discovery has moved on. Every summary * records what recognised it, so grouping on that gets each one back to * the pack that owns it however late it arrived. */ interface SummaryCounts { bound: number; providers: number; withBehavior: number; } declare function summaryCountsByPack(summaries: ReadonlyArray): Map; /** * An extraction report for an adapter with no gate stage of its own, * the way TypeScript reports an ungated pack: every file is a * candidate, and `unitsClaimed` tracks `unitsDiscovered` one for one * because nothing dedups across packs yet. */ declare function buildUngatedExtractionReport(args: { packs: ReadonlyArray<{ name: string; version: string | null; discovers: boolean; }>; tallies: ReadonlyMap; filesWalked: number; summaries: ReadonlyArray; }): ExtractionReport; /** * Checks that tell you a pack is probably not working. * * The funnel shows where a run's counts dropped to zero. These checks * ask something narrower of each pack on its own: did this pack lose * everything at some stage, when the stage before it had something? * * A pack that finds nothing in a codebase that does not use its library * is working correctly, so a count of zero on its own is never the * signal. What makes zero a signal is the count before it. A pack whose * import gate picked forty files and whose discovery then found no unit * in any of them said "look here" and failed to look. Every later stage * works the same way, which is why these checks are one comparison run * over a list of pairs rather than one check written per stage. */ /** One thing that looks wrong, reported the way the dogfood invariants are. */ interface HealthViolation { label: string; detail: string; } interface HealthCheck { /** The property being checked, as something either true or false. */ name: string; /** * The short word this check reports under, kebab-case. * * It is the first column of every line the check prints, so a reader * works out what it means once and recognises it after that. `name` * is written as an assertion and would read backwards over a list of * things failing it. */ code: string; /** * Who the finding is addressed to. * * A `run` check found something about the code in front of it, and * the person who started the run can do something about it: drop a * pack, install a dependency, open an issue with the file that broke. * A `pack` check found something about how a pack was built, which * only whoever ships that pack can fix. Printing the second kind on * every run would teach people to skim past the first. */ audience: "run" | "pack"; violations: HealthViolation[]; } /** * What one pack paid for what it matches. * * Expressiveness is bought link by link, and #542 asks for the price to * be printed. A pack with every link written as data runs on any * adapter with the executor ops. A link written as a function runs only * where its own language does, and one that reads the syntax tree is * the floor. All three are allowed, and this says which is which. */ interface PackGradient { pack: string; /** Links written as data, across every declaration. */ dataLinks: number; /** Links written as a function, as "declaration.question". */ functionLinks: string[]; /** Links whose function reads the adapter's own syntax tree. */ astLinks: string[]; /** Declarations shipped without a line of code to run against them. */ withoutExample: string[]; } /** The gradient for every pack in a run that declared anything. */ declare function packGradients(report: ExtractionReport): PackGradient[]; /** * Run every health check over one extraction report and return what * fired, grouped by which check caught it. */ declare function evaluatePackHealth(report: ExtractionReport): HealthCheck[]; /** * The health checks that fired, as lines for a terminal. * * `audiences` is who the caller is printing for. It is required because * no single choice suits every caller: a CLI run prints what the person * who started it can act on, while a run a pack author is reading wants * both kinds. */ declare function formatPackHealth(checks: ReadonlyArray, audiences: ReadonlyArray): string; /** * Lightweight phase-timing instrumentation. * * Measures wall time spent in named phases of an adapter run so the * CLI can surface "extract took 17s, of which 11s was in the * reachable closure" without engaging a profiler. Cheap enough to * always be on; a phase that isn't entered contributes zero. * * Not OpenTelemetry: this is single-process, stdout-bound, and should * not ship transitively to consumers. If suss ever becomes a * long-running daemon (LSP / file-watcher), revisit then. */ interface TimingReport { totalMs: number; phases: Array<{ label: string; durationMs: number; calls: number; }>; } interface Timer { /** Run `fn`, accumulate wall time under `label`, return its result. */ time(label: string, fn: () => T): T; /** Async variant: same accumulation rule. */ timeAsync(label: string, fn: () => Promise): Promise; /** Snapshot of all accumulated phases, ordered by total time descending. */ report(): TimingReport; } /** * Build a fresh timer. Each adapter run gets its own, keeps * concurrent extracts independent (irrelevant today, will matter if * we add a server mode). */ declare function createTimer(): Timer; /** Null implementation for callers that opt out of timing entirely. */ declare function noopTimer(): Timer; /** * The vocabulary a language adapter implements so that any declared * pack runs on it. * * A pack is data, so something has to turn "did this module declare the * method" into a yes or no about a particular program. That is the * adapter's job, and these interfaces are the whole of what a pack can * ask for. An adapter implements them once and every pack runs there, * which is what lets one pack drive several languages. * * They live here rather than beside the builders that produce them, * because an adapter needs this and none of the rest: the TypeScript * adapter imports these types and no runtime code from * `@suss/recognize`. Python and Ruby will want the same. * * Nothing here mentions a syntax tree. A pack that needs the tree goes * through `@suss/recognize/ast`, a separate import so that reaching for * it shows up in a diff and in the pack health report. */ /** * How a pack pins down the receiver a call is on. * * The migration plan for #542 lists four more: `factoryMade` (`app = * express()`), `imported`, `anchored` (a chain read from its anchor * call), `inherits` (a Ruby or Python receiver matched by ancestry) and * `global` (`process.env`, bare `fetch`). Each arrives as a member here * with an entry in every adapter's dispatch table. The README beside * this file says which starts are not receiver-shaped at all. */ type ReceiverOrigin = DeclaredBy | ConstructedFrom; /** * A receiver whose method one of these modules declared. * * This is the origin for a client the source never spells out. `const * redis = await this.getClient()` says nothing about ioredis, and the * declaration behind `redis.get` says everything. */ interface DeclaredBy { readonly origin: "declaredBy"; /** The modules whose declarations settle the call. */ readonly importedFrom: readonly string[]; } /** * A client made from a module's export, however the program caches it. * Asked of the receiver itself, so it still works where the method is untyped * and `declaredBy` finds nothing to read. A call that reaches for no * receiver, `new GetObjectCommand(...)`, is made by its own callee. */ interface ConstructedFrom { readonly origin: "constructed"; /** The modules whose export the client was made from. */ readonly importedFrom: readonly string[]; /** * Which of those exports, when a pack has to tell two apart. Every * AWS SDK command comes from the one module and goes through the one * `send`, so the command class is what says which operation a call * performs. Unset matches whatever the module exports. */ readonly named?: readonly string[]; } type UnsettledName = "nothing" | "reference"; /** * One value a call states, as the questions a pack can ask about it. * * `CallOps` reaches a call beside the one in hand, and this reaches the * values that are not calls. A library that takes one request object * puts everything the call is doing inside it, sometimes as a list and * sometimes as a string in a little language of the library's own, and * a pack that reads those is handed this rather than the adapter's own * node. So the rule it writes runs wherever the ops do. */ interface ValueOps { /** The text of the string the source wrote, or null for anything else. */ text(): string | null; /** * What this value is called, when the source names it rather than * writing it out. A queue URL is nearly always * `process.env.ORDERS_QUEUE_URL`, so the value only exists at deploy * time and the env var's name is what both sides of the boundary * agree on. `"reference"` gives that name back, `"nothing"` gives * null for anything the source does not settle. */ name(unsettled: UnsettledName): string | null; /** * The yes or no the source wrote here, or null for anything else. A * library that asks which fields a call wants states them as a map of * flags, `{ name: 1, password: 0 }`, and a number and a boolean mean * the same thing in one of those. */ flag(): boolean | null; /** What this object states, entry by entry. Empty for anything else. */ entries(unsettled: UnsettledName): readonly ValueEntry[]; /** What this list states, item by item. Empty for anything else. */ items(): readonly ValueOps[]; /** What one named property of this object states, or null for none. */ property(name: string): ValueOps | null; /** * This value in the form an effect records an argument, or null when * the adapter cannot write one. A pack that wants a payload compared * across a boundary asks for this, since a body reduced to text * cannot be paired field by field. */ asArg(): EffectArg | null; /** * The pieces of text the source wrote here, in order, with whatever * it interpolated between them left out. A string is one piece and a * template is one piece per hole plus one, so a reader that means to * put its own placeholders in the holes can. Null when the source * wrote neither. */ parts(): readonly string[] | null; /** * What the source interpolated between those pieces, in order, each * as the call it was written as. There is one per gap, one fewer than * `parts` gives, and a hole the source wrote as something other than * a call is null rather than being dropped, so the two lists stay in * step. * * What a hole comes to is the pack's to say. This says what was * there, so a query that hands over a table object can still be read. */ holes(): readonly (CallOps | null)[]; /** * The same holes as values, in the same order, for a reader that * wants what the source settled a hole to rather than what call was * written there. A table kept in a module constant is the case: the * hole is a name, not a call, so `holes` gives null for it and this * gives the constant. * * An adapter that has not implemented this leaves it out, and a * reader then has only the calls. */ interpolated?(): readonly ValueOps[]; } /** One entry of an object a call states. */ interface ValueEntry { /** * What the entry is called. A key the source computes, * `{ [table]: ... }`, is read the way any other name is. */ readonly key: string | null; /** What the entry says. */ readonly value: ValueOps; } /** * One call site, as the questions a chain asks about it. * * An adapter builds one of these per call and hands it to the * recognizer through its context. Every member is about the call in * hand, so there is no node to pass around and no place for a pack to * reach past what it declared. * * Two members give back another `CallOps`, which is how these questions * reach a call next to this one. A chain of calls and a command object * are both these same questions asked one step along, so neither needed * a question of its own. */ interface CallOps { /** Which method the call reaches for, spelled as the source spells it. */ method(): string | null; /** Whether the receiver came from where the origin says. */ receiverIsFrom(origin: ReceiverOrigin): boolean; /** * Whether the call itself came from where the origin says, which is * the same question asked of the callee rather than of what it was * called on. `new GetCommand(...)` is the case: a pack that steps to * an argument asks this of it before reading anything, so it never * reads an argument that was never the one. */ isFrom(origin: ReceiverOrigin): boolean; /** How many arguments the call passes. */ argumentCount(): number; /** The name the argument in this position gives. */ nameAt(index: number, unsettled: UnsettledName): string | null; /** The callee, as the source writes it. */ calleeText(): string; /** * The call the receiver is, or null when the receiver is not a call. * A receiver the source wrote into a variable comes back as the call * it was written as. */ receiver(): CallOps | null; /** * The call the argument in this position is, or null when that * argument is neither a call nor a construction. `new * GetObjectCommand(...)` comes back as a call whose callee is the * class, the same as any other call. */ argument(index: number): CallOps | null; /** * The call the callee itself was written as, or null when nothing * wrote it as one. A class a factory made is the case: `new * User({...})` says nothing about what `User` is, and the * `model("User", schema)` call it was declared as says everything. */ callee(): CallOps | null; /** * Whether the call goes to a name the program bound, rather than to * an expression written in place. `useAppStore(...)` does; * `create()(...)` does not, even though both are bare calls. */ namedCallee?(): boolean; /** * The properties the function argument in this position reads off * its first parameter, one per distinct first segment: `(s) => * s.bears.count` reads `bears`, and a parameter used whole reads * `*`. Null when the argument is not a function of one plain * parameter. */ parameterReadsAt?(index: number): readonly string[] | null; /** * What a named property of the object an argument states says. A * property bag is not a call, so nothing else here reaches into one. */ propertyAt(index: number, property: string, unsettled: UnsettledName): string | null; /** * The value the argument in this position states, or null when the * call passes none. `propertyAt` reads one name out of a property * bag, which covers a pack that wants one; this hands the bag over * for a pack whose rule has to walk it. */ valueAt(index: number): ValueOps | null; /** * The one call behind the receiver the origin accepts, however many * name, construction, or query hops separate them. Mongoose's * `model(...)` behind a document is the picture. Null when nothing * behind the receiver matches, when two distinct calls do (picking * one would depend on the order facts arrived), and for an origin * kind with no construction to hand back. */ anchorCall?(origin: ReceiverOrigin): CallOps | null; } /** * The property an adapter puts its ops on, in the context it hands a * recognizer. A context without it belongs to an adapter that has not * implemented the ops, and a declared pack matches nothing there. */ interface OpsCarrier { ops?: CallOps; } /** * The ops an adapter implements when it can hand out its own nodes. The * extra member is here rather than on `CallOps` so that a pack reaching * for a node has to import this module first. */ interface AstCapableOps extends CallOps { /** The adapter's own node for the call in hand. */ ast(): unknown; } /** * The assembly engine: a `RawCodeStructure` from a language adapter goes in, * a `BehavioralSummary` comes out. The package README explains where that step * fits in the pipeline. * * Two things surprise people reading this file. First, `RawCodeStructure` and * the raw types around it are the contract every adapter and pack implements, * so a field added here is a change to that contract. Second, this is the only * module allowed to turn a `Reading` into a claim on a summary; adapters hand * readings over uncollapsed and the rule for collapsing them lives here. */ interface RawParameter { name: string; position: number; /** * What the parameter is for, in the library's own vocabulary. When the * adapter could not tell, this is null rather than a guess, and the reason * goes in `readings`. */ role: string | null; typeText: string | null; } interface RawCondition { sourceText: string; structured: Predicate | null; polarity: "positive" | "negative"; source: ConditionSource; } interface RawTerminal { kind: "response" | "throw" | "return" | "render" | "delegate" | "emit" | "void"; statusCode: { type: "literal"; value: number; } | { type: "dynamic"; sourceText: string; } | null; body: { typeText: string | null; shape: TypeShape | null; } | null; exceptionType: string | null; message: string | null; /** Set on a throw whose pack declared the thrown status is the wire response. */ producesResponse?: boolean; component: string | null; /** Null when the pack read only the root element name, not the tree under it. */ renderTree: RenderNode | null; delegateTarget: string | null; emitEvent: string | null; location: { start: number; end: number; }; } /** * An invocation argument as the adapter read it. An argument that fits none of * these variants, arithmetic for instance, comes through as null instead of * being dropped, so a reader still knows how many arguments the call had. */ type EffectArg = { kind: "string"; value: string; } | { kind: "number"; value: number; } | { kind: "boolean"; value: boolean; } | { kind: "object"; fields: Record; } /** * The element shapes the adapter could read, which is not always one * per runtime element. An array a callback builds, * `tags.map((tag) => ({ name: tag }))`, arrives with a single item, * because every element it produces has that one shape. */ | { kind: "array"; items: EffectArg[]; } | { kind: "template"; sourceText: string; } /** * A bare variable or a whole access chain, written out as it appears. An * identifier bound to a module-level const with a simple initializer is * replaced by that initializer, so a constant does not hide the value. */ | { kind: "identifier"; name: string; } | { kind: "call"; callee: string; args: EffectArg[]; } | null; type RawEffect = { type: "mutation"; target: string; operation: "create" | "update" | "delete"; } | { type: "invocation"; callee: string; args: EffectArg[]; async: boolean; /** Empty means the call always fires, not that nobody looked. */ preconditions?: RawCondition[]; } | { type: "emission"; event: string; } | { type: "stateChange"; variable: string; }; interface RawBranch { conditions: RawCondition[]; terminal: RawTerminal; effects: RawEffect[]; /** * Effects a recognizer built itself, already in IR form, so these skip the * `RawEffect` conversion that `effects` goes through. */ extraEffects?: Effect[]; location: { start: number; end: number; }; isDefault: boolean; /** The fields a consumer reads off a response inside this branch. */ expectedInput?: TypeShape | null; /** * An adapter that passes its reading along uncollapsed sets this instead of * `terminal.statusCode`. A status nobody wrote then becomes a claim only * where a pack declared the default. */ statusCodeReading?: DefaultedReading; /** * Set instead of `terminal.body.shape`, on the same terms. * `terminal.body.typeText` is left alone, so a pack that gives the type as * text and also reads its structure can do both. */ bodyShapeReading?: DefaultedReading; } interface RawDependencyCall { name: string; assignedTo: string | null; async: boolean; returnType: string | null; location: { start: number; end: number; }; } interface RawDeclaredContract { framework: string; responses: Array<{ statusCode: number; /** Null means either the contract declared no body or suss could not read * the schema form, and nothing here tells the two apart. */ body?: TypeShape | null; }>; params?: Record; /** * "derived" means the contract and the transitions both come from the same * source, so comparing them proves nothing and the checker skips it. A pack * that says nothing gets "independent", which risks a spurious finding * rather than dropping a valid one. */ provenance?: "derived" | "independent"; } /** * What the adapter found where a unit's body should be. Downstream this is the * difference between a summary that says nothing because there was nothing to * say and one that says nothing because nobody could read the body. * * "absent" is a declaration with no body at all, such as an overload * signature. "empty" is a body with nothing in it, which an empty summary * describes completely. "statements" is a body with work in it, which an empty * summary describes none of. "elsewhere" is a body this run did not read: a * route registered with a handler the caller supplies points at no function to * go look in. */ type BodyContent = "absent" | "empty" | "statements" | "elsewhere"; interface RawCodeStructure { /** Types the unit's shapes refer to by name instead of spelling out, so a * reader who follows one of those names has somewhere to look. */ definitions?: Record | null; identity: { name: string; nameKind?: "binding" | "label"; kind: CodeUnitKind; file: string; range: { start: number; end: number; }; /** Character offsets of the unit, when a source position backs it. */ span?: { start: number; end: number; }; exportName: string | null; exportPath: string[] | null; }; /** Null when the unit is not on any cross-unit boundary, which is the * ordinary case for helpers and pure utilities. */ boundaryBinding: BoundaryBinding | null; deployableUnit?: DeployableUnit; parameters: RawParameter[]; branches: RawBranch[]; /** Return statements no terminal in the pack matched. Leave this at zero and * a handler the pack cannot describe looks like one that returns nothing. */ unmatchedReturns?: number; /** * Only the adapter can tell a body nobody could read from a body with * nothing in it, and an empty summary looks the same either way. */ bodyContent?: BodyContent; dependencyCalls: RawDependencyCall[]; /** * Reads the adapter saw in the body that never flow into a condition * or an output value, a render tree's `props.title` say. Merged into * `inputReads` beside the derived ones. */ extraInputReads?: InputRead[]; declaredContract: RawDeclaredContract | null; /** The property a consumer goes through to get at the body, `data` for * axios say, so the checker can unwrap it without knowing each pack. */ bodyAccessors?: string[]; /** The same, for the status: `status` for fetch and for axios. */ statusAccessors?: string[]; /** The same, for the success flag: `ok` for fetch, nothing for axios. */ successAccessors?: string[]; /** Whether this client's non-2xx arrives as a response or a rejection. */ failureDelivery?: FailureDelivery; /** Left exactly as written, because the extractor does not depend on * graphql-js. The parsing happens at check time. */ graphqlDocument?: string; graphqlSchemaSdl?: string; /** The document this unit was read out of, when something else read out of * the same document states what this one relies on. */ sourceDocumentLabel?: string; /** Set when several mounts serve the declaration and this unit is one of them. */ mount?: { siblings: number; prefix: string; }; /** The wrappers registered around this unit: middleware, error handlers. */ wrappers?: WrapperReference[]; /** Where a REST pack's handlers read each part of the request, from the pack. */ requestSpelling?: RequestSpellingMetadata; /** The extractor cannot derive this. An adapter that has the SDL and knows * which field the resolver serves fills it in. */ graphqlDeclaredContract?: GraphqlDeclaredContract; /** Fragment spreads in `graphqlDocument` with no definition in it, so a * partially read document is marked rather than passed off as whole. */ graphqlUnresolvedFragments?: string[]; /** Spreads in `graphqlDocument` the project defines more than once, with * different bodies, so no definition of them could be used. */ graphqlAmbiguousFragments?: string[]; /** Set when a document reference was recognized but its body could not be * read, so an unreadable document is accounted for instead of dropped. */ graphqlUnresolvedDocument?: { reference: string; reason: string; }; /** * Which part of the boundary the source does not state, and why. The binding * still goes out with that part empty, so the unit pairs with nothing rather * than with whatever a guess would have supplied. It comes out as an * `unreadOutcome` gap, so no checker counts it against the unit. */ unreadBinding?: string; /** * Readings the adapter passed along without collapsing. This module writes * the reason for any that came back unreadable or ambiguous. Written and * absent readings contribute nothing here, since what they found is already * on the summary. */ readings?: readonly Reading[]; } interface ExtractorOptions { gapHandling: "strict" | "permissive" | "silent"; } /** * The id for one branch's transition, built so that editing a handler does not * churn it. Reordering branches, or adding an unrelated one, must leave the * existing ids alone, otherwise `diffSummaries` reports "everything changed" * every time somebody shuffles a handler around. * * The id combines the enclosing function name, the terminal kind, the status * code (a literal value, the source text of a dynamic one, or "none"), and a * short hash of the condition chain's source texts. Editing a branch's body * without touching its guards or its status leaves the id alone, so a diff * reports one changed transition instead of an add plus a remove. Change any * of those signals and you get a new id. */ declare function makeTransitionId(functionName: string, branch: RawBranch): string; declare function assembleSummary(raw: RawCodeStructure, options?: ExtractorOptions): BehavioralSummary; declare function detectGaps(raw: RawCodeStructure, transitions: Transition[], options: ExtractorOptions): Gap[]; declare function assessConfidence(raw: RawCodeStructure): ConfidenceInfo; declare function terminalToOutput(terminal: RawTerminal): Output; /** A condition the adapter left unstructured becomes an opaque predicate * rather than being dropped, so the branch keeps its guard. */ declare function rawConditionToPredicate(c: RawCondition): Predicate; declare function effectToIR(effect: RawEffect): Effect; declare function paramToInput(param: RawParameter): Input; export { type AccessRecognizer, type AdapterCodeStamp, type AdapterStamp, type AstCapableOps, type BindingExtraction, type BodyContent, type CacheAttribution, type CacheDiagnostic, type CacheInput, type CacheLayer, type CacheLookup, type CallOps, type CaseGroup, type ChannelSource, type ChosenReading, type ConditionHandle, type ConditionInfo, type ConditionSource, type ConstructedFrom, type ContractPattern, type DeclaredBinding, type DeclaredBy, type DeclaredMatch, type DefaultedReading, type DiscoveredCustomUnit, type DiscoveredSubUnit, type DiscoveredSubUnitParent, type DiscoveryMatch, type DiscoveryPattern, type EffectArg, type EmptyStage, type ExitKind, type ExtractionReport, type ExtractorOptions, type FailureDelivery, type HealthCheck, type HealthViolation, type HelperDeclarations, type HelperSearch, type HelperSink, type HelperValue, IdMap, IdSet, type Identified, type InputMappingPattern, type InputRead, type InvocationRecognizer, type LanguageAdapter, type LoweredStatementParts, MAX_ENTRIES, MAX_PATHS, type ModuleInitOptions, type NodeVisitor, type OpsCarrier, type PackDeclarations, type PackFailure, type PackFunnel, type PackGradient, type PackTally, type PartialPlan, PathBudgetExceeded, type PatternPack, type ProjectHelper, type ProjectHelpers, type RawBranch, type RawCodeStructure, type RawCondition, type RawDeclaredContract, type RawDependencyCall, type RawEffect, type RawParameter, type RawTerminal, type Reading, type ReceiverOrigin, type RegistrationHelper, type ResponsePropertyMapping, type ResponsePropertyMeaning, type RootRecord, SKIP_CHILDREN, type SourceRange, type StatementBlock, type StructuredPathConditionsInput, type StructuredPathConditionsResult, type StructuredStatement, type TerminalExtraction, type TerminalMatch, type TerminalPattern, type Timer, type TimingReport, type TransparentWrapper, UnmodeledFlow, type UnsettledName, type ValueEntry, type ValueOps, type WalkableNode, type WrapperMethodRegistration, type WrapperOptionRegistration, type WrapperRegistration, absentReading, ambiguousReading, andThenReading, assembleSummary, assessConfidence, buildUngatedExtractionReport, commonDirectoryOf, composeWrappers, computeContentHash, computeDistHashFrom, configuredCallOption, createAdapterStamp, createCacheLayer, createPackTallies, createTimer, detectGaps, effectToIR, emptyTally, enumerateOrDegrade, enumerateStructuredPaths, evaluatePackHealth, firstWrittenReading, formatPackHealth, guardsHoldOn, httpRouteDiscovery, makeTransitionId, mapReading, moduleInitStructure, noopTimer, packGradients, paramToInput, projectFileStamp, rawConditionToPredicate, recordPackFailure, registrationHelperDiscovery, routeHelperIndex, runDigest, runsBefore, scopeOption, sharedGatingConditions, stampModuleImports, storageSystemOption, summaryCountsByPack, tallyUnit, terminalToOutput, unreadableReading, unwrapJsonStringify, valueToReadFurtherFrom, walkDescendants, wrapperDiscovery, writtenReading };