# lawref.query.facts

*module*

Реификация CLIR в плоские отношения LawQL (DECISION-0079, Ф1).

Виды верхнеуровневых узлов извлекаются из нормативной JSON Schema. Поэтому
новый вид нельзя молча потерять: документ с видом, которого схема не объявила,
отвергается `LDC-Q1001`, а схема одновременно публикуется через `--schema`.

## lawref.query.facts.DEFAULT_CORPUS

*attribute* · *module attribute*

```python
DEFAULT_CORPUS = ROOT / 'corpus/clir'
```

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

## lawref.query.facts.DEFAULT_SCHEMA

*attribute* · *module attribute*

```python
DEFAULT_SCHEMA = ROOT / 'spec/schema/legal-ir.schema.json'
```

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

## lawref.query.facts.RELATION_FIELDS

*attribute* · *module attribute*

```python
RELATION_FIELDS: dict[str, tuple[str, ...]] = {'nodes': ('id', 'kind', 'package', 'package_name', 'path'), 'edges': ('from', 'to', 'role', 'package', 'path'), 'anchors': ('node', 'anchor', 'act', 'locator', 'package', 'path'), 'labels': ('node', 'lang', 'text', 'status', 'package'), 'positions': ('norm', 'kind', 'bearer_type', 'package', 'path'), 'windows': ('norm', 'kind', 'deadline', 'package', 'path'), 'imports': ('package', 'imported', 'semantic_hash', 'path'), 'fields': ('node', 'field', 'value', 'value_kind', 'package', 'path'), 'literals': ('node', 'context', 'predicate', 'polarity', 'package', 'path'), 'variables': ('node', 'context', 'name', 'type', 'package', 'path'), 'rules': ('id', 'strength', 'head_predicate', 'head_polarity', 'head_variables', 'body_variables', 'body_predicates', 'package', 'path'), 'norms': ('id', 'modality', 'goal_kind', 'condition_predicates', 'package', 'path'), 'interpretations': ('id', 'predicate', 'role', 'package', 'path')}
```

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

## lawref.query.facts.ROOT

*attribute* · *module attribute*

```python
ROOT = Path(__file__).resolve().parents[4]
```

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

## lawref.query.facts.FactStore

*class* · *dataclass*

```python
class FactStore
```

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

### lawref.query.facts.FactStore.documents

*attribute* · *class attribute* · *instance attribute*

```python
documents: list[dict] = field(default_factory=list)
```

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

### lawref.query.facts.FactStore.enabled_relations

*attribute* · *class attribute* · *instance attribute*

```python
enabled_relations: set[str] = field(default_factory=lambda : set(RELATION_FIELDS))
```

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

### lawref.query.facts.FactStore.node_count

*attribute* · *class attribute* · *instance attribute*

```python
node_count: int = 0
```

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

### lawref.query.facts.FactStore.node_kinds

*attribute* · *class attribute* · *instance attribute*

```python
node_kinds: tuple[str, ...] = field(default_factory=schema_node_kinds)
```

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

### lawref.query.facts.FactStore.relations

*attribute* · *class attribute* · *instance attribute*

```python
relations: dict[str, list[dict]] = field(default_factory=lambda : {name: [] for name in RELATION_FIELDS})
```

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

### lawref.query.facts.FactStore.add_document

*method*

```python
def add_document(document: dict, path: Path, law_path: str | None = None) -> None
```

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

### lawref.query.facts.FactStore.document

*method*

```python
def document(package: str) -> dict | None
```

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

### lawref.query.facts.FactStore.finalize

*method*

```python
def finalize() -> None
```

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

### lawref.query.facts.FactStore.node

*method*

```python
def node(node_id: str) -> dict | None
```

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

### lawref.query.facts.FactStore.rows

*method*

```python
def rows(relation: str, package_glob: str = '*') -> list[dict]
```

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

### lawref.query.facts.FactStore.schema

*method*

```python
def schema() -> dict
```

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

## lawref.query.facts.discover_corpus

*function*

```python
def discover_corpus(corpus: Path = DEFAULT_CORPUS) -> list[Path]
```

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

## lawref.query.facts.load_corpus

*function*

```python
def load_corpus(paths: Iterable[Path] | None = None, package_glob: str = '*', relations: Iterable[str] | None = None, package_globs: Iterable[str] | None = None, include_law_paths: bool = True, documents: Iterable[tuple[Path | str, dict]] | None = None) -> FactStore
```

Снимок корпуса в FactStore.

Два входа, взаимоисключающие: `paths` — файлы CLIR (по умолчанию весь
`corpus/clir`), `documents` — пары (путь, уже разобранный CLIR) от хоста,
который держит корпус в памяти (каталог MCP: те же документы, сверенные по
stat за миллисекунды, вместо второго чтения и разбора 184 МБ на каждый
вызов). Путь при документе нужен ровно тем, чем и при файле: порядком
обхода (первый в канонической сортировке — единственный снимок пакета) и
полем `path` строк фактов; байты результата от входа не зависят — это
держит `check_query.py`. Документы не копируются и не мутируются: строки
фактов ссылаются на их узлы, поэтому хост обязан отдавать их разделяемыми
и неизменяемыми (у каталога MCP это уже контракт corpus_snapshot).

Глоб пакета применяется ДО разбора, когда путь известен: имя подглядывается
в байтах (`peek_package_name`), и файл вне глоба не разбирается вовсе.
Следствие, названное честно: испорченный CLIR ВНЕ глоба запрос больше не
роняет — его целостность держат ворота компилятора, а не LawQL.

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

## lawref.query.facts.peek_package

*function*

```python
def peek_package(data: bytes) -> dict | None
```

`package` документа по его байтам — без разбора 0,5 МБ узлов.

Точность, а не эвристика. Метка содержит неэкранированные кавычки, а
внутри JSON-строки `"` обязана быть `\"`, поэтому каждое вхождение метки
в валидном JSON — ключ `package`, чьё значение начинается с ключа `name`.
Берётся ПОСЛЕДНЕЕ вхождение, и `{` + хвост разбирается как объект: разбор
удаётся тогда и только тогда, когда вхождение стоит на глубине 1 — вложенное
закрыло бы больше скобок, чем открыло (`imports[].package` в канонических
байтах и так раньше top-level: ключи отсортированы). Дубликат top-level
ключа json.loads берёт последним — как и rfind. Документ с whitespace
(не канонический, вроде отформатированных календарных CLIR) метки не
содержит и честно уходит в полный разбор: None означает «не знаю», а не
«нет пакета».

Замер 02.09.2026 на 376 файлах / 183 МБ: чтение байтов 0,31 с, 369
подглядываний 0,003 с против 2,2 с json.loads всего корпуса.

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

## lawref.query.facts.peek_package_name

*function*

```python
def peek_package_name(data: bytes) -> str | None
```

Read package.name with the same conservative fast path as before.

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

## lawref.query.facts.schema_node_kinds

*function*

```python
def schema_node_kinds(schema_path: Path = DEFAULT_SCHEMA) -> tuple[str, ...]
```

Все допустимые `Node`, а не все вложенные term/formula kinds.

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