Metadata-Version: 2.4
Name: sembra
Version: 0.1.0
Summary: Structural similarity scoring engine for JSON-compatible data
Project-URL: Homepage, https://github.com/nhemlos/sembra
Project-URL: Repository, https://github.com/nhemlos/sembra
Project-URL: Documentation, https://github.com/nhemlos/sembra#readme
Author: nhemlos
License: MIT
Keywords: comparison,diff,fuzzy,json,matching,similarity,structural
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Utilities
Requires-Python: >=3.10
Provides-Extra: dev
Requires-Dist: hatchling>=1; extra == 'dev'
Requires-Dist: hypothesis>=6; extra == 'dev'
Requires-Dist: pytest-benchmark>=4; extra == 'dev'
Requires-Dist: pytest-cov>=4; extra == 'dev'
Requires-Dist: pytest>=7; extra == 'dev'
Requires-Dist: ruff>=0.1; extra == 'dev'
Provides-Extra: test
Requires-Dist: hypothesis>=6; extra == 'test'
Requires-Dist: pytest-benchmark>=4; extra == 'test'
Requires-Dist: pytest-cov>=4; extra == 'test'
Requires-Dist: pytest>=7; extra == 'test'
Description-Content-Type: text/markdown

# Sembra

> **Structural similarity scoring for JSON-compatible data**
>
> *Italian: "sembrare" — to seem, to resemble*

---

## The Problem

Existing deep comparison libraries answer only binary questions:

| Library | Gives you |
|---------|-----------|
| `fast-deep-equal` | Equal or not? |
| `lodash.isequal` | Equal or not? |
| `deep-diff` | What changed? |
| `deepdiff` (Python) | What changed? |

None answer the most natural question:

> **How similar are these two structures?**

When schemas evolve, keys get renamed, or data comes from different sources, binary equality is useless. You need a *similarity score*.

## The Algorithm: Sembra Similarity

Sembra computes a continuous similarity score **S(a, b) ∈ [0, 1]** for any two JSON-compatible values using a novel multi-phase algorithm:

### Phase 1: Type-Aware Primitive Matching

- **Strings**: Jaro-Winkler distance (handles typos, minor variations)
- **Numbers**: Relative difference scoring
- **Booleans**: Exact match (configurable partial mode)
- **Null**: Always exact

### Phase 2: Optimal Array Alignment

Instead of index-by-index comparison, Sembra computes a similarity matrix between all pairs of elements and finds the optimal matching using a greedy assignment algorithm. This handles:
- Reordered elements
- Inserted/deleted elements
- Partially matching elements

### Phase 3: Value-Guided Key Rename Detection

**This is the key novelty.** When object keys differ between two structures, Sembra infers renames by comparing the *values* at those keys:

```python
a = {"name": "Alice", "age": 30}
b = {"fullName": "Alice", "yearsOld": 30}
# Sembra detects: name → fullName, age → yearsOld
# Score: >0.85
```

No other library does this.

### Phase 4: Recursive Composition

All phases compose recursively — nested arrays, nested objects, and mixed structures are handled uniformly.

## Performance

| Operation | Complexity |
|-----------|------------|
| Identical structures (early exit) | **O(n)** |
| Similar structures | **O(n log n)** |
| Dissimilar structures | **O(n × m)** |

n = number of nodes in the larger structure.

## Quick Start

```python
from sembra import sembra, sembra_report, SembraConfig

# Basic similarity
score = sembra({"a": 1, "b": 2}, {"a": 1, "b": 3})
print(score)  # ~0.89

# Key rename detection
score = sembra(
    {"userName": "Alice", "userAge": 30},
    {"name": "Alice", "age": 30}
)
print(score)  # >0.8

# Reordered arrays
score = sembra([1, 2, 3], [3, 1, 2])
print(score)  # 1.0 (all elements match)

# Detailed report
report = sembra_report({"a": 1}, {"a": 2})
print(report.score)
print(report.matches)
print(f"Computed in {report.duration_ns}ns")

# Configuration
config = SembraConfig(
    string_similarity=False,
    object_key_rename=False,
    number_threshold=0.9,
)
score = sembra({"a": "hello"}, {"a": "hallo"}, config)
```

## Configuration Reference

| Option | Default | Description |
|--------|---------|-------------|
| `string_similarity` | `True` | Enable fuzzy string matching |
| `string_threshold` | `0.6` | Minimum string similarity to count as match |
| `number_similarity` | `True` | Enable fuzzy number matching |
| `number_threshold` | `0.8` | Minimum number similarity to count as match |
| `boolean_partial` | `False` | Allow partial boolean matching |
| `array_matching` | `True` | Enable optimal array element matching |
| `array_match_threshold` | `0.4` | Minimum element similarity to match |
| `object_key_rename` | `True` | Enable value-guided key rename detection |
| `object_rename_threshold` | `0.65` | Minimum value similarity to infer rename |
| `max_depth` | `16` | Maximum recursion depth |
| `max_array_size` | `200` | Max array size for element-by-element matching |
| `use_cache` | `True` | Cache intermediate results |
| `early_exit` | `True` | Exit early for same-reference objects |
| `key_weight_mode` | `'uniform'` | Key importance weighting |

## Use Cases

- **Test assertions** with tolerance for structural variation
- **Schema migration** — map old API responses to new schemas
- **Data deduplication** — find structurally similar records
- **Configuration comparison** — detect drifted configs
- **API monitoring** — alert on structural drift
- **Merge conflict resolution** — find best correspondence

## Comparison with Existing Libraries

| Feature | Sembra | deepdiff | fast-deep-equal | deep-diff |
|---------|--------|----------|-----------------|-----------|
| Continuous similarity score | ✅ | ❌ | ❌ | ❌ |
| Key rename detection | ✅ | ❌ | ❌ | ❌ |
| Array alignment | ✅ | ❌ | ❌ | ❌ |
| Fuzzy strings | ✅ | ❌ | ❌ | ❌ |
| Fuzzy numbers | ✅ | ❌ | ❌ | ❌ |
| Binary equality | ✅ | ✅ | ✅ | ❌ |
| Diff script | ✅ | ✅ | ❌ | ✅ |
| Cross-type matching | ✅ | ❌ | ❌ | ❌ |

## License

MIT — see [LICENSE](../LICENSE)

## Author

**nhemlos**
