/** * DC-5 — obligation change-vs-addition classification + paired/scoped negative * test-spec gate (single source). * * Two latent failure modes this module closes: * * CE-013 (render-only misclassification): the paired-obligation gate used to * force EVERY testable obligation to carry a positive+negative pair, with no * notion of whether the obligation changes prior behavior or adds new * behavior. A pure addition has no behavior to regress, so pairing it is * burden without signal — and, worse, the "classification" lived only in the * prompt prose (render-only), never as a recorded, checkable verdict. This * module classifies each obligation *deterministically first* (does it touch * a symbol/file that already exists?) and records the verdict on the ledger, * so an LLM may confirm/override but the result is never silent. * * CE-006 (unscoped repo-wide-grep negative): a behavior-CHANGE obligation's * negative half could be satisfied by an assertion that greps the whole repo * ("no file anywhere contains X") — which rots immediately and proves nothing * about the changed symbol. The negative must be SCOPED to the changed * symbol/file. The scope check is a structural PREDICATE over the assertion * (it must name an anchor AND must not be an unscoped global scan), not a * keyword match — keyword matching alone is exactly what let the unscoped * negative through. * * Single-source invariant (mirrors `derive.ts`): the deriver, the test-plan * derivation gate (`validatePairedObligations`), and the `mergeImplementResults` * verify gate ALL classify and pair through the helpers here. No parallel logic. */ import { type ObligationChangeClassification } from "audit-tools/shared"; /** Extract candidate symbol/file tokens from free text (lowercased, de-noised). */ export declare function extractSymbolTokens(text: string): string[]; /** * Build the baseline corpus of pre-existing symbol/file tokens from the finalized * module contracts. A symbol present here already exists, so an obligation that * references it is *changing* existing behavior rather than adding new behavior. * * The corpus is drawn from the declared interface surface (inputs / outputs / * side_effects / validation_boundary) plus each module name — the things the * contract says already exist at the seam. */ export declare function buildBaselineSymbolCorpus(finalizedModuleContracts: unknown): Set; /** * Classify one obligation change-vs-addition deterministically. * * Heuristic: the obligation *touches an existing symbol* when any symbol token in * its description is present in the baseline corpus of pre-existing symbols. Such * an obligation is a behavior CHANGE; otherwise it is a pure ADDITION. * * The matched tokens become `touched_symbols` — the scope anchors a paired * negative assertion must name. This is the deterministic FIRST pass; an LLM may * confirm or override it via `applyLlmConfirmation`, and the override is recorded. */ export declare function classifyObligationChange(description: string, baselineSymbols: Set): ObligationChangeClassification; /** * A negative assertion for a behavior-CHANGE obligation is SCOPED when it names * at least one of the change's anchors (the touched symbol/file) AND does not * perform an affirmative unscoped repo-wide scan. Naming an anchor is necessary * but not sufficient: an assertion that both names the symbol and says "grep the * whole repo" is still rejected, because the scan, not the symbol, is what it * actually checks. A merely DESCRIPTIVE mention of a global scan ("scoped to X, * not an unscoped repo-wide check") does NOT disqualify — the veto reads the * assertion's action, not the literal words, so a host need not euphemise (CE-006). * * Returns true when the assertion is acceptably scoped to the change. */ export declare function negativeAssertionIsScoped(assertion: string, anchors: readonly string[]): boolean; /** Phrases that mark a negative/failure assertion (paired-obligation half). */ export declare const NEGATIVE_ASSERTION_PATTERN: RegExp; /** Phrases that mark a positive/satisfied assertion (paired-obligation half). */ export declare const POSITIVE_ASSERTION_PATTERN: RegExp; /** The polarity an assertion declares, accounting for an authoritative label. */ export type AssertionPolarity = "positive" | "negative" | "both" | "none"; /** * Classify one assertion's polarity. An explicit `POSITIVE:` / `NEGATIVE:` label * is authoritative and skips the keyword fallback (so "POSITIVE: must not exceed * N" counts only as positive). Unlabeled assertions fall through to the keyword * regexes, which may match both polarities — run against an identifier-masked copy * so a polarity word embedded in a cited id (e.g. `fail` in `OBL-AUTH-fail-session`) * doesn't misclassify the assertion. */ export declare function assertionPolarity(assertion: string): AssertionPolarity; /** * The pairing verdict for a single CHANGE obligation against its covering test * specs' assertions. `ok` is true only when BOTH a positive assertion and a * SCOPED negative assertion are present. Each reason is a short, stable code the * callers turn into a ValidationIssue (test-plan gate) or a block reason (verify * gate) — single-sourced so both gates report the same failures. */ export interface PairingVerdict { ok: boolean; hasPositive: boolean; hasNegative: boolean; /** A negative was present but none of them were scoped to the change. */ negativeUnscoped: boolean; } /** * Evaluate the paired positive+scoped-negative requirement for a behavior-CHANGE * obligation. `anchors` are the change's scope anchors (touched symbols/file). * * - A positive half is any assertion with positive polarity. * - A negative half counts ONLY when it is scoped to the change (CE-006): an * unscoped repo-wide negative does not satisfy the negative requirement. * `negativeUnscoped` is reported when the sole negative(s) failed scoping, so * the diagnostic distinguishes "no negative at all" from "negative not scoped". */ export declare function evaluatePairing(assertions: readonly string[], anchors: readonly string[]): PairingVerdict; /** * Read an obligation's `change_classification` from a raw, untrusted payload. * Returns the narrowed classification, or `undefined` only when the field is * genuinely ABSENT — a truly unclassified obligation (e.g. a structural one), which * the gates already treat as a CHANGE (fail-closed) at the consumer. * * A classification that is PRESENT but CORRUPT (not a record, or an unrecognized * `change_kind`) is read as a fail-closed CHANGE rather than `undefined` (INV-IR-4): * a dropped or garbled classification can never relax the paired-test requirement * and can never let incremental item-scoping (contract-incremental-reconvergence) * carry a changed item forward as if it were an unchanged addition. */ export declare function readObligationChangeClassification(obligation: unknown): ObligationChangeClassification | undefined; /** * The scope anchors a paired negative must name for one obligation. Prefers the * classification's recorded `touched_symbols`; falls back to the obligation id * plus any symbol tokens in its description when the classification carries none * (e.g. an unclassified obligation treated as a fail-closed change). Always * non-empty when the id is a real id, so a fail-closed change still has a concrete * anchor to scope against rather than vacuously accepting any negative. */ export declare function obligationScopeAnchors(obligationId: string, description: string, classification: ObligationChangeClassification | undefined): string[]; //# sourceMappingURL=changeClassification.d.ts.map