Metadata-Version: 2.4
Name: persona-dsl
Version: 2026.4.24.1rc213
Summary: Persona DSL - Framework for implementing Screenplay pattern in Python tests
Author-email: Pavel Glyanenko <pglyanenko@me.com>
Requires-Python: >=3.12
Description-Content-Type: text/markdown
Requires-Dist: playwright==1.55.0
Requires-Dist: allure-python-commons<3,>=2.15
Requires-Dist: python-dotenv
Requires-Dist: pyyaml
Requires-Dist: pydantic<3,>=2
Requires-Dist: requests
Requires-Dist: pyhamcrest
Requires-Dist: redis
Requires-Dist: Faker
Requires-Dist: pillow
Requires-Dist: zeep
Requires-Dist: pg8000
Requires-Dist: oracledb
Requires-Dist: kafka-python
Requires-Dist: Unidecode>=1.3
Requires-Dist: black
Requires-Dist: libcst
Requires-Dist: python-dateutil
Requires-Dist: xsdata[cli]
Requires-Dist: xmlschema
Requires-Dist: rich
Requires-Dist: textual
Provides-Extra: dev
Requires-Dist: black; extra == "dev"
Requires-Dist: ruff; extra == "dev"
Requires-Dist: mypy; extra == "dev"
Requires-Dist: pytest; extra == "dev"
Requires-Dist: allure-pytest<3,>=2.15; extra == "dev"
Requires-Dist: pytest-xdist; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Requires-Dist: pre-commit; extra == "dev"
Requires-Dist: types-requests; extra == "dev"
Requires-Dist: types-PyYAML; extra == "dev"
Requires-Dist: types-redis; extra == "dev"
Requires-Dist: types-python-dateutil; extra == "dev"
Requires-Dist: pytest-asyncio; extra == "dev"
Requires-Dist: pytest-timeout; extra == "dev"

# persona-dsl: фреймворк для автотестов на Python

`persona-dsl` — это библиотека для E2E-, API- и интеграционных тестов, построенная вокруг Screenplay-подхода и тесно связанная с runtime/discovery/tooling, которые использует TaaS.

## Установка

```bash
pip install persona-dsl
python -m playwright install --with-deps
```

## Базовая модель

Публичный слой строится вокруг:

- `Persona`
- top-level `test_*` сценариев в `scenarios/**`
- directory-level `__suite__.py`
- `Ops`
- `Step`
- `CombinedStep`
- алиасов `Action`, `Fact`, `Expectation`, `Goal`
- `Page` и `Element`

## Skills и resources

`Skill` является capability-границей runtime: browser, api, db, kafka и другие
интеграции. Сценарий получает `persona: Persona`, а reusable domain-объекты
поверх skills поднимаются через native resource-layer.

Базовое правило такое:

- `persona.skill(...)` для прямого доступа к capability
- `persona.resource(...)` для typed/reusable объектов проекта
- lifecycle hooks для setup/cleanup orchestration

Ресурсы объявляются в `support/resources/**` или
`scenarios/**/support/resources/**`:

```python
from persona_dsl import Persona, define_resource


@define_resource("workspace.dashboard", scope="test")
def workspace_dashboard(persona: Persona):
    browser = persona.skill("browser")
    browser.page.goto("/dashboard")
    yield {"page": browser.page}
```

Использование в сценарии остаётся persona-centric:

```python
from persona_dsl import Persona


def test_dashboard(persona: Persona) -> None:
    dashboard = persona.resource("workspace.dashboard")
    assert dashboard["page"] is not None
```

`scope="test"` очищается после attempt, `scope="session"` живёт до закрытия
runtime session, а `scope="worker"` переиспользуется между сценариями внутри
одного worker launch-а. Этот контракт даёт повторное использование дорогих
ресурсов без fixture injection в сигнатуру сценария.

## Инженерный контракт

В `persona-dsl` жёстко зафиксирован contract по длине файлов:

- рабочая цель для production и verification модулей — держать файл в диапазоне 300-400 строк;
- после 300 строк файл считается кандидатом на разбиение;
- 500 строк — жёсткий предел;
- всё, что временно не помещается в лимит, должно быть явно перечислено в `scripts/file_length_policy.toml` как tracked overflow-долг.

Проверка выполняется через:

```bash
make structure-policy
```

Это правило распространяется на `src/persona_dsl/**` и `tests/**`, включая TypeScript-часть runtime.

