Metadata-Version: 2.4
Name: pramiti-merger-reconciler
Version: 0.1.0
Summary: Standalone data model reconciler for M&A data integration
Author-email: "Pramiti Labs, Inc." <hello@getpramiti.com>
License-Expression: MIT
Project-URL: Homepage, https://getpramiti.com
Keywords: ai,ontology,reconciliation,merger,data-model,pramiti
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Software Development :: Libraries
Requires-Python: <3.14,>=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Dynamic: license-file

# pramiti-merger-reconciler

Standalone data model reconciler for M&A integration. Matches, classifies, and suggests resolutions for conflicting business concepts across two workspaces. Zero dependencies -- inject an optional LLM adapter for semantic matching.

## Install

```bash
pip install pramiti-merger-reconciler
```

## Quick Start

```python
from pramiti_merger_reconciler import ClassSpec, ClassConflict, match_classes, classify, suggest

classes_a = [ClassSpec(class_name="Revenue", definition="Net revenue from paid invoices")]
classes_b = [ClassSpec(class_name="Revenue", definition="Total gross revenue")]

matches = match_classes(classes_a, classes_b)
for m in matches:
    conflict_type = classify(m)
    proposal = suggest(ClassConflict(m, conflict_type))
    print(f"{m.match_type} (score={m.score}): {conflict_type} -> {proposal}")
```

## 4-Step Matching Algorithm

1. **Exact name match** (score=1.0) -- case-insensitive string comparison
2. **Normalized name match** (score=0.9) -- strips suffixes (`-data`, `-record`, `-table`, `-entity`, `-type`, `-class`, `-model`, `-info`, `-detail`), whitespace, and underscores
3. **Semantic LLM similarity** (score=0.5--0.85, threshold=0.7) -- capped at `max_similarity_pairs` (default: 200) to prevent O(N*M) LLM budget exhaustion. Pairs the cap drops are returned as `MatchResult(match_type="skipped", skipped_by_cap=True)` rather than disappearing into unmatched residuals.
4. **Unmatched residuals** (score=0.0) -- classes with no match in either workspace

## Conflict Classification

```python
from pramiti_merger_reconciler import classify

conflict_type = classify(match_result)
```

| Type | Meaning |
|------|---------|
| `identical` | Exact/normalized match with same source tables |
| `compatible` | High-score match, one workspace's tables are a subset of the other's |
| `structural_conflict` | Same concept name, completely disjoint source tables |
| `semantic_conflict` | Similar name but incompatible definitions or tables |
| `missing` | Class exists only in one workspace |

## Resolution Suggestions

```python
from pramiti_merger_reconciler import suggest, ClassConflict

# Without LLM -- canned responses for identical/compatible/missing
proposal = suggest(ClassConflict(match_result, "semantic_conflict"))

# With LLM -- richer proposals for semantic/structural conflicts
proposal = suggest(ClassConflict(match_result, "semantic_conflict"), llm=my_llm)
```

## Custom LLM Adapter

Implement `ILLMAdapter` (structural subtyping via `Protocol` -- no inheritance required):

```python
from pramiti_merger_reconciler import ILLMAdapter

class MyLLM:
    def compute_similarity(self, description_a: str, description_b: str) -> float:
        """Return semantic similarity 0.0-1.0."""
        ...

    def generate_resolution(
        self, name_a: str, name_b: str,
        definition_a: str, definition_b: str,
        tables_a: str, tables_b: str,
        conflict_type: str,
    ) -> str:
        """Return a resolution proposal string."""
        ...

matches = match_classes(classes_a, classes_b, llm=MyLLM(), max_similarity_pairs=200)
```

Without an LLM, `NoOpLLMAdapter` is used (returns 0.0 for all similarities, so step 3 produces no matches).

## API

| Export | Description |
|--------|-------------|
| `ClassSpec` | Dataclass: `class_name`, `definition`, `mapped_tables`. Supports `from_dict()` and `from_orm()`. |
| `MatchResult` | Dataclass: `class_a`, `class_b`, `match_type`, `score`, `skipped_by_cap`. |
| `ClassConflict` | Dataclass: `match` (MatchResult) + `conflict_type`. |
| `ReportSummary` | Dataclass: aggregate counts and conflict details for a reconciliation run. |
| `match_classes()` | Run the 4-step matching algorithm. |
| `classify()` | Classify a `MatchResult` into a conflict type string. |
| `suggest()` | Generate a resolution proposal (canned or LLM-assisted). |
| `ILLMAdapter` | Protocol for LLM backends. |
| `NoOpLLMAdapter` | Default no-op adapter (returns 0.0 for all similarities). |
| `ReconcilerError` | Base exception for all reconciler errors. |
| `MatchingError` | Raised on unrecoverable matching errors. Also subclasses `RuntimeError` for backward compatibility. |
| `LLMUnavailableError` | Subclass of `MatchingError`. Raised when the injected LLM adapter fails 3 consecutive times during matching (the match aborts rather than silently scoring pairs 0.0). |

## License

MIT -- Pramiti Labs
