/** * Slice 2 of the self-scaling remediation pipeline (design of record: * `spec/self-scaling-pipeline-design.md`): the ONE shared risk/complexity signal * that BOTH self-scaling dials (adversarial depth, phase granularity) will read. * * This module *produces and carries* the signal; the two dials that consume it * are now wired in `src/remediate/steps/contractPipeline.ts` — the adversarial-depth * dial (`adversarialDepthForTier`, T1 slice 4a) and the round-trip granularity-collapse * dial (`roundTripGranularityForTier`, T1 slice 4b) both key off the (possibly escalated) * tier, so the signal actively shapes pipeline behavior. This module remains the single * source both dials read. * * Hard constraints (from the spec): * - Computed CHEAPLY at intake from data available at the routing point only: * affected-files + a deterministic, configurable path-risk pattern set + the * run intent (goals). It must NOT depend on any pipeline-internal output (the * lap-3 circularity — `changeClassification` consumes finalized contracts that * do not exist at the routing point; a routing signal cannot be a pipeline * output). * - Fail-CLOSED: anything unevaluable rounds toward MORE scrutiny, never less. * - Re-assessable as the run produces evidence (escalate-on-evidence), and the * re-assessment may only RAISE the tier, never lower it. */ import type { Finding } from "audit-tools/shared"; export declare const INTAKE_RISK_SIGNAL_SCHEMA_VERSION: "remediate-code-intake-risk-signal/v1alpha1"; /** * Ordered risk tiers. The two dials map onto this: * - depth dial: low → inline light self-check; high → full independent passes * - granularity dial: low → coarse / collapsed round-trips; high → fine-grained * The floor is `low`, never "off" — nothing reaches zero adversarial scrutiny. */ export type RiskTier = "low" | "medium" | "high"; /** The higher (more scrutiny) of two tiers. Fail-closed combinator. */ export declare function maxRiskTier(a: RiskTier, b: RiskTier): RiskTier; /** * Adversarial-depth dial (T1 slice 3). The depth at which the critique / * counterexample phases run, derived from the risk tier: * - `light` — an inline lightweight self-check, no independent sub-agent; * - `full` — full independent critique + counterexample (the earned cost). * The floor is `light`, never "off": even a low-risk change gets a real (if * lightweight) adversarial pass, because remediation legitimately catches * upstream (audit) errors. */ export type AdversarialDepth = "light" | "full"; /** * Map a risk tier to its adversarial depth. Only `low` earns the light inline * self-check; `medium`/`high` get the full independent review. Fail-safe toward * more scrutiny: an absent/unknown tier resolves to `full`. */ export declare function adversarialDepthForTier(tier: RiskTier | undefined): AdversarialDepth; /** * Round-trip granularity dial (T1 slice 4b). The granularity at which the * authoring phases are gated: * - `collapsed` — coherent authoring acts fold into ONE round-trip producing * several artifacts (the ceremony saving — fewer gated steps); * - `fine` — every phase its own gated round-trip (failure-isolation + * per-phase validation, earned only when there is real complexity to isolate). * Only `low` collapses; `medium`/`high` stay fine-grained. Fail-safe toward more * isolation: an absent/unknown tier resolves to `fine`. This composes with * escalate-on-evidence: a run begins collapsed (optimistic-start) and the moment * decomposition raises the tier (slice 4a), the *remaining* phases re-derive * fine-grained — the dial is read per next-step, never frozen at run start. */ export type RoundTripGranularity = "collapsed" | "fine"; /** Map a risk tier to its round-trip granularity. Only `low` collapses. */ export declare function roundTripGranularityForTier(tier: RiskTier | undefined): RoundTripGranularity; /** A single deterministic path-risk family: a repo path family that warrants scrutiny. */ export interface PathRiskPattern { /** Stable label, surfaced in the rationale (e.g. "concurrency"). */ label: string; /** Matched against the normalized (forward-slash) repo-relative path. */ pattern: RegExp; } /** * Default deterministic path-risk pattern set. These are the subsystems where a * change is correctness-sensitive (the spec's "concurrency / dispatch / merge / * state / quota / shared-core = risky"). Configurable: callers may pass their own * set via {@link RiskSignalConfig.pathRiskPatterns}. * * Patterns are intentionally broad — fail-closed means a false "risky" match only * costs extra scrutiny, never silently under-scrutinizes. */ export declare const DEFAULT_PATH_RISK_PATTERNS: readonly PathRiskPattern[]; /** * Default intent-risk keywords. A goal/brief mentioning one of these signals * inherently risky work regardless of which files are touched (e.g. a security or * concurrency goal). Matched case-insensitively against the goals text. */ export declare const DEFAULT_INTENT_RISK_KEYWORDS: readonly string[]; /** File-count thresholds (inclusive lower bounds) that bump the tier. Configurable. */ export interface RiskSignalConfig { pathRiskPatterns?: readonly PathRiskPattern[]; intentRiskKeywords?: readonly string[]; /** affected-file count at/above which the tier is at least `medium`. */ mediumFileCount?: number; /** affected-file count at/above which the tier is at least `high`. */ highFileCount?: number; } export interface IntakeRiskSignalInputs { /** Distinct affected-file count considered. */ file_count: number; /** Path-risk family labels that matched at least one affected file. */ matched_path_risks: string[]; /** Intent-risk keywords that matched the goals text. */ matched_intent_risks: string[]; } export interface IntakeRiskSignal { schema_version: typeof INTAKE_RISK_SIGNAL_SCHEMA_VERSION; tier: RiskTier; /** Human-readable reasons the tier landed where it did (ordered, append-only on escalation). */ rationale: string[]; inputs: IntakeRiskSignalInputs; /** * Whether this signal has been raised by escalate-on-evidence since intake. * Lets a reader tell an intake assessment apart from an evidence-raised one. */ escalated: boolean; } export interface ComputeIntakeRiskInput { /** Best-available affected-file list at intake (repo-relative paths). */ affectedFiles: readonly string[]; /** Run intent — the intake goals (and any brief text the caller folds in). */ goals: readonly string[]; config?: RiskSignalConfig; } /** * Compute the shared intake risk/complexity signal. Pure + deterministic; uses * only intake-available data. Fail-closed: an unevaluable input (no files AND no * goals to assess) rounds up to `high`. */ export declare function computeIntakeRiskSignal(input: ComputeIntakeRiskInput): IntakeRiskSignal; /** Evidence that may raise the run's risk tier as the pipeline produces it. */ export interface RiskEscalationEvidence { /** The tier this evidence justifies (the signal is raised to at least this). */ tier: RiskTier; /** Why — appended to the signal's rationale (e.g. "decomposition surfaced a cross-module seam"). */ reason: string; } /** * Escalate-on-evidence: re-assess the signal as the run produces evidence the * work is harder than the intake assessment assumed (a cross-module seam, a light * self-check flag, a verify failure). May only RAISE the tier — a wrong call can * cost extra scrutiny, never silently relax it. Returns the same object reference * unchanged when the evidence does not raise the tier (no spurious rewrite). */ export declare function escalateRiskSignal(current: IntakeRiskSignal, evidence: RiskEscalationEvidence): IntakeRiskSignal; export interface DecompositionEvidenceInput { /** Number of modules the decomposition produced. */ moduleCount: number; /** Union of every module's declared file_scope (repo-relative paths). */ fileScopes: readonly string[]; config?: RiskSignalConfig; } /** * Derive escalate-on-evidence from a COMPLETED decomposition (self-scaling * pipeline, slice 4 — optimistic-start). A run begins at the cheap intake tier; * once decomposition reveals the work's actual shape, raise the tier where the * evidence demands it so the adversarial-depth dial (and downstream granularity) * tighten: * - any module file_scope touches a path-risk subsystem ⇒ `high` (a * correctness-sensitive subsystem the intake affected-file list may not have * surfaced); * - >1 module ⇒ cross-module seams exist ⇒ at least `medium` (real complexity * to isolate — exactly when the fine-grained per-phase gates earn their cost). * Returns undefined when the decomposition reveals no new risk. Pure + * deterministic; the {@link escalateRiskSignal} combinator enforces raise-only. */ export declare function decompositionRiskEvidence(input: DecompositionEvidenceInput): RiskEscalationEvidence | undefined; /** * Max findings a `low`-tier (lean) run will take — "a handful". Above this, the * coordination risk of the batch warrants at least `medium` (the full pipeline). */ export declare const MAX_FAST_PATH_FINDINGS = 5; /** * Max DISTINCT affected files across the approved set for a `low`-tier run. A small * footprint is the primary structural proxy for "no broad cross-module ripple". * (Aligned with the intake breadth threshold `DEFAULT_MEDIUM_FILE_COUNT = 6`, so >5 * files is `medium` by both the intake signal and this finding-level check.) */ export declare const MAX_FAST_PATH_FILES = 5; /** Distinct affected-file paths across a finding set. */ export declare function distinctAffectedFiles(findings: Finding[]): string[]; /** * Derive escalate-on-evidence from the APPROVED finding set's finding-level quality / * coupling signals — the dimension the intake path/breadth/intent signal does not * capture. Any signal that carries design-level doubt raises the tier so the run leaves * the `low` (lean) tier for the full contract pipeline: * - a `systemic` finding, or an `architecture`-lens finding ⇒ `high` (design-level); * - an ungrounded finding, a below-high-confidence finding, a finding coupled to * related findings (seam risk), more than {@link MAX_FAST_PATH_FINDINGS} findings, * or more than {@link MAX_FAST_PATH_FILES} distinct affected files ⇒ `medium`. * Returns undefined when the set is a clean handful of grounded, high-confidence, * localized, non-coupled fixes (the tier stays `low` ⇒ lean). Pure + deterministic; * the {@link escalateRiskSignal} combinator enforces raise-only. Mirrors * {@link decompositionRiskEvidence}'s shape — evidence, never a direct tier write. */ export declare function findingRiskEvidence(findings: Finding[]): RiskEscalationEvidence | undefined; /** Read the persisted intake risk signal, or undefined when none is recorded. */ export declare function readIntakeRiskSignal(artifactsDir: string): Promise; /** Persist (overwrite) the intake risk signal. */ export declare function writeIntakeRiskSignal(artifactsDir: string, signal: IntakeRiskSignal): Promise; /** * Compute-and-persist the intake risk signal the FIRST time only. Idempotent * across the many `next-step` calls of a run: once recorded it is never * recomputed, so an escalate-on-evidence raise (which rewrites the file with a * higher tier) is never clobbered by a later intake-only recompute. Returns the * effective signal (existing or freshly computed). * * The inputs are supplied by a lazy provider so any cost of gathering them (e.g. * reading the audit report to union per-finding affected files) is paid only on * the single run that actually computes, never on every subsequent next-step. */ export declare function ensureIntakeRiskSignal(artifactsDir: string, resolveInput: () => ComputeIntakeRiskInput | Promise): Promise; export declare const LEAN_LIGHT_REVIEW_SCHEMA_VERSION: "remediate-code-lean-light-review/v1alpha1"; export type LeanLightReviewDisposition = "clear" | "escalate"; /** * Interpret a host-written light-review verdict, fail-safe toward escalation: * any malformed / ambiguous verdict, or an `escalate` with no stated concern, * routes to the full pipeline. The floor must never silently pass — when in * doubt, escalate (a wrong call costs extra pipeline work, never skipped review). */ export declare function interpretLeanLightReviewVerdict(raw: unknown): { disposition: LeanLightReviewDisposition; concerns: string[]; }; //# sourceMappingURL=riskSignal.d.ts.map