```python
from __future__ import annotations

from persona_dsl import Persona, story, title
from persona_dsl.expectations.generic import IsEqualTo
from persona_dsl.ops.web import Click, CurrentPath, Fill, NavigateTo
from persona_dsl.pages import Button, Page, TextField


class LoginPage(Page):
    expected_path = "/login"

    username = TextField(name="username", accessible_name="Логин")
    password = TextField(name="password", accessible_name="Пароль")
    submit_button = Button(name="submit_button", accessible_name="Войти")


@story("Авторизация")
@title("Логин пользователя")
def test_login(persona: Persona) -> None:
    login_page = LoginPage()
    persona.parameter("env", persona.runtime.env)

    with persona.step("Авторизоваться под admin"):
        persona.make(
            NavigateTo(login_page),
            Fill(login_page.username, "admin"),
            Fill(login_page.password, "secret"),
            Click(login_page.submit_button),
        )
        current_path = persona.get(CurrentPath())
        persona.check(current_path, IsEqualTo("/dashboard"))
```

## Страницы и элементы

`Page` наследуется от `Element` и добавляет:

- `expected_path`
- `default_query_params`
- `build_url(base_url=None)`

`Element` поддерживает:

- declarative class attrs
- `aria_ref`
- multi-strategy resolve
- `.using(...)`, `.robust()`, `.resolve_or(...)`
- `ElementList`
- `Table` и declarative table DSL

`VirtualPage` используется для динамической in-memory модели страницы.

## UI ожидания

Persona поддерживает ожидания DOM-состояний и HTML-атрибутов поверх page objects.

Проверочные expectations выполняют Playwright auto-wait в рамках `persona.check(...)`:

- `persona.check(element, BeVisible(timeout=5000))` — элемент присутствует в DOM и видим пользователю;
- `persona.check(element, BeDetached(timeout=5000))` — элемент удалён из DOM;
- `persona.check(element, HaveAttribute("data-import-state", timeout=5000))` — HTML-атрибут появился у элемента;
- `persona.check(element, HaveAttribute("data-import-state", "loaded", timeout=5000))` — HTML-атрибут получил ожидаемое значение.

Операция `WaitForElementState(element, state=..., timeout=5000)` доступна для
синхронизации перед действием. Поддерживаемые состояния: `visible`, `hidden`,
`attached`, `detached`.

Чтение текущего атрибута:

```python
from persona_dsl import Persona
from persona_dsl.expectations.generic import IsEqualTo
from persona_dsl.expectations.web import BeVisible, BeDetached, HaveAttribute
from persona_dsl.ops.web import ElementAttribute
from persona_dsl.pages import Element, Page


class PlanningPage(Page):
    refresh_button = Element(test_id="refresh-imports")
    imports_loader = Element(test_id="imports-loader")
    imports_summary = Element(test_id="imports-summary")


def test_import_state(persona: Persona) -> None:
    page = PlanningPage()
    persona.check(page.imports_summary, BeVisible(timeout=5000))
    persona.check(page.imports_loader, BeDetached(timeout=5000))
    persona.check(
        page.refresh_button,
        HaveAttribute("data-import-state", "loaded", timeout=5000),
    )
    import_state = persona.get(
        ElementAttribute(page.refresh_button, "data-import-state")
    )
    persona.check(import_state, IsEqualTo("loaded"))
```

## Генерация page object'ов

`PageGenerator` и `GeneratePageObject` генерируют **declarative** page objects.

Для generated страниц контракт такой:

- committed generated file не содержит `def __init__(...)`
- не содержит `self.add_element(...)`
- не содержит `self.add_element_list(...)`
- хранит `expected_path`, snapshot/screenshot paths и `aria_ref`
- для повторяющихся анонимных контролов может выпускать declarative group, доступную по индексу как `page.otp_kod[0]`
- внутри таких групп item-level `aria_ref` может отсутствовать; resolve идёт через container scope и `role + index`
- для `combobox`, `listbox` и `radiogroup` собирает варианты из связанных popup/listbox по `aria-controls` и `aria-owns`
- закрытый `combobox` может быть раскрыт на время сбора вариантов и возвращён в исходное состояние при подтверждённом `aria-expanded`
- поддерживает merge/regeneration
- использует footer-секции unresolved/archived diagnostics

CLI:

```bash
persona-page-gen --url http://localhost:8080 --output support/pages/home_page.py
```

Сгенерированный файл не считается исключением из структурного контракта. Если page object растёт, его нужно дробить:

