/** * Repository-scoped full-analysis ownership. * * Only one full analysis may own a repository at a time. Duplicate analyses were * observed across distinct processes and frontends (CLI, MCP, daemon bootstrap, * Pi), so process-local promise deduplication cannot fix them — the guarantee has * to be cross-process (change `harden-spec-workflow-lifecycle`, decision 4da0a04f). * * This is one thin binding of the repository's single advisory-lock loop, * not a second locking mechanism. It supplies policy values and nothing else: a * structured JSON payload, a dead-PID-plus-stale-heartbeat staleness predicate, * `report` contention, `bestEffortAfterMaxWait: false`, and — under `--wait` — an * unbounded wait, since an attach ends when the owner finishes or dies. * * Ownership cannot simply reuse `acquireAnalysisLock`: it uses a different path, * reports its structured holder to attach-capable callers, and spans the whole * analysis run rather than only the publication write set. Both bindings fail * closed and wait indefinitely when their correctness guarantee requires it. * * Runtime lock and progress state deliberately live OUTSIDE the analysis * artifacts: heartbeat timestamps are not deterministic evidence and must never * enter an artifact digest. */ /** Runtime state directory. Sibling of the analysis output, never inside it. */ export declare const RUNTIME_SUBDIR = "runtime"; export declare const PROGRESS_FILE = "analysis-progress.json"; /** Owner refresh cadence. The CLI heartbeat runs at twice this interval. */ export declare const PROGRESS_INTERVAL_MS = 15000; /** Maximum silence a user should see during an unchanged long phase. */ export declare const VISIBLE_HEARTBEAT_MS = 30000; export interface AnalysisOwnerPayload { /** Canonical repository path this ownership is scoped to. */ repository: string; pid: number; /** ISO timestamp the analysis started. */ startedAt: string; /** ISO timestamp of the owner's last refresh. */ heartbeatAt: string; /** Coarse stage name, e.g. `parsing`, `call-graph`, `artifacts`. */ stage: string; /** Absolute path of the progress sidecar this owner writes. */ progressPath: string; } export interface AnalysisProgress { stage: string; /** 0–100 when known; `null` while a stage cannot estimate itself. */ percent: number | null; detail?: string; updatedAt: string; } export type AnalysisOwnership = { state: 'owned'; payload: AnalysisOwnerPayload; /** * Milliseconds spent waiting on a PREVIOUS owner before acquiring (`--wait` * only; `0` otherwise). Non-zero means another process just finished a full * analysis, so the caller should re-check freshness before redoing the work * it was waiting for. */ waitedMs: number; /** Publish a new stage/progress and refresh the heartbeat. */ update: (stage: string, progress?: Omit) => Promise; /** Release ownership and remove the progress sidecar. */ release: () => Promise; } | { state: 'in-progress'; /** The live owner, when its payload could be parsed. */ owner: AnalysisOwnerPayload | null; /** Age of the owner's last heartbeat, in milliseconds. */ heartbeatAgeMs: number; /** Milliseconds since the owner started, when its payload was readable. */ elapsedMs: number | null; progressPath: string | null; }; export declare function runtimeDirOf(analysisDir: string): string; export declare function progressPathOf(analysisDir: string): string; /** Is a PID alive? `kill(pid, 0)` throws ESRCH for a dead process. */ export declare function isProcessAlive(pid: number): boolean; /** * Reclamation predicate: BOTH the owner PID must be dead AND its heartbeat stale. * * Elapsed time alone is never sufficient — a long analysis is not an abandoned * one — and a dead PID alone is not either, because PID reuse can make a live * unrelated process look like the owner. Requiring both, plus a repository match, * is what keeps reclamation from stealing a healthy run. * * PID reuse deliberately fails closed. A stale heartbeat does not prove that the * process currently carrying the PID is unrelated, so elapsed time alone never * authorizes a second full analysis. An ambiguous lock requires operator cleanup. */ export declare function isOwnershipStale(mtimeMs: number, contents: string, repository: string): boolean; /** Legacy filename retained for compatibility checks. It is never written or executed. */ export declare const WATCHDOG_FILE = ".analysis-watchdog.cjs"; /** * Try to become the sole owner of a full analysis for `repository`. * * Returns `owned` with a handle, or `in-progress` with the live owner's metadata. * `wait` mode polls until ownership is free instead of reporting — used only by * `analyze --wait`, which is attaching deliberately. An attach that waited reports * `waitedMs > 0`, which is how the caller knows the work may already be done. */ export declare function acquireAnalysisOwnership(repository: string, analysisDir: string, options?: { wait?: boolean; stage?: string; heartbeatIntervalMs?: number; }): Promise; /** Read the current progress sidecar, or `null` when no analysis is publishing. */ export declare function readAnalysisProgress(analysisDir: string): Promise; /** * Non-blocking ownership read for status/preflight. * * Never acquires, steals, or waits. Returns `null` when no live analysis owns the * repository — including when a stale lock is present, since a crashed holder is * not an analysis in progress. */ export declare function readAnalysisOwner(repository: string, analysisDir: string): Promise<{ owner: AnalysisOwnerPayload | null; heartbeatAgeMs: number; elapsedMs: number | null; } | null>; //# sourceMappingURL=analysis-ownership.d.ts.map