# lawref.loophole

*module*

Bounded-поиск лазеек — L8.2 плана LAYERS-NEXT (горизонт [§5](https://github.com/arxohq/law/blob/master/spec/SPEC.ru/02-part-ii-goals-and-scope.ru.md#5-главные-инварианты-дизайна), заведён
поручением владельца; см. spec/decisions/0035-loophole-and-amend.ru.md).

Лазейка — последовательность шагов (дата, утверждение), после которой цель
достигнута, а НИ НА ОДНОМ шаге не возникло позиции VIOLATED ([§136](https://github.com/arxohq/law/blob/master/spec/SPEC.ru/19-part-xviii-norm-lifecycle-fulfillment-violation.ru.md#136-achievement-satisfaction)–[§137](https://github.com/arxohq/law/blob/master/spec/SPEC.ru/19-part-xviii-norm-lifecycle-fulfillment-violation.ru.md#137-maintenance-satisfaction)) или
CONFLICTED ([§134](https://github.com/arxohq/law/blob/master/spec/SPEC.ru/19-part-xviii-norm-lifecycle-fulfillment-violation.ru.md#134-positioninstance-statuses) публикует и violated-сторону). Право на каждом шаге — своё:
документ проецируется на дату шага машиной применимости D1
(`impact.edition_projection`, [§31](https://github.com/arxohq/law/blob/master/spec/SPEC.ru/07-part-vii-source-and-document-model.ru.md#31-lifecycle)), rule-окна `effective` [§87](https://github.com/arxohq/law/blob/master/spec/SPEC.ru/12-part-xii-events-actions-temporal-model.ru.md#87-rule-temporal-qualifiers) движок читает
сам. Факты аккумулируются: шаг добавляет одно положительное утверждение, и
каждый следующий шаг переоценивает ВЕСЬ накопленный мир на своей дате.

Оркестрация без семантики (инвариант CLAUDE.md №5): модуль только строит
входы (alphabet — по машинерии фаззера О-4), зовёт `evaluate` через
`impact.run_scenario` и читает витрину — статусы позиций из ответа на
`positions`-запрос и truth status цели. Ни одно правило права здесь не
интерпретируется; сравниваются выходы движка.

Поиск детерминирован БЕЗ PRNG: bounded model checking — исчерпывающий обход
в ширину (кратчайшие последовательности первыми) по отсортированным датам и
атомам, с капами и бюджетом вызовов evaluate; каждое усечение названо в
манифесте (правило «no silent caps»). Даты кандидатов добываются механически
из самого документа (границы `effective`, lifecycle редакций [§31](https://github.com/arxohq/law/blob/master/spec/SPEC.ru/07-part-vii-source-and-document-model.ru.md#31-lifecycle), Date-термы)
плюс день после каждой границы — ради «после окна».

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

- `orderSensitive`: те же утверждения одним пакетом на финальной дате
  НЕ проходят (значит существенна последовательность, не набор);
- `timingSensitive`: тот же пакет на ПОЗДНЕЙШЕЙ дате кандидатов не проходит
  (значит существенно окно — классическая лазейка «успеть до/переждать»).

Обход по проекции даёт и свою слепую зону, поэтому отчёт называет её сам:
если проекция сняла правила пакета, манифест несёт маркер
`NORMS_NOT_IN_PROJECTION` (то же имя, что у process-run, DECISION-0052 [§8](https://github.com/arxohq/law/blob/master/spec/SPEC.ru/03-part-iii-architecture-and-conformance.ru.md#8-conformance-профили)), а
снятые ВСЕ правила на всех датах поднимают `projectionVoid` — «ноль находок»
там сказано о пустом праве, а не об отсутствии лазеек. См.
`projection_report`.

Пространство поиска усекается ДО обхода двумя капами — атомов alphabet и
дат-кандидатов, — и оба управляются вызывающим (`alphabet_cap`, `dates_cap`;
флаги `--alphabet-cap`, `--dates-cap`). Усечение не молчит вдвойне: манифест
называет предикаты, у которых в alphabet не осталось НИ ОДНОГО действия
(`alphabetOmittedPredicates` — измерено 01.09.2026 на
`doctrine/aba/equivalence-instruction`: все 24 атома ушли на groundings журнала, и
целевое действие в алфавит не попало ни разу), а «не найдено» при усечённом
пространстве получает СВОЙ вердикт `SPACE-TRUNCATED` и код выхода 4 — по
тому же доводу, что `PARAM-AXIS` у impact-editions: конвейер не должен
читать «лазеек нет» там, где искали не во всём пространстве. Тот же код 4
у `BUDGET-EXHAUSTED` — обход, оборванный бюджетом посреди заданной глубины.
Положительная находка стоит и при усечении; см. `verdict`.

Не входит: генерация отрицательных утверждений и скалярных значений
(alphabet — положительные атомы по пулам О-4); adjudicated-события [§73](https://github.com/arxohq/law/blob/master/spec/SPEC.ru/11-part-xi-assertions-facts-evidence.ru.md#73-origin);
оценка «хорошо/плохо» — отчёт называет пути, судить о дыре — нормотворцу.

## lawref.loophole.ALPHABET_CAP

*attribute* · *module attribute*

```python
ALPHABET_CAP = 24
```

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

## lawref.loophole.DATES_CAP

*attribute* · *module attribute*

```python
DATES_CAP = 6
```

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

## lawref.loophole.DEFAULT_BUDGET

*attribute* · *module attribute*

```python
DEFAULT_BUDGET = 400
```

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

## lawref.loophole.DEFAULT_DEPTH

*attribute* · *module attribute*

```python
DEFAULT_DEPTH = 3
```

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

## lawref.loophole.FINDINGS_CAP

*attribute* · *module attribute*

```python
FINDINGS_CAP = 5
```

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

## lawref.loophole.POSITIONS_QUERY

*attribute* · *module attribute*

```python
POSITIONS_QUERY = {'queryId': 'urn:query:loophole:positions', 'kind': 'positions'}
```

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

## lawref.loophole.PROJECTION_LOSS_KIND

*attribute* · *module attribute*

```python
PROJECTION_LOSS_KIND = 'NORMS_NOT_IN_PROJECTION'
```

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

## lawref.loophole.SCHEMA_VERSION

*attribute* · *module attribute*

```python
SCHEMA_VERSION = 'law.loophole/0.1'
```

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

## lawref.loophole.SHRINK_BUDGET

*attribute* · *module attribute*

```python
SHRINK_BUDGET = 120
```

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

## lawref.loophole.VERDICT_ALREADY

*attribute* · *module attribute*

```python
VERDICT_ALREADY = 'ALREADY-REACHABLE'
```

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

## lawref.loophole.VERDICT_BUDGET_EXHAUSTED

*attribute* · *module attribute*

```python
VERDICT_BUDGET_EXHAUSTED = 'BUDGET-EXHAUSTED'
```

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

## lawref.loophole.VERDICT_EXIT

*attribute* · *module attribute*

```python
VERDICT_EXIT = {VERDICT_FOUND: 1, VERDICT_NOT_FOUND: 0, VERDICT_ALREADY: 0, VERDICT_PROJECTION_VOID: 3, VERDICT_SPACE_TRUNCATED: 4, VERDICT_BUDGET_EXHAUSTED: 4}
```

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

## lawref.loophole.VERDICT_FOUND

*attribute* · *module attribute*

```python
VERDICT_FOUND = 'FOUND'
```

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

## lawref.loophole.VERDICT_NOT_FOUND

*attribute* · *module attribute*

```python
VERDICT_NOT_FOUND = 'NOT-FOUND'
```

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

## lawref.loophole.VERDICT_PROJECTION_VOID

*attribute* · *module attribute*

```python
VERDICT_PROJECTION_VOID = 'PROJECTION-VOID'
```

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

## lawref.loophole.VERDICT_SPACE_TRUNCATED

*attribute* · *module attribute*

```python
VERDICT_SPACE_TRUNCATED = 'SPACE-TRUNCATED'
```

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

## lawref.loophole.VIOLATING_STATUSES

*attribute* · *module attribute*

```python
VIOLATING_STATUSES = ('VIOLATED', 'CONFLICTED')
```

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

## lawref.loophole.emit_scenarios

*function*

```python
def emit_scenarios(report: dict, goal_scenario: dict, out_dir) -> int
```

Каждая находка — цепочка воспроизводимых сценариев формата О-1:
step-NN.json (positions-запрос на дате шага, факты накоплены) и
goal.json (запрос цели на финальной дате).

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

## lawref.loophole.exit_code

*function*

```python
def exit_code(report: dict) -> int
```

Код выхода CLI: 1 — найдено, 0 — не найдено во всём пространстве (или
цель достижима фоном), 3 — обход по пустому праву, 4 — обошли не всё:
пространство усечено капом либо бюджет кончился (см. `summary.verdict`).

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

## lawref.loophole.mine_dates

*function*

```python
def mine_dates(document: dict, extra: list[str], cap: int = DATES_CAP) -> tuple[list[str], bool]
```

Даты-кандидаты из документа: (даты, было_ли_усечение).

Каждая найденная граница даёт и день ПОСЛЕ себя: включительность краёв
различна ([§87](https://github.com/arxohq/law/blob/master/spec/SPEC.ru/12-part-xii-events-actions-temporal-model.ru.md#87-rule-temporal-qualifiers)), и «сразу после окна» — отдельная точка поведения.

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

## lawref.loophole.projection_report

*function*

```python
def projection_report(search: _Search, dates: list[str]) -> tuple[list[dict], bool]
```

Что проекция [§31](https://github.com/arxohq/law/blob/master/spec/SPEC.ru/07-part-vii-source-and-document-model.ru.md#31-lifecycle)/D1 сняла с дат обхода: (маркеры, датируемого_не_осталось).

ЗАЧЕМ ЭТО ЗДЕСЬ. Право каждого шага — проекция акта на дату шага, и это
ровно то, чем ловятся лазейки «успеть до вступления» и «переждать». Но у
пакета, чьи редакции не несут lifecycle-дат (обычай, доктрина,
реставрация текста — их не «вводили в действие» датой, и это ВЕРНО про
них), проекция снимает все заякоренные правила на ЛЮБУЮ дату: обход
честно исполняет пустое право и честно не находит путей. «Findings: 0»
при этом неотличимо от «лазеек нет» — тот же класс, что у process-run,
закрытый находкой NORMS_NOT_IN_PROJECTION (DECISION-0052 [§8](https://github.com/arxohq/law/blob/master/spec/SPEC.ru/03-part-iii-architecture-and-conformance.ru.md#8-conformance-профили)).

Замерено 30.08.2026 на `vory.corpus.ponyatiya`: цель `gnutyi` достижима
одним шагом при обычном evaluate (TRUE_ONLY), а поиск возвращал ноль
находок и `alreadyReachable: false` — все 28 правил сняты на каждой из
дат-кандидатов, потому что ни одна из семи редакций lifecycle не
объявляет.

Оркестрация, не семантика (инвариант CLAUDE.md №5): сравнивается ЧИСЛО
правил в проекции с числом правил документа, состояния редакций берутся
из меты как есть. Ни одно правило права здесь не интерпретируется.

Даты с одинаковой подписью потери сливаются в один маркер: шесть
одинаковых строк на шести датах — шум, а не диагноз.

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

## lawref.loophole.render_text

*function*

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

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

## lawref.loophole.report_bytes

*function*

```python
def report_bytes(report: dict) -> bytes
```

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

## lawref.loophole.run_search

*function*

```python
def run_search(document: dict, goal_scenario: dict, depth: int = DEFAULT_DEPTH, budget: int = DEFAULT_BUDGET, extra_dates: list[str] | None = None, alphabet_cap: int = ALPHABET_CAP, dates_cap: int = DATES_CAP) -> dict[str, Any]
```

Обход; отчёт детерминирован кортежем (документ, цель, depth, budget,
alphabet_cap, dates_cap).

`goal_scenario` — {"case"?: {...}, "query": {...}, "expect"?: {...}}:
query — ground truth-запрос [§168](https://github.com/arxohq/law/blob/master/spec/SPEC.ru/24-part-xxiii-cases-snapshots-evaluation.ru.md#168-case-file); case.assertions, если даны, — неизменный
фон каждого шага; expect.truthStatus — целевой статус (TRUE_ONLY).

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

## lawref.loophole.truncation_lines

*function*

```python
def truncation_lines(manifest: dict) -> list[str]
```

Строки об усечении пространства — одни на текстовый вид и на MCP.

Называется не факт усечения, а его ЦЕНА: сколько атомов отброшено и какие
предикаты остались без единого действия — по ним видно, искали ли там,
где лежит ответ.

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

## lawref.loophole.verdict

*function*

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

Вердикт отчёта — одна политика на текст, MCP и код выхода.

Порядок разбора несимметричен, как у `impact.verdict`: находка —
утверждение ПОЛОЖИТЕЛЬНОЕ об одном пути и стоит при любом усечении.
«Не найдено» — утверждение ОТРИЦАТЕЛЬНОЕ обо всём пространстве, и оно
печатается только когда пространство было обойдено целиком: не по пустому
праву (проекция сняла правила) и не по усечённому капом алфавиту или
списку дат. Иначе это `SPACE-TRUNCATED` — «в этих границах не найдено,
границы выбраны не вами».

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

## lawref.loophole.verdict_hint

*function*

```python
def verdict_hint(kind: str) -> str
```

Пояснение к вердикту, общее для текстового вида и MCP.

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