- выделять отдельные `Page`/`Section`/`Table` по доменным зонам экрана;
- генерировать несколько page-файлов вместо одного монолита по всей странице;
- выносить повторяющиеся блоки в переиспользуемые sections/components;
- для schema/codegen разбивать входные XSD/API-пакеты по доменным границам, а не наращивать один огромный generated module.

Из сценария:

```python
from persona_dsl import Persona
from persona_dsl.ops.web import GeneratePageObject, NavigateTo


def test_generate_home_page(persona: Persona) -> None:
    with persona.step("Открыть страницу и сгенерировать page object"):
        persona.make(NavigateTo("/"))
        persona.make(
            GeneratePageObject(
                class_name="HomePage",
                output_path="support/pages/home_page.py",
                wait_for_state="networkidle",
                sleep_before=1,
            )
        )
```

## Контракт `test_id` для generated page object

`generator.test_id_attribute` задаёт имя DOM-атрибута, из которого runtime и
генератор читают test-id.

```yaml
generator:
  test_id_attribute: "data-testid"
```

Правила контракта:

- effective test-id attribute равен `data-testid`, если ключ не задан;
- если ключ задан, effective test-id attribute равен значению
  `generator.test_id_attribute`;
- runtime snapshot, JS runtime, `page.get_by_test_id(...)` и
  `GeneratePageObject` используют одно и то же effective значение;
- generated Python page object хранит в `test_id=...` только значение
  локатора, например `"submit-btn"`;
- `aria_ref` использует `data-persona-id`.

## Таблицы

Актуальный table contract включает:

- `Column(...)`
- `TableColumn`
- `table.filters.*`
- `row.elements.*`
- `row.element(column)`
- `row.value(column)`
- `row.details`
- `table.select_all_checkbox`

Generator использует этот DSL и для обычных, и для сложных таблиц.

## Структура native-проекта

Для новых native-проектов Persona authoring contract строится вокруг:

- `pyproject.toml`
- `config/{env}.yaml`
- `scenarios/`
- `scenario_data/`
- `support/`
- `reports/`
- `artifacts/`

Канонический масштабируемый проект выглядит так:

```text
pyproject.toml
config/
  dev.yaml
  stage.yaml
scenarios/
  __suite__.py
  checkout/
    __suite__.py
    smoke/
      __suite__.py
      test_checkout_smoke.py
      support/
      data/
    matrix/
      __suite__.py
      test_checkout_matrix.py
  session/
    __suite__.py
    core/
      __suite__.py
      test_persona_core.py
scenario_data/
  shared/
  variants/
  schemas/
support/
  pages/
  resources/
  contracts/
  shared/
reports/
artifacts/
scripts/
```

Правила раскладки такие:

- `scenarios/` содержит только исполняемые top-level `test_*` и directory-level `__suite__.py`.
- `scenarios/**/support/` и `scenarios/**/data/` нужны для domain-specific helper-кода и данных, которые живут рядом с конкретной веткой сценариев и не должны засорять общий слой проекта.
- `support/pages/`, `support/resources/`, `support/contracts/` и `support/shared/` предназначены для по-настоящему общих артефактов проекта: page objects, typed resources, generated contracts и reusable helpers.
- `support/pages/`, `support/resources/` и `support/contracts/` масштабируются через доменную группировку внутри каталога: `support/pages/checkout/`, `support/resources/session/`, `support/contracts/billing/`.
- `support/shared/` держит только cross-domain helper-код без привязки к одному продуктовому разделу.
- переносить helper из `scenarios/**/support/` в верхний `support/**` стоит только после появления повторного использования между независимыми ветками сценариев.
- `scenario_data/` хранит статические декларативные входные данные, variant sets, схемы и другие committed inputs, которые не исполняются как сценарии.
- `reports/` и `artifacts/` являются output-каталогами раннера и не входят в пользовательский authoring-контур.
- `scripts/` опционален и используется только для проектных служебных проверок.

Публичный native-контракт не предполагает пользовательские `tests/`,
`pytest.ini`, `conftest.py`, class-based suites и marker-driven discovery.
Discovery читает только каталог сценариев из project layout; default-значение:
`scenarios/**`.

Минимальный `pyproject.toml` для native-проекта фиксирует:

