oc_chat
Loading...
Searching...
No Matches
conflict.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oc_chat/registry/round/conflict.py
4
5# OCT-LINT: disable=tests_exist reason="Covered by oc_chat/tests/registry/test_round_close_escalation.py"
6"""
7Merge-conflict resolution policy for round-close.
8
9Purpose
10-------
11Decide, for a set of conflicted paths from a staging merge, whether the
12round-close job may auto-resolve (generated views only) or must abort
13(OI-210 abort-by-default).
14
15Responsibilities
16----------------
17- Hold the tight allowlist of auto-resolvable generated-file prefixes.
18- Classify a conflict set into resolvable vs blocking paths.
19- Default to abort whenever any path is outside the allowlist.
20
21Diagnostics
22-----------
23Domain: OC_CHAT.MAIN
24Levels:
25 L2 - decision events
26 L3 - per-path classification
27 L4 - deep tracing
28
29Contracts
30---------
31- An empty conflict set is never "resolvable"; callers treat it as clean.
32- The allowlist is prefix-based and tight (views only) by default.
33
34Dependencies
35------------
36- oc_diagnostics (structured debug logging via _dbg)
37- dataclasses (the frozen ConflictDecision and ConflictPolicy records)
38- enum (the ConflictStrategy enum)
39"""
40from __future__ import annotations
41
42from dataclasses import dataclass, field
43from enum import Enum
44from typing import Sequence
45
46from oc_diagnostics import _dbg
47
48
49_dbg("OC_CHAT.MAIN", "module", "loaded", 4)
50
51# Q-1 lean: keep the allowlist tight — generated registry views only.
52DEFAULT_ALLOWLIST_PREFIXES: tuple[str, ...] = ("oc_chat/registry/views/",)
53
54
55class ConflictStrategy(str, Enum):
56 """How the round-close job should treat a conflict set."""
57
58 ABORT = "abort"
59 TAKE_CURRENT = "take_current"
60
61
62@dataclass(frozen=True)
64 """The classification result for one conflict set."""
65
66 strategy: ConflictStrategy
67 resolvable_paths: tuple[str, ...] = field(default_factory=tuple)
68 blocking_paths: tuple[str, ...] = field(default_factory=tuple)
69
70 @property
71 def is_clean(self) -> bool:
72 """True when there were no conflicts to resolve."""
73
74 return not self.resolvable_paths and not self.blocking_paths
75
76
77@dataclass(frozen=True)
79 """Prefix-allowlisted, abort-by-default conflict policy."""
80
81 allowlist_prefixes: tuple[str, ...] = DEFAULT_ALLOWLIST_PREFIXES
82
83 def classify(self, conflicted_paths: Sequence[str]) -> ConflictDecision:
84 """Classify ``conflicted_paths`` into a resolution decision."""
85
86 func = "classify"
87 paths = tuple(p.strip() for p in conflicted_paths if p.strip())
88 if not paths:
89 _dbg("OC_CHAT.MAIN", func, "no conflicts", 4)
90 return ConflictDecision(strategy=ConflictStrategy.TAKE_CURRENT)
91 resolvable = tuple(p for p in paths if self._allowlisted(p))
92 blocking = tuple(p for p in paths if not self._allowlisted(p))
93 if blocking:
94 _dbg("OC_CHAT.MAIN", func, f"abort: {len(blocking)} blocking", 2)
95 return ConflictDecision(
96 strategy=ConflictStrategy.ABORT,
97 resolvable_paths=resolvable,
98 blocking_paths=blocking,
99 )
100 _dbg("OC_CHAT.MAIN", func, f"take_current: {len(resolvable)} views", 2)
101 return ConflictDecision(
102 strategy=ConflictStrategy.TAKE_CURRENT,
103 resolvable_paths=resolvable,
104 )
105
106 def _allowlisted(self, path: str) -> bool:
107 normalized = path.replace("\\", "/")
108 return normalized.startswith(self.allowlist_prefixes)
ConflictDecision classify(self, Sequence[str] conflicted_paths)
Definition conflict.py:83