/** * O2 ↔ F1 residual-risk fix for judge-accepted CE-011: element-staleness for an * already-ingested logical result must compare a FRESHLY-COMPUTED current * `contentKey` (derived from the LIVE `task_content_signature` at advance time) * against a prior BASELINE `contentKey` — and that baseline must live OUTSIDE the * immutable per-record ledger instance. * * Why not read the contentKey off the ledger record? The append-only ledger * (INV-O2-1) keys records on `idempotency_key`, which is signature-STABLE: a * benign content edit (C1 → C2) does NOT move it, so idempotent re-ingest of the * already-ingested result stays a no-op (no duplicate record is appended). If * staleness ALSO read its prior contentKey from that same immutable record, the * record would still carry C1 and a benign edit could never fire staleness — the * exact CE-011 residual hole. So the baseline contentKey is persisted in a * SEPARATE store (`artifact_metadata.result_baselines`, keyed by * `idempotency_key`) that the staleness gate updates as the live signature moves, * while the ledger record itself is never mutated. Append-only ledger invariants * are therefore untouched. * * The comparison driver is the seam's `contentKey` (signature-SENSITIVE), so: * live C2 != baseline C1 ⟹ staleness FIRES, * while the seam's signature-STABLE `idempotencyKey` keeps idempotent re-ingest a * no-op. The two are reconciled by construction (see src/shared/contentKey.ts). */ import { type ResultEmitSource } from "audit-tools/shared"; import { type ArtifactMetadataManifest } from "../types/artifactMetadata.js"; import type { AuditResult, AuditTask } from "../types.js"; /** * The baseline store — the per-logical-result contentKey snapshot, keyed by the * signature-STABLE `idempotency_key`. Persisted in `artifact_metadata` OUTSIDE * the per-record ledger, so updating a baseline never mutates an immutable ledger * record (INV-O2-1 untouched). */ export type ResultBaselineStore = Record; /** The live coordinate + signature needed to freshly compute the current keys. */ export interface LiveResultKeyInput { unit_id: string; lens: string; pass_id: string; /** * The result's task_id — its file-split sibling discriminator is derived from * it (N-IDEMPOTENCY) so split siblings of one {unit_id, lens, pass_id} * coordinate freshly compute DISTINCT live keys, matching what * `stampLedgerKeys` stamped. Omitted ⇒ lone-base key (no split component). */ task_id?: string; /** Tool-owned emit source (base | deepening | steward | redispatch). */ source: ResultEmitSource; /** Required when `source === 'redispatch'`. */ attempt?: number; /** * The LIVE task-content signature derived from the current task content at * advance time — NEVER a value read off a stored ledger record. */ task_content_signature: string; } /** * Freshly derive the {idempotency_key, content_key} pair for a live result from * the seam — both computed from the live `task_content_signature` at advance * time. `idempotency_key` is the baseline-store key (signature-STABLE); * `content_key` is the staleness driver (signature-SENSITIVE). */ export declare function deriveLiveResultKeys(input: LiveResultKeyInput): { idempotency_key: string; content_key: string; }; /** * Is the already-ingested logical result stale, given its LIVE keys? Stale iff a * baseline exists for the idempotency_key AND the freshly-computed live * content_key differs from it. A result with no recorded baseline is NOT stale — * it has simply never been compared (the first ingest establishes the baseline). * * The caller supplies live keys via `deriveLiveResultKeys` (freshly computed from * the live signature) — this function NEVER reads a contentKey off a ledger * record. */ export declare function isResultStaleAgainstBaseline(baselines: ResultBaselineStore | undefined, liveKeys: { idempotency_key: string; content_key: string; }): boolean; /** * Return an updated baseline store with this logical result's baseline set to its * live content_key. Pure: returns a new object, never mutates the input — so a * caller persists the result through the normal artifact_metadata write path. * Idempotent: recording the same content_key again yields an equal store. */ export declare function recordResultBaseline(baselines: ResultBaselineStore | undefined, liveKeys: { idempotency_key: string; content_key: string; }): ResultBaselineStore; /** * Metadata-migration fail-safe (CE-007). An on-disk manifest is recognized as * F1-current ONLY when it carries `metadata_schema_version >= METADATA_SCHEMA_VERSION`. * An absent/older tag (a pre-F1, whole-artifact-only manifest) is NOT recognized, * so its still-matching whole-artifact hashes must NEVER be trusted to skip a * changed element — every element is treated as stale (all re-derived). Pure * predicate; never throws. */ export declare function isMetadataManifestCurrent(manifest: ArtifactMetadataManifest | undefined): boolean; /** * The LIVE task-content signature for a result's owning task, derived at advance * time from the CURRENT task content (never off a stored ledger record). Only the * fields that define the audited material feed the signature — the identity * (`task_id`), lifecycle (`status`, `completed_at`, `completion_reason`), and * planning metadata (`token_estimate`, `risk_estimate`, `tags`) * are deliberately excluded so a benign status flip or estimate refresh never * re-fires staleness. A genuine change to the files/ranges/inputs/rationale under * review moves the signature and re-fires (CE-011). The basis is single-sourced * here so record, consume, and drift-rekey hash identically. */ export declare function taskContentSignatureForTask(task: AuditTask): string; /** * Record (refresh) the baselines for a batch of just-ingested results to their * LIVE content_key, keyed by each result's CURRENT (possibly re-keyed) lineage. * Called from the ingestion executor AFTER drift re-keying, so a re-dispatched * result refreshes the baseline under its NEW redispatch idempotency_key — which * is the lineage `selectCurrentResults` then resolves to, so staleness clears and * the loop converges. Pure: returns a new store. A result whose owning task is * absent (no derivable live signature) is left untouched. */ export declare function refreshResultBaselines(baselines: ResultBaselineStore | undefined, results: readonly AuditResult[], tasksByTaskId: ReadonlyMap): ResultBaselineStore; /** * The set of task_ids whose CURRENT result has DRIFTED from its recorded baseline * — the live task content moved since the result was produced. Consumed by the * obligation state and pending-work selection to treat those tasks as * not-yet-complete so they return for review. Caller passes the * SUPERSESSION-RESOLVED results * (`selectCurrentResults`) so a superseded base record never keeps firing after * its re-dispatch landed. A result with no matching task, no recorded baseline * (never compared), or an underivable signature is not reported stale here. */ export declare function computeStaleResultTaskIds(results: readonly AuditResult[], tasks: readonly AuditTask[], baselines: ResultBaselineStore | undefined): Set; /** * Drift re-keying authority (O3). A just-submitted BASE result whose owning task's * live content has drifted from the recorded baseline for its base idempotency_key * is re-keyed `emit_source: 'redispatch'` with the next 1-based `attempt`, and its * stamped ledger keys are cleared so `appendResultsToLedger` re-stamps a DISTINCT * idempotency_key (the append-only ledger accepts the fresh findings instead of * no-opping on the signature-stable base key). Deterministic + fully tool-owned: * the host never authors `emit_source`/`attempt`. Results that are not base, lack * a live task, have no baseline, or have not drifted pass through unchanged. */ export declare function rekeyDriftedResults(incoming: readonly AuditResult[], tasksByTaskId: ReadonlyMap, baselines: ResultBaselineStore | undefined, existingLedger: readonly AuditResult[]): AuditResult[]; //# sourceMappingURL=resultBaseline.d.ts.map