- `[project].name` как `project_id`
- `[tool.persona].default_env`
- `[tool.persona].default_role_id`
- `[tool.persona.layout].scenarios_dir`
- `[tool.persona.layout].scenario_data_dir`
- `[tool.persona.layout].support_dir`
- `[tool.persona.layout].reports_dir`
- `[tool.persona.layout].artifacts_dir`
- `[tool.persona.runner].default_workers`
- `[tool.persona.runner].default_console`
- `[tool.persona.runner].default_log_console`

Если проекту нужны default bindings для навыков, они задаются там же:

- `[tool.persona.skills.by_role.<role_id>]` для role-specific overrides

`config/{env}.yaml` задаёт env-specific роли, skill profiles, lifecycle bindings
и retry bindings.

Типичный runner-блок:

```toml
[tool.persona]
default_env = "dev"
default_role_id = "default"

[tool.persona.layout]
scenarios_dir = "scenarios"
scenario_data_dir = "scenario_data"
support_dir = "support"
reports_dir = "reports"
artifacts_dir = "artifacts"

[tool.persona.runner]
default_workers = "auto"
default_console = "auto"
default_log_console = "warning"
```

## Suite metadata и inheritance

Directory-level `__suite__.py` нужен для декларативных suite-метаданных,
suite-level lifecycle и suite-level retry без xUnit-классов и без импорта
пользовательского runtime-кода во время discovery.

Базовые правила такие:

- runner читает только `scenarios/**/*.py` и `scenarios/**/__suite__.py`;
- исполняемыми считаются только top-level функции `test_*`;
- `__suite__.py` читается AST-ом и не импортируется как обычный модуль;
- файл должен содержать только import-ы, docstring и ровно один top-level вызов `suite(...)`;
- аргументы `suite(...)` должны быть статическими строковыми литералами или tuple/list строковых литералов;
- поддерживаемые аргументы сейчас: `name`, `epic`, `feature`, `role_id`, `tags`, `lifecycle`, `retry`.

Типичный `__suite__.py` выглядит так:

```python
from persona_dsl import suite

suite(
    name="Checkout",
    epic="Витрина магазина",
    feature="Оформление заказа",
    role_id="web",
    tags=("checkout", "smoke"),
    lifecycle=("ui_smoke",),
    retry="fragile_ui",
)
```

Наследование идёт сверху вниз по каталогу:

`scenarios/__suite__.py` -> доменный `__suite__.py` -> leaf `__suite__.py` -> `test_*`

Вложенность каталогов с `__suite__.py` не ограничена. Runner проходит всю
цепочку от каталога файла вверх до `scenarios/` и сохраняет полную иерархию в
`suite_path`.

Практический смысл inheritance такой:

- корневой `scenarios/__suite__.py` задаёт верхнеуровневую карту проекта: верхнее имя suite, общие теги, глобальные lifecycle-профили;
- доменные `__suite__.py` раскладывают проект по доменным границам и добавляют `epic`, `feature`, доменные теги и role bindings;
- leaf `__suite__.py` уточняют самый узкий контур конкретной папки сценариев;
- сами `test_*` оставляют persona-centric код и точечные метаданные уровня сценария: `title`, `story`, `tag`, `use_*`, `apply_*`.

Складывание метаданных и политик происходит не по правилу "что позже написано,
то главнее", а по типу поля.

Внутри самой цепочки `__suite__.py` правила такие:

- `name` не переопределяется, а накапливается в порядке `root -> ... -> deepest`;
- `tags` накапливаются как уникальное объединение в порядке `root -> ... -> deepest`;
- `lifecycle` накапливается в порядке `root -> ... -> deepest`; если один и тот же profile id встретился несколько раз, в runtime остаётся первое вхождение;
- `epic` и `feature` не имеют single-winner semantics: разные значения остаются как отдельные labels, точные дубликаты не дублируются;
- `role_id` переопределяется самым узким suite;
- `retry` переопределяется самым узким suite.

`role_id` задаёт роль для аргумента `persona: Persona`. Сценарии с несколькими
ролями используют `personas: PersonaSession` и доступ к ролям через
`personas.get(...)`.

Правила разрешения для test-level деклараций и внешних bindings:

- lifecycle additive, а не winner-takes-all;
- retry singleton, то есть выбирается первый подходящий источник по жёсткому порядку.

Порядок применения lifecycle:

