# lawref.reanchor

*module*

Перевешивание якорей между редакциями — рычаг формализации предшествующей
редакции (LAYERS-NEXT, спутник О-2).

Задача, ради которой модуль существует. Нормы заякорены в НОВУЮ редакцию акта;
предшествующая редакция не заформализована. Механизм миграций [§32.1](https://github.com/arxohq/law/blob/master/spec/SPEC.ru/07-part-vii-source-and-document-model.ru.md#321-операции-поправки) здесь не
помогает: он бежит вперёд (`apply(previous, operations) ≡ next`), операция
`replace` несёт только новый текст, `repeal` удаляет — восстановить старую
редакцию из новой нечем. Тем более он не применим к ЗАМЕНЕ акта (новая
конституция взамен старой): там нет ребра `revision`, там два разных
`source_work`.

Рычаг — постатейное совпадение текстов. Статья, чей текст в обеих редакциях
совпадает побайтово, не требует переформализации: правило, заякоренное в неё,
перевешивается на редакцию-цель механически. Работа сжимается с «весь акт» до
«статьи, где текст разошёлся».

ЧТО МОДУЛЬ НЕ УТВЕРЖДАЕТ. Совпадение текста статьи НЕ доказывает совпадения
нормы: правило про ст. 5 может опираться на определение из ст. 2, а оно
изменилось. Поэтому выход называется КАНДИДАТАМИ, а не решением, и транзитивные
зависимости здесь намеренно не считаются — это была бы семантика в инструменте
(инвариант CLAUDE.md №5). Проверяется кандидат исполнением: перевесить, прогнать
корпус через `impact-editions` и посмотреть, где исходы разошлись неожиданно.

Сравнение — побайтовое. Нормализация пробелов не делается: она молча решала бы
за автора, что «тот же текст», а [§194](https://github.com/arxohq/law/blob/master/spec/SPEC.ru/27-part-xxvi-static-semantics-and-diagnostics.ru.md#194-sourceprovenance-checks) держит тексты байтами.

## lawref.reanchor.ANCHORED_KINDS

*attribute* · *module attribute*

```python
ANCHORED_KINDS = ('assertion', 'constraint', 'norm_template', 'priority_rule', 'rule')
```

[View source](https://github.com-arxohq/arxo-io/law/blob/2edc2b92ce22b52e03f4081d2769a58229684379/engines/lawref/lawref/reanchor.py#L55-L56)

## lawref.reanchor.CHANGED

*attribute* · *module attribute*

```python
CHANGED = 'changed'
```

[View source](https://github.com-arxohq/arxo-io/law/blob/2edc2b92ce22b52e03f4081d2769a58229684379/engines/lawref/lawref/reanchor.py#L38-L38)

## lawref.reanchor.DEFAULT_MIN_MARGIN

*attribute* · *module attribute*

```python
DEFAULT_MIN_MARGIN = 500
```

[View source](https://github.com-arxohq/arxo-io/law/blob/2edc2b92ce22b52e03f4081d2769a58229684379/engines/lawref/lawref/reanchor.py#L50-L50)

## lawref.reanchor.DEFAULT_MIN_SCORE

*attribute* · *module attribute*

```python
DEFAULT_MIN_SCORE = 6000
```

[View source](https://github.com-arxohq/arxo-io/law/blob/2edc2b92ce22b52e03f4081d2769a58229684379/engines/lawref/lawref/reanchor.py#L49-L49)

## lawref.reanchor.IDENTICAL

*attribute* · *module attribute*

```python
IDENTICAL = 'identical'
```

[View source](https://github.com-arxohq/arxo-io/law/blob/2edc2b92ce22b52e03f4081d2769a58229684379/engines/lawref/lawref/reanchor.py#L37-L37)

## lawref.reanchor.ONLY_SOURCE

*attribute* · *module attribute*

```python
ONLY_SOURCE = 'only_source'
```

[View source](https://github.com-arxohq/arxo-io/law/blob/2edc2b92ce22b52e03f4081d2769a58229684379/engines/lawref/lawref/reanchor.py#L39-L39)

## lawref.reanchor.ONLY_TARGET

*attribute* · *module attribute*

```python
ONLY_TARGET = 'only_target'
```

[View source](https://github.com-arxohq/arxo-io/law/blob/2edc2b92ce22b52e03f4081d2769a58229684379/engines/lawref/lawref/reanchor.py#L40-L40)

## lawref.reanchor.SCORE_SCALE

*attribute* · *module attribute*

```python
SCORE_SCALE = 10000
```

[View source](https://github.com-arxohq/arxo-io/law/blob/2edc2b92ce22b52e03f4081d2769a58229684379/engines/lawref/lawref/reanchor.py#L45-L45)

## lawref.reanchor.ReanchorError

*class*

```python
class ReanchorError(ValueError)
```

Bases: `ValueError`

Отказ разбора: редакция не найдена либо языки пары не совпадают.

[View source](https://github.com-arxohq/arxo-io/law/blob/2edc2b92ce22b52e03f4081d2769a58229684379/engines/lawref/lawref/reanchor.py#L59-L60)

## lawref.reanchor.apply_moves

*function*

```python
def apply_moves(document: dict, report: dict) -> dict
```

Документ с перевешенными якорями кандидатов; вход не мутируется.

Порождает ВАРИАНТ для проверки исполнением, а не готовую формализацию:
решение о переносе нормы остаётся за автором, и коммитить выход этой
функции без прогона корпуса значило бы поверить кандидату на слово.

[View source](https://github.com-arxohq/arxo-io/law/blob/2edc2b92ce22b52e03f4081d2769a58229684379/engines/lawref/lawref/reanchor.py#L416-L434)

## lawref.reanchor.compare_editions

*function*

```python
def compare_editions(document: dict, source: str, target: str) -> dict[str, Any]
```

Постатейное сравнение двух редакций одного языка.

`source` — редакция, в которую нормы заякорены СЕЙЧАС (новая);
`target` — редакция, на которую их предстоит перевесить (старая).

Языки обязаны совпадать: сравнивать русскую редакцию с казахской по тексту
бессмысленно, и молча выбрать одну — ровно то угадывание, которое [§249](https://github.com/arxohq/law/blob/master/spec/SPEC.ru/33-part-xxxii-interoperability.ru.md#249-legalruleml-adapter)
запрещает импортёрам.

[View source](https://github.com-arxohq/arxo-io/law/blob/2edc2b92ce22b52e03f4081d2769a58229684379/engines/lawref/lawref/reanchor.py#L70-L114)

## lawref.reanchor.locator_matches

*function*

```python
def locator_matches(document: dict, source: str, target: str, *, min_score: int = DEFAULT_MIN_SCORE, min_margin: int = DEFAULT_MIN_MARGIN) -> dict[str, Any]
```

Кандидаты соответствия локаторов МЕЖДУ номерами — переписка с перенумерацией.

Зачем нужно отдельно от `compare_editions`. Тот сравнивает статью с
ОДНОИМЁННОЙ: `article/5` новой редакции с `article/5` старой. При переписке,
которая перенумеровала акт, такое сравнение структурно слепо — все статьи
объявляются `changed`, и рычаг пуст, хотя сами тексты в основном уцелели.
Замер на Конституции 2026↔1995 (31.08.2026), ровно тот случай, ради
которого рычаг заводился:

- побайтово совпали 0 локаторов из 104, `changed` — 96;
- но совпадение по номеру сохранила РОВНО ОДНА статья из 98: остальные 97
  переехали, причём систематическим сдвигом на 1–3 (`article/91` → `89`,
  `article/79` → `78`, `article/90` → `88` — счёт 0,99);
- кросс-локаторно мешком слов находится 87 из 96 при счёте ≥ 0,6 и 71 при
  ≥ 0,7, медианный отрыв лучшего кандидата от второго — 0,32, спорных
  (отрыв < 0,05) всего 11.

То есть потеря была не в строгости сравнения ТЕКСТА, а в том, что
сопоставления НОМЕРОВ не существовало вовсе. Мера здесь лексическая и
встроенная (мешок слов, стдлиб): нейронная модель этот класс не
открывает — она нужна там, где слова РАЗНЫЕ, а смысл один (слот [§5](https://github.com/arxohq/law/blob/master/spec/SPEC.ru/02-part-ii-goals-and-scope.ru.md#5-главные-инварианты-дизайна)
DECISION-0040, слой обнаружения `law_search`), тогда как переписка акта
сохраняет словарь почти целиком.

Что функция НЕ утверждает — то же, что и весь модуль: сходство текста не
доказывает тождества нормы. Выход — кандидаты со счётом, и спорные
названы отдельно, а не утоплены в общем списке.

[View source](https://github.com-arxohq/arxo-io/law/blob/2edc2b92ce22b52e03f4081d2769a58229684379/engines/lawref/lawref/reanchor.py#L178-L270)

## lawref.reanchor.reanchor_candidates

*function*

```python
def reanchor_candidates(document: dict, pairs: list[tuple[str, str]], *, fuzzy: bool = False, min_score: int = DEFAULT_MIN_SCORE, min_margin: int = DEFAULT_MIN_MARGIN) -> dict[str, Any]
```

Кандидаты на механическое перевешивание по списку пар (источник, цель).

Пар обычно столько, сколько аутентичных языков у акта: правило конституции
заякорено сразу в русский и казахский фрагменты одной статьи, и перевесить
его можно только целиком — значит, обе пары обязаны быть заданы.

Узел — кандидат, если КАЖДЫЙ его якорь разрешается во фрагмент редакции из
пары, у локатора есть двойник в редакции-цели, и тексты совпадают побайтово.
Иначе узел попадает в `blocked` с причиной — молчаливый пропуск здесь был бы
потерей покрытия того же класса, что молчаливый пропуск нормы.

`fuzzy=True` добавляет второй источник соответствия — `locator_matches`:
двойник ищется не по номеру, а по тексту. Это ровно тот случай, ради
которого рычаг заводился и на котором он был пуст: переписка с
перенумерацией (замер — в докстринге `locator_matches`; на Конституции
2026↔1995 побайтовых двойников 0, текстовых 84). Режим по умолчанию
ВЫКЛЮЧЕН и им остаётся: побайтовое совпадение — факт, а счёт сходства —
предположение, и смешивать их в одном списке без признака было бы
подменой. Поэтому у fuzzy-кандидата в `moves` стоит `score`, а у
побайтового его нет вовсе.

[View source](https://github.com-arxohq/arxo-io/law/blob/2edc2b92ce22b52e03f4081d2769a58229684379/engines/lawref/lawref/reanchor.py#L283-L413)

## lawref.reanchor.render_report

*function*

```python
def render_report(report: dict) -> str
```

Человекочитаемая сводка.

[View source](https://github.com-arxohq/arxo-io/law/blob/2edc2b92ce22b52e03f4081d2769a58229684379/engines/lawref/lawref/reanchor.py#L437-L487)
