import type { Decision, Evidence, Intent, Operation, OperationStatus, Policy } from "../objects/types.ts"; import { type ConflictRegion } from "../merge/merge3.ts"; import { Buffer } from "node:buffer"; export interface ConflictOption { opOid: string; actor: string; purpose: string; evidence: string[]; score: number; blocked: boolean; requiresHuman: boolean; } export interface Conflict { id: string; key: string; kind: "concurrent_write" | "needs_human"; options: ConflictOption[]; /** Policy's provisional recommendation (never set when a human is required). */ recommendedOp: string | null; reason: string; /** Actor ids that own this scope and should make the call (Phase 5). */ requiredOwners?: string[]; } /** A contest the policy resolved by itself — recorded so auto-merges are auditable. */ export interface AutoDecision { key: string; conflictId: string; chosenOp: string; rejectedOps: string[]; reason: string; policyVersion: string; /** Set when the decision settled one contended REGION of a file rather than a whole op * (docs/22 §3.3): the base line range `[baseStart, baseEnd)` the winner took. Together * with `key` and `chosenOp` this is the decision's idempotent identity — the same region * never mints a second record on a re-reduce. */ region?: { baseStart: number; baseEnd: number; }; /** Per-option score breakdown, in option order: which option each op represented, the * score that represented it, and whether it was out of candidacy. Answers "why did my * change lose this region". Free — `evaluateOp` already computed all of it. */ optionScores?: { opOid: string; score: number; excluded: boolean; }[]; } export interface ReductionResult { /** path → blobOid */ tree: Map; treeHash: string; statuses: Map; conflicts: Conflict[]; autoDecisions: AutoDecision[]; /** Line-level text merge conflicts among concurrent edit_file ops, filled by the repo's * post-reduce pass (detectFileConflicts). reduce() itself leaves this empty. */ fileConflicts: FileConflict[]; /** Frontier op ids: accepted ops that no other accepted op descends from. */ headOps: string[]; /** * Why a rejected op was blocked, op oid → reason (issue #66). The policy engine * already computes this; surfacing it means a file can never leave the * projection without an explanation the caller can print. */ blockedReasons: Map; /** * Count of evidence + decisions discarded by the signature trust gate. Non-zero * means trust — not content — removed support from some op. */ untrustedEvidence: number; /** * Content for blob oids synthesized during reduction (a 3-way text merge produces file * content that is not any single stored blob). oid → content. The caller persists or * writes these directly. Synthetic oids are content-derived, so the treeHash that * references them stays deterministic. */ synthBlobs: Map; } export interface ReduceInput { ops: Operation[]; evidence: Evidence[]; decisions: Decision[]; intents: Map; policy: Policy; /** Which statuses get projected into the tree. Default: accepted only. */ materializeStatuses?: OperationStatus[]; /** blob oid → content bytes, for ops that need file text (edit_file 3-way merge). */ blobContent?: Map; /** actorId → bounded reliability nudge (Phase 5 trust learning). */ reliability?: Map; /** deciderId → role weight; resolves contradictory decisions by authority (docs/08 §4). */ authority?: Map; } /** * The contended keys an op occupies. A rename reads its source and writes its * destination, so it contends on BOTH — otherwise a concurrent write to either * path would slip through unmerged. note ops contend on nothing. */ export declare function keysOf(op: Operation): string[]; export declare function conflictIdFor(key: string): string; /** A file whose concurrent edits overlapped at the line level — a genuine text merge * conflict. Language-neutral: detected purely by merge3 over the file's content. */ export interface FileConflict { file: string; ops: string[]; regions: ConflictRegion[]; /** Binary/non-line-mergeable content: the whole file is ONE opaque contest and the * region's `sides` index distinct contents, not `ops`. Never arbitrated (docs/22 §3.1) * — side → op is not recoverable, so only a human can settle it. */ atomic?: boolean; } /** * Detect line-level merge conflicts among CONCURRENT accepted edit_file ops on the same * file. The reducer's grouping accepts all such ops (their disjoint hunks compose); this * pass runs the authoritative N-way `merge3` over the file's concurrent frontier to find * the ones whose hunks actually OVERLAP, so the repo can surface a Conflict / hold back. * * No language knowledge — merge3 compares lines. Ancestor relations (an edit built on a * prior edit) are not concurrent and never flagged. Deterministic over canonical order. * * "The same file" means the same ALIAS-RESOLVED path, not the same declared one (docs/19 * §3.2). Two concurrent edits can reach one file by two names — one naming the path from * before a move, the other naming it after — and bucketing them by whichever name each op * happened to use would put them in separate buckets, never compare them, and let the * loser's overlapping change disappear without a word. With no renames in the op set, * resolution is the identity and the buckets are exactly the declared paths, as before. */ export declare function detectFileConflicts(ops: Operation[], result: ReductionResult, blobContent: Map): FileConflict[]; /** The authoritative conflict set after region arbitration (docs/22 §3.4). */ export interface FileConflictArbitration { /** Files that still hold at least one region policy could not decide. A file whose every * region was decided drops out entirely — there is nothing left to ask. */ remaining: FileConflict[]; /** One {@link AutoDecision} per decided region (docs/22 §3.3). */ decisions: AutoDecision[]; } /** * Arbitrate the regions {@link detectFileConflicts} found — the authoritative pass, and the * one place where side → op is exact: `FileConflict.ops` is the file's concurrent frontier in * canonical order and `region.options[].sides` index straight into it. * * A decided region leaves the conflict set (policy decided it, so it is no longer a question * for a human) and is recorded as an `AutoDecision` — an automatic decision without an audit * trail is not allowed (docs/00 principle 4, at region granularity). An undecided region * stays exactly as it was: the tree holds the deterministic fallback content and the conflict * still reaches the release gate. * * Scores are lazy and memoized per op, so only the ops of a file that actually contended are * ever evaluated (docs/22 R-c). */ export declare function arbitrateFileConflicts(fileConflicts: FileConflict[], input: ReduceInput): FileConflictArbitration; /** A single group's locally-decided statuses + the conflicts/autoDecisions it emitted. */ export interface PerKeyDecision { local: Map; conflicts: Conflict[]; autoDecisions: AutoDecision[]; } /** Observability for an incremental re-reduce: how much work the dirty-set skipped. * Does not affect the result; purely for benchmarks/metrics (docs/11 A1). */ export interface IncrementalStats { groupsTotal: number; groupsRecomputed: number; groupsReused: number; dirtyKeys: number; } /** * A full reduce plus the per-group bookkeeping an incremental re-reduce needs to reuse * clean groups (see incremental.ts / docs/11). The `result` is exactly `reduce(input)`. */ export interface ReduceSnapshot { input: ReduceInput; result: ReductionResult; perKey: Map; groupOrder: string[]; groupMembers: Map; /** Set by `reduceIncremental` (a full `snapshotReduce` recomputes every group). */ stats: IncrementalStats; } export declare function reduce(input: ReduceInput): ReductionResult; export declare function snapshotReduce(input: ReduceInput): ReduceSnapshot; /** Thrown when an incremental re-reduce's preconditions don't hold; the caller must * fall back to a full `reduce`. Never indicates a correctness failure — only that the * fast path doesn't apply (policy/authority/materializeStatuses changed, or `next` is * not an append-superset of the snapshot's input). */ export declare class NonIncrementalError extends Error { constructor(reason: string); } /** * Incremental re-reduce (docs/11 Track A). Given a prior `snapshotReduce` and a `next` * input that is an APPEND-SUPERSET of the snapshot's input (same policy/authority/ * materializeStatuses; ops/decisions/evidence only added), recompute only the groups * whose decision could have changed (the "dirty set") and reuse every clean group's * cached decision verbatim. The returned result is structurally identical to * `reduce(next)` — this is the invariant the differential harness enforces. * * Dirty keys (see docs/11): keys of new ops; keys of ops targeted by new decisions or * new evidence (these can flip blocked/accept regardless of contention); keys whose * group membership changed or are brand new; and the keys of any op whose actor's * reliability changed (a needs_human conflict embeds the op's reliability-derived score, * so reliability changes are not gated by contention here — A1 may tighten this). * * Throws {@link NonIncrementalError} when the preconditions don't hold; the caller then * falls back to a full reduce. tree/headOps are rebuilt fully (cheap, in-memory) in A0; * A3 will make the tree update incremental too. */ export declare function reduceIncremental(snap: ReduceSnapshot, next: ReduceInput): ReduceSnapshot; export declare function serializeSnapshot(snap: ReduceSnapshot): unknown; export declare function deserializeSnapshot(raw: unknown): ReduceSnapshot; /** One op's policy verdict, as region arbitration needs it (docs/22 §3.2). */ export interface OpScore { score: number; /** Out of candidacy: a failed evidence gate, or a rule that reserves the call for a * human. An excluded op's content must never take a region — that exclusion is the * real effect of this whole track (docs/22 §3.2-3). */ excluded: boolean; } /** opOid → verdict. `undefined` for an op the scorer does not know, which makes * arbitration abstain rather than guess. */ export type OpScorer = (opOid: string) => OpScore | undefined; /** * Build an {@link OpScorer} from a bare `ReduceInput`, for a caller outside `reduce` — the * authoritative post-reduce pass (`detectFileConflicts` → {@link arbitrateFileConflicts}). * Evaluation is lazy and memoized per op, so only the ops of a file that actually contended * are ever scored. */ export declare function buildOpScorer(input: ReduceInput): OpScorer; //# sourceMappingURL=reducer.d.ts.map