1. `@use_lifecycle(...)` на самом `test_*`;
2. `@apply_lifecycle(...)` на самом `test_*`;
3. suite `lifecycle=(...)`, уже собранный из всей цепочки `root -> ... -> deepest`;
4. `config.framework.lifecycle.tests`;
5. `config.framework.lifecycle.test_prefixes`;
6. `config.framework.lifecycle.selectors`;
7. `config.framework.lifecycle.roles`;
8. `config.framework.lifecycle.all_tests`;
9. кодовые `bind_lifecycle(..., all_tests=True)`;
10. кодовые `bind_lifecycle(..., selector=...)`.

Порядок применения retry:

1. `@use_retry(...)` на самом `test_*`;
2. `@apply_retry(...)` на самом `test_*`;
3. suite `retry=...`, уже разрешённый из deepest `__suite__.py`;
4. `config.framework.retry.tests`;
5. `config.framework.retry.test_prefixes`;
6. `config.framework.retry.selectors`;
7. `config.framework.retry.roles`;
8. `config.framework.retry.all_tests`;
9. кодовые `bind_retry(..., all_tests=True)`;
10. кодовые `bind_retry(..., selector=...)`.

Для retry выбирается первый подходящий источник по порядку precedence. Более
широкие уровни после этого не применяются. Для lifecycle источники суммируются
в указанном порядке.

Retry не накапливается. Retry-policy задаёт единый набор `max_attempts`,
`retry_on`, backoff и retry hooks.

`component.with_retry(...)` остаётся самым узким уровнем и влияет только на
конкретный шаг, а не на retry-политику всего сценария.

Хорошая практика для масштабируемого проекта:

- использовать `scenarios/__suite__.py` для общей карты проекта, а не для набора локальных исключений;
- держать `epic/feature/tags` на уровне каталогов, где это действительно общая семантика для всех сценариев внутри;
- не дублировать в `__suite__.py` то, что относится только к одному тесту;
- не писать в `__suite__.py` вычисления, helper-функции, условную логику и runtime side effects: такой файл должен оставаться статическим DSL-описанием каталога.

Основные команды из корня Persona-проекта:

```bash
persona run
persona run --max-workers auto
persona run --console rich
persona run --console tui
persona run --log-console info
persona run --include-tag smoke
persona run --test-path scenarios/checkout/smoke/test_checkout_smoke.py
persona run --test-path scenarios/session
persona run --suite Smoke
persona run --scenario-id checkout.matrix --variant-id guest
persona launch start --max-workers 8
persona launch start --test-path scenarios/session
persona launch start --suite Smoke
persona launch start --scenario-id checkout.matrix --variant-id auth
persona launch list
persona launch status
persona launch workers --launch-id <launch_id>
persona launch watch
persona launch pause --launch-id <launch_id>
persona launch resume --launch-id <launch_id>
persona launch cancel --launch-id <launch_id>
persona launch cancel-item --launch-id <launch_id> --work-item-id <scenario_id>::<variant_id>
persona launch terminate --launch-id <launch_id>
persona launch scale --launch-id <launch_id> --max-workers 4
persona launch rerun-failed --launch-id <launch_id> --max-workers 2
persona launch retry-stuck --launch-id <launch_id> --max-workers 2
persona report info --launch-id <launch_id>
persona report build --launch-id <launch_id>
persona report serve --launch-id <launch_id>
```

## Локальный Runner

`persona run` использует native discovery, canonical execution journal и
изолированный Allure export adapter вне execution path.

Публичный CLI-контракт:

- `--max-workers auto|N`
- `--console auto|plain|rich|tui`
- `--log-console quiet|error|warning|info|debug`
- `--project-root <path>` задаёт корень Persona-проекта; значение по умолчанию — текущая директория
- `--test-path <path>` выбирает сценарии из файла или каталога внутри `scenarios/`; относительный путь резолвится от корня Persona-проекта
- `--suite <name>` выбирает сценарии по точному имени из `suite_path`, собранному из цепочки `__suite__.py`
- `--path-prefix <prefix>` фильтрует catalog path по префиксу

Правила по умолчанию:

- `default_workers = "auto"` означает `min(selected_work_items, os.cpu_count())`
- `default_console = "auto"` выбирает `rich` в интерактивном TTY и `plain` в неинтерактивном потоке
- `default_log_console = "warning"` выводит в консоль runtime logs уровней `warning` и `error`

Назначение режимов консоли:

- `plain` — append-only вывод для CI, пайпов и лог-файлов
- `rich` — live progress bar, активные worker/item, recent items, runtime logs и failure cards
- `tui` — интерактивный монитор локального прогона с pause/resume/cancel launch и cancel selected item

