/** * KERN Concept Model — universal code concepts for cross-language review. * * Concepts model MEANING, not syntax. A mapper per language translates * language-specific syntax into universal concepts. Rules operate on concepts. * * ConceptNode: entity (entrypoint, effect, guard, error, state mutation) * ConceptEdge: relation (call, dependency) */ export interface ConceptSpan { file: string; startLine: number; startCol: number; endLine: number; endCol: number; } export type ConceptNodeKind = 'entrypoint' | 'effect' | 'state_mutation' | 'error_raise' | 'error_handle' | 'guard' | 'function_declaration'; export type ConceptEdgeKind = 'call' | 'dependency'; export interface EntrypointPayload { readonly kind: 'entrypoint'; subtype: 'route' | 'handler' | 'main' | 'export' | 'event-listener' | 'route-mount'; /** * - `'route'`: the route path (e.g. `/current`, `/api/users/{id}`). * - `'route-mount'`: the URL prefix applied by the mount (e.g. `/api/nutrition-goals`). * - Others: the function/handler name. */ name: string; httpMethod?: string; /** * For Python/FastAPI-style route decorators, the declared response shape * from `response_model=...`. Omitted when the mapper cannot prove one is * present. */ responseModel?: string; /** * For route entrypoints whose mapper can inspect the backing handler. * Omitted when the route abstraction does not expose handler async-ness. */ isAsync?: boolean; /** * For `'route'` and `'route-mount'` only — the variable name the * decorator was applied to (`router`, `app`) or the target of * `include_router(, ...)`. Used by `collectRoutes` to join per-file * route decorators with the `include_router(prefix=…)` call that mounts * them under a URL prefix. */ routerName?: string; /** * For `'route-mount'` only — the imported module specifier hosting the * router. FastAPI example: `from app.api import nutrition_goals; * app.include_router(nutrition_goals.router, prefix="/api/nutrition-goals")` * → `sourceModule: 'app.api.nutrition_goals'`. The cross-stack collector * resolves this against route file paths to attach the prefix. */ sourceModule?: string; /** * For `'route'` only, and only when the handler is an INLINE arrow / * function expression on the same call (e.g. `app.get('/x', (req, res) => {})`). * Holds the concept id of the handler's `function_declaration` concept. * Undefined when the handler is an imported identifier or not resolvable. * * Rules that reason about the handler body (what fields it reads from * `req.body`, whether it calls `res.status()` etc.) resolve this id back * to the `function_declaration` concept in the same file, then walk the * primary span. */ handlerConceptId?: string; /** * For `'route'` only — the REQUIRED body field names the handler reads * from `req.body`, either via property access (`req.body.name`) or * destructuring (`const { name } = req.body`). Defaults in destructuring * (`const { status = 'active' } = req.body`) are treated as optional and * excluded. Present only when `bodyFieldsResolved === true`. */ bodyFields?: readonly string[]; /** * True when the mapper is confident it saw every field the handler reads. * False / undefined when evidence was ambiguous (spread rest, dynamic key * access, whole-body forwarding, imported handler). Cross-stack rules * like body-shape-drift only fire when this is true to avoid noisy * warnings on opaque handlers. */ bodyFieldsResolved?: boolean; /** * Coarse type tag per `bodyFields` entry — server-side mirror of * `EffectPayload.sentFieldTypes`. Same tag union: `'string' | 'number' | * 'boolean' | 'null' | 'object' | 'array' | 'unknown'`. Populated only * when `bodyFieldsResolved === true`. * * Keys are a subset of `bodyFields`. When the handler reads `req.body` * with no usable type information (the default Express `any`), every * entry is `'unknown'`. The `body-shape-drift` rule's type-aware step * skips any pair where either side is `'unknown'` to keep precision * high — type-mismatch findings only fire when both ends are typed. */ bodyFieldTypes?: Readonly>; /** * For server route entrypoints only — HTTP error status codes the handler can * explicitly return/raise. Mappers only populate high-signal statuses such as * 401/403/404/422/500 from constructs like Express `res.status(404)` or * FastAPI `HTTPException(status_code=404)`. */ errorStatusCodes?: readonly number[]; /** * For server route entrypoints only — HTTP success status codes (2xx) the * handler emits. Populated from explicit `res.status(2xx)` / `res.sendStatus(2xx)` * on Express, or `@app.get(..., status_code=201)` on FastAPI. Implicit 200 is * inferred only when the handler has a terminal call (`.json()` / `.send()` / * `.end()`) AND the mapper sees no explicit 2xx anywhere in the body. * * Consumed by `status-code-drift` to flag clients that branch on a 2xx the * server doesn't actually emit (e.g. `if (res.status === 201)` against a * route that returns 200). */ successStatusCodes?: readonly number[]; /** * True when the mapper had full visibility into the handler body to enumerate * success status codes. False/undefined when the handler is imported, dynamic, * or has wrappers/middleware the mapper can't statically resolve. Cross-stack * rules consume `successStatusCodes` only when this flag is `true` to keep * precision high. */ successStatusCodesResolved?: boolean; /** * For server route entrypoints only — pagination strategy the handler accepts, * inferred from the query keys it reads. Anchor sets: * - `'page'` → handler reads `page` / `pageNumber` / `page_number` * - `'offset'` → handler reads `offset` / `skip` * - `'cursor'` → handler reads `cursor` / `after` / `before` / `next` / `previous` * - `'mixed'` → handler reads keys from multiple anchor families (e.g. both * `page` and `offset`) — server tolerates either, so cross-stack * rules MUST stay silent * - `'none'` → handler reads no pagination anchors at all (size-only keys * like `limit` count as no anchor; non-pagination keys count * as no anchor) * * Consumed by `pagination-key-drift` to flag a client that uses a different * anchor family from the server. */ paginationStrategy?: 'page' | 'offset' | 'cursor' | 'mixed' | 'none'; /** * True when the mapper had full visibility into the handler body for * pagination-key extraction (no dynamic `req.query[varName]` accesses, no * unresolved imports). Cross-stack rules consume `paginationStrategy` only * when this flag is `true`. */ paginationStrategyResolved?: boolean; /** * True when the route appears to return a DB-backed collection without a * limit/page/cursor/offset bound. Used with client query evidence by * `unbounded-collection-query`. */ hasUnboundedCollectionQuery?: boolean; /** True when the route performs a DB write. */ hasDbWrite?: boolean; /** * True when the mapper sees idempotency/duplicate-protection evidence such as * an idempotency key, transaction, upsert, unique guard, or conflict clause. */ hasIdempotencyProtection?: boolean; /** True when the route validates request body data before use. */ hasBodyValidation?: boolean; /** * Body fields accepted by a resolved validation schema/model. Present only * when `bodyValidationResolved === true`. */ validatedBodyFields?: readonly string[]; /** * True when the mapper is confident the validation field list is complete. */ bodyValidationResolved?: boolean; /** * Coarse type tag per validated body field, derived from a recognised * schema literal (e.g. Zod `z.object({ name: z.string(), age: z.number() })`). * Same tag union as `bodyFieldTypes`. Populated only when the validator * is a recognised schema DSL whose call shapes can be coarsened. * * Used by `body-shape-drift/type` as a precision fallback: when the * handler reads `req.body` with no usable TS type info (the default * Express `any`, so `bodyFieldTypes[f] === 'unknown'`), the rule consults * this map. Catches `userId: string` (client) vs `userId: z.number()` * (server schema) on handlers that validate but don't type `req.body`. */ validatedBodyFieldTypes?: Readonly>; } export interface EffectPayload { readonly kind: 'effect'; subtype: 'network' | 'db' | 'fs' | 'process' | 'time' | 'random' | 'background-task'; target?: string; async: boolean; /** * For `network` subtype only. `true` when the call's eventual JSON value is * consumed with a type annotation, `as T` cast, or `satisfies T` clause; * `false` when `.json()` is awaited without any assertion; `undefined` when * the mapper can't tell (no `.json()` in scope, or the shape is too * complex to analyze statically). Feeds the `untyped-api-response` rule. */ responseAsserted?: boolean; /** * For `network` subtype only. Classifies what the call sends on the wire: * - `'none'` — no body (GET, or no options arg at all). * - `'static'` — body is a string literal or literal object without any * dynamic interpolation. * - `'dynamic'` — body is built from variables, template literals with * `${…}`, or `JSON.stringify(x)` for some non-literal `x`. * Feeds the `tainted-across-wire` rule so it can fire only on the class * of calls that actually carry user-controlled data. */ bodyKind?: 'none' | 'static' | 'dynamic'; /** * For `network` subtype only. Uppercase HTTP method (`GET`, `POST`, …) when * the mapper can derive it confidently: axios-style `axios.post(…)`, wrapped * `apiClient.get(…)`, or raw `fetch(url, { method: 'POST' })`. Undefined * when the call is a generic `axios(config)` whose method lives in a runtime * variable — we'd rather stay silent than guess. Feeds the * `contract-method-drift` rule. */ method?: string; /** * For `network` subtype only. `true` when the call's options literal carries * an `Authorization` header (any value — we don't inspect the token). `false` * when the options literal is present but no Authorization header exists. * `undefined` when the options arg is a variable, spread, or missing. Feeds * the `auth-drift` cross-stack rule. */ hasAuthHeader?: boolean; /** * For `network` subtype only — names of body fields the call sends. * Populated when the body is a literal object, or when the mapper can derive * a complete field set from a local payload type/interface. Present only * when `sentFieldsResolved === true`. */ sentFields?: readonly string[]; /** * True when the mapper is confident the extracted `sentFields` list is * complete. False / undefined when the body uses spread, variable * references, dynamic keys, or non-object shapes (FormData, Blob, raw * strings). The `body-shape-drift` rule fires only when BOTH this and * the server-side `bodyFieldsResolved` are true. */ sentFieldsResolved?: boolean; /** * For `network` subtype only — coarse type tag per `sentFields` entry. * Same key set as `sentFields`. Each tag is one of: * - `'string' | 'number' | 'boolean' | 'null'` — primitive literals * - `'object' | 'array'` — nested structured values * - `'unknown'` — value position present but type couldn't be inferred * (fallback so the key isn't silently dropped from the type map) * * The tag is intentionally coarse — no string-literal narrowing * (`'admin' | 'user'`), no nested object shape, no generics. Powers * cross-stack rules that want "names + types match" precision (e.g. * `body-shape-drift` upgrading from "name overlap" to "name + type * overlap"). Without this every `userId: string` (client) would silently * "match" a `userId: number` (server) just because the names overlap. * * Populated only when `sentFieldsResolved === true`. Field-set is a * subset of `sentFields` (we never emit a tag for a field name that * isn't already in the names list). */ sentFieldTypes?: Readonly>; /** * For `network` subtype only. `true` when the call-site has a local error * path (try/catch, `.catch`, `response.ok`/status check, or known error UI); * `false` when a raw inspectable call has no such path; `undefined` when a * wrapper or dynamic call prevents a confident answer. */ handlesApiErrors?: boolean; /** * For `network` subtype only. Whether the call-site visibly propagates auth * (Authorization/Cookie/session credentials or known authenticated wrapper), * visibly does not, or is opaque. */ authPropagation?: 'present' | 'absent' | 'unknown'; /** * Query string parameter names from a literal/template URL target. Present * when `queryParamsResolved === true`. */ queryParams?: readonly string[]; /** * True when the mapper could fully inspect query parameters on the target URL. */ queryParamsResolved?: boolean; /** * For `network` subtype only. Lower-case host extracted from the call's URL * when it was an absolute URL (`https://api.example.com/users` → `api.example.com`, * `http://localhost:8080/x` → `localhost:8080`). Undefined for relative * URLs (`/api/users`), template literals where the host is interpolated * (`https://${HOST}/api`), and unresolved expressions. * * Captured but not yet consumed by cross-stack rules. Phase 1 of the * surface-fingerprinting work — phase 2 will let cross-stack rules use * this to filter out third-party hosts (e.g. a frontend `fetch` to * `stripe.com/api/charges` should not match against the partner backend's * `/api/charges` route). */ host?: string; /** * For `network` subtype only. The set of HTTP status codes the call-site * EXPLICITLY branches on — extracted from `response.status === N`, * `err.status === N`, `err.response?.status === N`, or `case N:` in a * switch over one of those expressions. Excludes generic catch-all * handlers (`catch (e) { log(e); }`), `response.ok` checks, and * status-range tests (`status >= 400`). * * Empty list means we saw the network call but no explicit status * dispatch — the call-site treats every failure identically. Undefined * means the analysis was inconclusive (response variable escaped the * scope, dynamic status check, etc). * * Phase 1 of the `error-contract-drift` work — phase 2 will compare * this against the server-side `errorStatusCodes` to flag PRs that add * a server status the client doesn't handle. Captured but not yet * consumed. */ handledErrorStatusCodes?: readonly number[]; } export interface StateMutationPayload { readonly kind: 'state_mutation'; target: string; scope: 'local' | 'module' | 'global' | 'shared'; via?: 'assignment' | 'increment' | 'call'; api?: string; } export interface FunctionDeclarationPayload { readonly kind: 'function_declaration'; name: string; async: boolean; hasAwait: boolean; isComponent: boolean; isExport: boolean; } export interface ErrorRaisePayload { readonly kind: 'error_raise'; subtype: 'throw' | 'reject' | 'err-return' | 'panic'; errorType?: string; } export interface ErrorHandlePayload { readonly kind: 'error_handle'; disposition: 'ignored' | 'logged' | 'wrapped' | 'returned' | 'rethrown' | 'retried'; errorVariable?: string; } export interface GuardPayload { readonly kind: 'guard'; subtype: 'auth' | 'validation' | 'policy' | 'rate-limit'; name?: string; } export interface CallPayload { readonly kind: 'call'; async: boolean; name: string; } export interface DependencyPayload { readonly kind: 'dependency'; subtype: 'internal' | 'external' | 'stdlib'; specifier: string; } export type ConceptNodePayload = EntrypointPayload | EffectPayload | StateMutationPayload | ErrorRaisePayload | ErrorHandlePayload | GuardPayload | FunctionDeclarationPayload; export type ConceptEdgePayload = CallPayload | DependencyPayload; export interface ConceptNode { /** Deterministic ID: `${filePath}#${kind}@${offset}` */ id: string; kind: ConceptNodeKind; primarySpan: ConceptSpan; evidenceSpans?: ConceptSpan[]; /** The actual code that was classified */ evidence: string; /** 0.0–1.0: how confident the mapper is */ confidence: number; /** Source language: 'ts', 'py', 'go', etc. */ language: string; /** Parent function/class ID for scoping */ containerId?: string; /** Typed payload — specific to kind */ payload: ConceptNodePayload; } export interface ConceptEdge { /** Deterministic ID */ id: string; kind: ConceptEdgeKind; sourceId: string; targetId: string; primarySpan: ConceptSpan; evidence: string; confidence: number; language: string; payload: ConceptEdgePayload; } export interface ConceptMap { filePath: string; language: string; nodes: ConceptNode[]; edges: ConceptEdge[]; extractorVersion: string; } export declare function conceptId(filePath: string, kind: string, offset: number): string; export declare function conceptSpan(file: string, startLine: number, startCol: number, endLine?: number, endCol?: number): ConceptSpan;