import { MultiDirectedGraph } from 'graphology'; import { GraphNode, GraphEdge, EdgeEvidence, ErrorEvent, SpanAttributes, DeployContext, EdgeTypeValue, StaleEvent, BlastRadiusResult, RootCauseResult, RegistryEntry } from '@neat.is/types'; export { StaleEvent } from '@neat.is/types'; import { FastifyInstance } from 'fastify'; export { ProjectNameCollisionError, addProject, getProject, listProjects, normalizeProjectPath, readRegistry, registryLockPath, registryPath, removeProject, setStatus, touchLastSeen, writeAtomically } from './registry.js'; type NeatGraph = MultiDirectedGraph; declare function getGraph(project?: string): NeatGraph; declare function resetGraph(project?: string): void; interface ExtractionError { producer: string; file: string; error: string; stack?: string; ts: string; source: 'extract'; } interface DroppedExtractedEdge { source: string; target: string; type: string; confidence: number; confidenceKind: string; evidence: EdgeEvidence; } interface SkippedSourceFile { path: string; reason: 'minified'; detail: string; } interface SourceCommit { repository: string; sha: string; } interface ExtractResult { nodesAdded: number; edgesAdded: number; frontiersPromoted: number; extractionErrors: number; errorEntries: ExtractionError[]; ghostsRetired: number; skippedFiles: number; skippedEntries: SkippedSourceFile[]; extractedDropped: number; droppedEntries: DroppedExtractedEdge[]; } interface ExtractOptions { sourceCommit?: SourceCommit; onPolicyTrigger?: (graph: NeatGraph) => Promise | void; project?: string; errorsPath?: string; source?: string; announce?: boolean; } declare function extractFromDirectory(graph: NeatGraph, scanPath: string, opts?: ExtractOptions): Promise; interface CompatibilityResult { compatible: boolean; reason?: string; minDriverVersion?: string; } interface CompatPair { kind?: 'driver-engine'; driver: string; engine: string; minDriverVersion: string; minEngineVersion?: string; reason: string; } declare function checkCompatibility(driver: string, driverVersion: string, engine: string, engineVersion: string): CompatibilityResult; declare function compatPairs(): readonly CompatPair[]; /** * Write the snapshot, atomically, without ever holding it as one string. * * `JSON.stringify` on a whole large graph hits V8's ~2^29-character cap and * throws `RangeError: Invalid string length` (#1254) — extraction finishes and * the snapshot is simply lost. Streaming the two big arrays element by element * bounds the file by disk rather than by string length. The bytes are identical * to what the single-call version wrote, so `loadGraphFromDisk` and every other * reader are untouched. * * Note the read side still holds the file as one string, so a snapshot past the * same cap will write but not load back. That ceiling is real and separate. */ declare function saveGraphToDisk(graph: NeatGraph, outPath: string): Promise; declare function loadGraphFromDisk(graph: NeatGraph, outPath: string): Promise; interface PersistLoopOptions { intervalMs?: number; exitOnSignal?: boolean; } declare function startPersistLoop(graph: NeatGraph, outPath: string, opts?: PersistLoopOptions): () => void; interface ScoredNode { node: GraphNode; score: number; } interface SearchResponse { query: string; provider: 'ollama' | 'transformers' | 'substring'; matches: ScoredNode[]; } interface SearchIndex { readonly provider: SearchResponse['provider']; search(query: string, limit?: number): Promise; refresh(graph: NeatGraph): Promise; } interface ProjectPaths { snapshotPath: string; errorsPath: string; staleEventsPath: string; embeddingsCachePath: string; policyViolationsPath: string; } interface ProjectContext { name: string; graph: NeatGraph; scanPath?: string; paths: ProjectPaths; searchIndex?: SearchIndex; } declare class Projects { private contexts; upsert(ctx: ProjectContext): void; set(name: string, init: Omit & { graph?: NeatGraph; }): ProjectContext; get(name: string): ProjectContext | undefined; has(name: string): boolean; list(): string[]; attachSearchIndex(name: string, index: SearchIndex | undefined): void; } /** What one pass did: whether the bound-repo list was read, and how the repos it took on came out. */ interface RepoSyncPassCounts { listed: boolean; synced: number; failed: number; } /** A finished pass, as the on-demand trigger reports it. */ interface RepoSyncPassSummary extends RepoSyncPassCounts { startedAt: string; finishedAt: string; } /** * The answer to "sync now" (#1293). `started` — nothing was running, a pass has begun. `queued` — a pass was * already running, and one more will follow it. Joining the running pass outright would be wrong: it read * the bound-repo list before this request, so a repo bound since would be missed; the follow-up re-reads the * list. Any number of requests during one pass coalesce into that single follow-up. `lastPass` is the most * recent pass to finish, absent until one has. */ interface RepoSyncRequestResult { status: 'started' | 'queued'; lastPass?: RepoSyncPassSummary; } interface PrVerdictRequest { owner: string; name: string; baseSha: string; headSha: string; /** `https://x-access-token:@github.com//.git`. Used for the two clones, then gone. */ cloneUrl: string; /** Repo-relative paths the PR changes, from the caller. Absent → the two trees are compared. */ changedFiles?: string[]; tone?: 'loud' | 'professional'; } interface PrVerdictResult { marker: string; body: string; base: { sha: string; nodes: number; edges: number; }; head: { sha: string; nodes: number; edges: number; }; changedFiles: number; observedBreaks: number; divergences: number; durationMs: number; } interface PrVerdictContext { /** The project's live graph. Read for OBSERVED; never written. */ liveGraph: NeatGraph; /** The project's recorded incidents, for symbol-grain divergences — the same read `/graph/divergences` does. */ incidents?: ErrorEvent[]; } type PrVerdictRunner = (req: PrVerdictRequest, ctx: PrVerdictContext) => Promise; /** * A connector implements exactly one method. Everything downstream of * `poll()` — resolving a static call site, minting the OBSERVED edge — is * shared, generic code in connectors/index.ts. */ interface ObservedConnector { readonly provider: string; poll(ctx: ConnectorContext): Promise; } /** * Everything a connector's `poll()` needs, resolved once at connector setup * — never re-derived at poll time. * * `credentials` is opaque here on purpose: its shape is entirely provider- * and profile-defined (local vs hosted — docs/contracts/connectors.md §3). * It flows through to `poll()` only. Never log it, never write it into a * node or edge, never let it reach the graph snapshot (contract §6, and the * `.env`-contents rule docs/contracts.md Rule 4 already states for local * config). */ interface ConnectorContext { projectDir: string; credentials: Record; since?: string; errorsPath?: string; project?: string; } /** * `file:line` the provider's own signal carries, when it does (rare — see * docs/connectors/README.md §Provider interface, which notes this is * "usually resolved by the mapping layer below, not here"). Reconciled onto * the EXTRACTED service-relative path by the shared fuse step the same way * an OTel span's call site is (file-awareness.md §4). */ interface ConnectorCallSite { file: string; line: number; } /** * A failure the provider already recorded, carried on a signal so the shared * pipeline can mint it as an OBSERVED incident on the repo node the failure * implicates — the incident-emitting connector shape (connectors.md §10, * ADR-185). EAS is the first user: an `ERRORED` build becomes one of these. * * It is a *pre-built* incident minus its `affectedNode`: the connector's * mapping layer fills every field here, and the shared pipeline sets the * `affectedNode` from `resolveTarget` and derives the `traceId`/`spanId` the * `ErrorEvent` shape requires from the stable `id`. So the terminal write is * an `ErrorEvent` on the incident ledger (the same store OTLP-derived * incidents use), not an `upsertObservedEdge`. */ interface ConnectorIncident { id: string; timestamp: string; service: string; errorType: string; errorMessage: string; attributes?: SpanAttributes; deployContext?: DeployContext; } /** * One provider-agnostic observation. `targetKind`/`targetName` are the * provider's own vocabulary (`'supabase-table'`/`'orders'`, * `'route'`/`'GET /users/:id'`, ...) — resolving that pair to a NEAT node id * is the one genuinely provider-specific step (README.md's pipeline * diagram), supplied to `runConnectorPoll` (connectors/index.ts) as a * `resolveTarget` callback. Everything downstream of that resolution — file * grain fusion, OBSERVED mint — is shared. */ interface ObservedSignal { targetKind: string; targetName: string; callCount: number; errorCount: number; lastObservedIso: string; callSite?: ConnectorCallSite; columns?: string[]; incident?: ConnectorIncident; deployState?: { image?: string; readyReplicas?: number; addresses?: string[]; }; } /** * A per-tick credential source (docs/contracts/connector-config.md §9, ADR-230) — the shape the poll loop's * `refreshCredentials` seam (index.ts) already consumes for the hosted profile. A credential that mints a * short-lived token (a GCP service-account key → a ~1h Google access token) resolves to one of these instead * of a fixed value: the loop calls it each tick to get that tick's credentials, and the source caches and * re-mints behind its own expiry logic. `poll()` is unchanged — the connector still receives an opaque, * already-minted `credentials` record and performs no auth handshake of its own (connectors.md §3). */ type CredentialSource = () => Promise>; /** * What a provider's target-resolution step hands back for one signal. The * generic pipeline needs both endpoints of the edge it's about to mint: * * - `serviceName` is the NEAT manifest service whose code produced the * signal — the edge's source. The shared pipeline turns it into a plain * ServiceNode id, or a FileNode id once the fuse step below resolves the * signal's callSite against it. * - `targetNodeId` is the id the provider's own mapping already resolved — * an `infraId(...)` sub-resource, a RouteNode, a ServiceNode, whatever * (see each provider's docs/connectors/.md §Fusion). * * Returning `null` skips the signal honestly: an unresolvable target never * fabricates a node or edge (the same discipline file-awareness.md §6 * states for OTel ingest). */ interface ResolvedConnectorTarget { targetNodeId: string; serviceName: string; edgeType: EdgeTypeValue; /** * Set when `targetNodeId` names an InfraNode no static extractor has (yet) * declared — the honest "observed but undeclared" fallback * (docs/contracts/connectors.md §4a, ADR-133). A provider's `resolveTarget` * has no mutation authority of its own (ADR-030), so it declares the need * here instead of creating the node itself; the generic pipeline below * calls `ensureInfraNode` before minting the edge. `targetNodeId` MUST equal * `infraId(kind, name)` when this is set. */ ensureInfraNode?: { kind: string; name: string; provider: string; }; } type ResolveConnectorTarget = (signal: ObservedSignal, ctx: ConnectorContext) => ResolvedConnectorTarget | null; interface ConnectorPollResult { signalCount: number; edgesCreated: number; edgesUpdated: number; unresolved: number; } /** * One poll cycle: fetch, map, fuse, mint. Pure with respect to `ctx` — the * caller (`startConnectorPollLoop` below, or a one-shot `neat sync`) owns * advancing `ctx.since` between calls. */ declare function runConnectorPoll(connector: ObservedConnector, ctx: ConnectorContext, graph: NeatGraph, resolveTarget: ResolveConnectorTarget): Promise; /** * One project's registered connector, ready for `daemon.ts` to poll on an * interval. Deliberately thin — no config-loading or credential-broker logic * lives here (that's provider- and profile-specific, later work per * docs/contracts/connectors.md §3); this is just the seam a daemon slot * wires a connector through. */ interface ConnectorRegistration { id?: string; connector: ObservedConnector; credentials: Record; resolveTarget: ResolveConnectorTarget; intervalMs?: number; refreshCredentials?: CredentialSource; } interface BuildApiOptions { projects?: Projects; startedAt?: number; graph?: NeatGraph; scanPath?: string; errorsPath?: string; staleEventsPath?: string; searchIndex?: SearchIndex; authToken?: string; incidentStreamToken?: string; /** Set only after the hosted substrate restores the complete errors ledger before boot. */ incidentReplayDurable?: boolean; trustProxy?: boolean; publicRead?: boolean; otlpReceiver?: () => FastifyInstance | undefined; bootstrap?: { status: (name: string) => 'bootstrapping' | 'active' | 'broken' | undefined; list: () => Array<{ name: string; status: 'bootstrapping' | 'active' | 'broken'; elapsedMs: number; }>; }; singleProject?: { name: string; path: string; }; connectorsHome?: string; repoSync?: (project: string) => (() => RepoSyncRequestResult) | undefined; prVerdict?: PrVerdictRunner; runPoll?: typeof runConnectorPoll; } declare function buildApi(opts: BuildApiOptions): Promise; interface ParsedSpan { service: string; resourceServiceNamePresent?: boolean; traceId: string; spanId: string; parentSpanId?: string; name: string; kind?: number; startTimeUnixNano: string; endTimeUnixNano: string; startTimeIso?: string; durationNanos: bigint; env: string; attributes: Record; dbSystem?: string; dbName?: string; dbCollection?: string; dbTable?: string; dbColumns?: string[]; httpRoute?: string; httpMethod?: string; messagingSystem?: string; messagingDestination?: string; graphqlOperationName?: string; graphqlOperationType?: string; rpcSystem?: string; rpcService?: string; rpcMethod?: string; peerService?: string; peerHostname?: string; websocketChannel?: string; statusCode?: number; errorMessage?: string; exception?: { type?: string; message?: string; stacktrace?: string; }; flagEvaluations?: FlagEvaluation[]; } interface FlagEvaluation { key: string; variant?: string; provider?: string; reason?: string; } type AttributeValue = string | number | boolean | bigint | string[] | number[] | boolean[] | null; type SpanHandler = (span: ParsedSpan) => void | Promise; type ProjectSpanHandler = (project: string, span: ParsedSpan) => void | Promise; interface BuildOtelReceiverOptions { onSpan: SpanHandler; onErrorSpanSync?: (span: ParsedSpan) => Promise; onProjectSpan?: ProjectSpanHandler; onProjectErrorSpanSync?: (project: string, span: ParsedSpan) => Promise; isProjectRegistered?: (project: string) => boolean; classifyBareRoutability?: (spans: ParsedSpan[]) => { rejected: number; message?: string; }; bodyLimit?: number; authToken?: string; trustProxy?: boolean; } interface OtlpKeyValue { key: string; value?: OtlpAnyValue; } interface OtlpAnyValue { stringValue?: string; intValue?: string | number; doubleValue?: number; boolValue?: boolean; arrayValue?: { values?: OtlpAnyValue[]; }; } interface OtlpStatus { code?: number; message?: string; } interface OtlpEvent { name?: string; timeUnixNano?: string; attributes?: OtlpKeyValue[]; } interface OtlpSpan { traceId?: string; spanId?: string; parentSpanId?: string; name?: string; kind?: number; startTimeUnixNano?: string; endTimeUnixNano?: string; attributes?: OtlpKeyValue[]; events?: OtlpEvent[]; status?: OtlpStatus; } interface OtlpScopeSpans { spans?: OtlpSpan[]; } interface OtlpResourceSpans { resource?: { attributes?: OtlpKeyValue[]; }; scopeSpans?: OtlpScopeSpans[]; } interface OtlpTracesRequest { resourceSpans?: OtlpResourceSpans[]; } declare function parseOtlpRequest(body: OtlpTracesRequest): ParsedSpan[]; declare function buildOtelReceiver(opts: BuildOtelReceiverOptions): Promise Promise; }>; declare function logSpanHandler(span: ParsedSpan): void; interface BuildOtelGrpcReceiverOptions { onSpan: SpanHandler; authToken?: string; trustProxy?: boolean; } interface OtelGrpcReceiver { address: string; stop: () => Promise; } declare function startOtelGrpcReceiver(opts: BuildOtelGrpcReceiverOptions & { host?: string; port?: number; }): Promise; interface IngestContext { graph: NeatGraph; errorsPath: string; scanPath?: string; project?: string; now?: () => number; writeErrorEventInline?: boolean; onPolicyTrigger?: (graph: NeatGraph) => Promise | void; burstState?: Map; } declare function thresholdForEdgeType(edgeType: string, overrides?: Record): number; interface BurstState { count: number; firstTs: string; lastTs: string; lastMs: number; codes: Map; } declare function stitchTrace(graph: NeatGraph, sourceServiceId: string, ts: string): void; declare function handleSpan(ctx: IngestContext, span: ParsedSpan): Promise; declare function makeSpanHandler(ctx: IngestContext): (span: ParsedSpan) => Promise; interface MarkStaleOptions { thresholds?: Record; now?: number; staleEventsPath?: string; project?: string; } declare function markStaleEdges(graph: NeatGraph, options?: MarkStaleOptions): Promise<{ count: number; events: StaleEvent[]; }>; declare function readStaleEvents(staleEventsPath: string): Promise; interface StalenessLoopOptions { thresholds?: Record; intervalMs?: number; staleEventsPath?: string; project?: string; onPolicyTrigger?: (graph: NeatGraph) => Promise | void; onReconcile?: (graph: NeatGraph) => Promise | void; } declare function startStalenessLoop(graph: NeatGraph, options?: StalenessLoopOptions): () => void; declare function readErrorEvents(errorsPath: string, opts?: { limit?: number; }): Promise; declare function confidenceForEdge(edge: GraphEdge, now?: number): number; declare function getBlastRadius(graph: NeatGraph, nodeId: string, maxDepth?: number): BlastRadiusResult; declare function getRootCause(graph: NeatGraph, errorNodeId: string, errorEvent?: ErrorEvent, incidents?: ErrorEvent[], opts?: { navigation?: boolean; now?: number; }): RootCauseResult | null; interface PersistedNodeEntry { key?: string; attributes?: Record; } interface PersistedEdgeEntry { key?: string; source?: string; target?: string; attributes?: Record; } interface PersistedSnapshot { schemaVersion?: number; exportedAt?: string; graph?: { nodes?: PersistedNodeEntry[]; edges?: PersistedEdgeEntry[]; }; } interface GraphDiff { base: { exportedAt?: string; }; current: { exportedAt: string; }; added: { nodes: GraphNode[]; edges: GraphEdge[]; }; removed: { nodes: GraphNode[]; edges: GraphEdge[]; }; changed: { nodes: { id: string; before: GraphNode; after: GraphNode; }[]; edges: { id: string; before: GraphEdge; after: GraphEdge; }[]; }; } declare function loadSnapshotForDiff(target: string): Promise; declare function computeGraphDiff(liveGraph: NeatGraph, baseSnapshot: PersistedSnapshot, currentExportedAt?: string): GraphDiff; /** * Multi-project daemon (ADR-049). * * Single long-lived process watching every project in the machine-level registry. * Per-project graph isolation: each registered project owns its own * `MultiDirectedGraph` slot keyed by name (ADR-026), and a failure during * one project's bootstrap is logged + marked `broken` without taking down * the rest of the daemon. * * MVP scope (v0.2.5): * - Read registry; refuse to boot when it's missing. * - Write PID at `~/.neat/neatd.pid` for external supervisors. * - Per project: load any existing snapshot, run initial extraction, * start a per-project persist loop. * - SIGHUP triggers a reload — re-reads the registry, picks up new * projects, drops removed ones, leaves untouched ones in place. * - Provide `routeSpanToProject(serviceName, projects)` for OTel ingest * to dispatch by `service.name` across registered projects, falling * back to `default` for unknown services per ADR-033. * * Out of MVP scope (deferred): * - Live OTel listener wiring per project — daemon exposes the routing * primitive; the actual receiver attachment lands alongside v0.2.6. * - Policy reload on `policy.json` mtime — `startWatch` already does this * per-project; the daemon-level loop reuses that machinery in a follow-up. * - Auto-restart on crash. PID file is the supervisor handoff. */ interface DaemonPorts { rest: number; otlp: number; web: number; } interface DaemonRecord { project: string; projectPath: string; pid: number; status: 'running' | 'stopped'; ports: DaemonPorts; startedAt: string; neatVersion: string; } interface DaemonOptions { neatHome?: string; project?: string; projectPath?: string; webPort?: number; restPort?: number; otlpPort?: number; host?: string; bindListeners?: boolean; connectors?: ConnectorRegistration[]; } interface ProjectSlot { entry: RegistryEntry; graph: NeatGraph; outPath: string; paths: ProjectPaths; stopPersist: () => void; stopStaleness: () => void; stopConnectors: () => void; stopHostedConnectors: () => void; stopK8sSubstrate: () => void; stopFlagStores: () => void; stopRepoSync: () => void; syncRepos?: () => RepoSyncRequestResult; detachEvents: () => void; status: 'active' | 'broken'; errorReason?: string; } type BootstrapPhase = 'bootstrapping' | 'active' | 'broken'; interface BootstrapTracker { status: (name: string) => BootstrapPhase | undefined; list: () => Array<{ name: string; status: BootstrapPhase; elapsedMs: number; }>; } interface DaemonHandle { slots: Map; reload: () => Promise; stop: () => Promise; pidPath: string; restAddress: string; otlpAddress: string; bootstrap: BootstrapTracker; initialBootstrap: Promise; daemonRecord: DaemonRecord | null; neatHome: string; } /** * Resolve which project's graph an OTel span belongs to. Looks up the * `service.name` against the registry and returns the matching project's * name, or `DEFAULT_PROJECT` for unknown services so the FrontierNode * auto-creation flow keeps working per ADR-033. * * Pure function. Daemon callers pass a snapshot of the registry to avoid * per-span fs reads. * * Matching order (ADR-072 — real-world `service.name` rarely equals project * name; monorepos publish per-package names like `brief-api` under a * project named `brief`): * * 1. Exact: `entry.name === serviceName`. * 2. Hyphen/underscore-separated prefix: `entry.name` is a leading token * of `serviceName` (`brief` matches `brief-api`, `brief_worker`). * Longest-match wins so `brief-api` beats `brief` when both are * registered. * 3. Containment as a separator-delimited token (`api` inside * `brief-api-staging`). * * Routing eligibility (ADR-071): * - `active` matches at every pass (the steady-state path). * - `broken` also matches — the daemon needs the span to reach the broken * slot so the ingest-time auto-recover path can attempt a bootstrap and * lift the project back to `active`. The router only chooses the target; * whether the span actually lands is the ingest handler's decision. * - `paused` is intentionally not routed; the operator paused it on * purpose, so the span falls through to the default-project flow. * * Falls back to `DEFAULT_PROJECT` when nothing matches. */ declare function routeSpanToProject(serviceName: string | undefined, projects: ReadonlyArray): string; declare function startDaemon(opts?: DaemonOptions): Promise; export { type BuildApiOptions, type BuildOtelGrpcReceiverOptions, type BuildOtelReceiverOptions, type CompatPair, type CompatibilityResult, type DaemonHandle, type DaemonOptions, type ExtractResult, type GraphDiff, type IngestContext, type MarkStaleOptions, type NeatGraph, type OtelGrpcReceiver, type OtlpTracesRequest, type ParsedSpan, type PersistedSnapshot, type ProjectSlot, type SpanHandler, type StalenessLoopOptions, buildApi, buildOtelReceiver, checkCompatibility, compatPairs, computeGraphDiff, confidenceForEdge, extractFromDirectory, getBlastRadius, getGraph, getRootCause, handleSpan, loadGraphFromDisk, loadSnapshotForDiff, logSpanHandler, makeSpanHandler, markStaleEdges, parseOtlpRequest, readErrorEvents, readStaleEvents, resetGraph, routeSpanToProject, saveGraphToDisk, startDaemon, startOtelGrpcReceiver, startPersistLoop, startStalenessLoop, stitchTrace, thresholdForEdgeType };