Артефакты локального запуска:

- `summary.json`
- `journal.jsonl`
- `allure-results/`
- `console.txt`
- `console.html` для `rich`
- `runtime.log`
- `runtime.ndjson`
- `stdout.log`
- `stderr.log`
- `traceback.txt` для failed/broken execution

Локальный report flow:

- `persona report info` показывает raw paths выбранного launch, текущую `reports/latest/` projection и размер historical chain для выбранного `env`
- `persona report build` собирает `reports/latest/allure-report/` для выбранного launch с history по предыдущим terminal launch того же `env`
- `persona report serve` собирает тот же historical report и открывает его через Allure CLI
- `reports/latest/allure-results/` хранит raw `allure-results` текущей projection
- `reports/latest/allure-report/` хранит materialized HTML report
- `reports/latest/allure-report.json` фиксирует `launch_id`, `env` и список launch, вошедших в historical chain

Публичный execution-контракт строится вокруг native `scenario_id`,
`persona run`, `persona launch *` и `run_scenario` в MCP.

## MCP Server

Persona DSL включает встроенный MCP JSON-RPC server:

```bash
persona-mcp
```

`--project-root <path>` задаёт Persona-проект, который индексирует сервер;
значение по умолчанию — текущая директория.
MCP читает тот же native contract, что и локальный runner, discovery и
canonical report model.

Публичный набор инструментов:

- `persona_guide` — встроенное руководство по Persona DSL без project docs.
- `project_inventory`, `project_search`, `project_entity` — project-aware inventory, typed search и точечное чтение сущности по `entity_id`.
- `lifecycle_preview`, `retry_preview`, `retry_explain` — explain lifecycle/retry политики для `scenario_id` или component symbol.
- `runtime_open`, `runtime_get`, `runtime_call`, `runtime_close` — runtime session для skills, resources и symbol invocation.
- `run_scenario` — адресный запуск одного `scenario_id`.
- `generate_preview` — preview генерации `page`, `xsd`, `api`, `test` и `step` без записи файлов.

Каждый `tools/call` возвращает единый `structuredContent`:

- `ok` — признак отсутствия критической ошибки инструмента.
- `status` — `ok`, `partial` или `error`.
- `data` — результат инструмента или `null`.
- `diagnostics` — список ошибок с `kind`, `path`, `message`, `context`,
  `line`, `column` и `symbol_id` при наличии координат.

`persona_guide` использует только встроенную справку Persona DSL и core symbols.
Project-aware инструменты возвращают доступную часть индекса и diagnostics по
секциям discovery. Runtime, launch и generation инструменты возвращают
диагностику в envelope при недоступном env, session, schema, scenario или
runtime state.

`tools/list` публикует `inputSchema` с одинарными JSON Schema типами.
Необязательные аргументы задаются отсутствием поля в `required`; `null` не
входит в `type` и `enum` публичных спецификаций инструментов.

Discovery индексирует native-структуру `<scenarios_dir>/**/test_*.py` и
`<scenarios_dir>/**/__suite__.py`, где `scenarios_dir` берётся из
`[tool.persona.layout]` и по умолчанию равен `scenarios`. Каталог `tests/**` не
является источником Persona scenarios.

## Runtime Logging

Runtime logging доступен в активном сценарии, `ops`, компонентах, ресурсах и
других слоях, где уже поднят runtime context.

Публичный API:

- `persona.log(level, message, **fields)`
- `persona.debug(...)`
- `persona.info(...)`
- `persona.warning(...)`
- `persona.error(...)`
- `persona_dsl.runtime.log(...)`
- `persona_dsl.runtime.debug(...)`
- `persona_dsl.runtime.info(...)`
- `persona_dsl.runtime.warning(...)`
- `persona_dsl.runtime.error(...)`

`level` фиксирован как `debug | info | warning | error`. Вызов вне активного
runtime даёт явный `RuntimeError`.

Если нужен локальный генератор страниц или шаблонный проект, ориентируйтесь на
`example_template/templates/persona_project_starter`.

## Где смотреть дальше

- [Starter template](../example_template/templates/persona_project_starter/README.md)
- [Project principles](../docs/CODESTYLE.md)

Полная пользовательская карта Persona DSL живёт именно в starter template:
там для каждого публичного capability указан конкретный `test_*`, соседний
README и support-layer файл, включая lifecycle/retry, resources, browser/UI,
service skills, tooling, local launch UX и report surface.
