Skip to content

Nihil: черновик API конфига — кандидат 💡

Nihil: черновик API конфига — кандидат 💡

Section titled “Nihil: черновик API конфига — кандидат 💡”

Статус: кандидат 💡, не решение. Спроектировано по SPEC раздел 4 (решение ✅, реализация 💡). Связанные вопросы SPEC: 8 (граница «Lua декларирует / плагин исполняет»), 10 (масштаб мира / дефолт density). Сетевые настройки (PROTOCOL.md) здесь не рассматриваются — отдельный черновик, когда N0.

Инвариант из SPEC, соблюдённый везде ниже: Lua исполняется ровно один раз при загрузке и возвращает декларативные данные. Никаких колбэков на события. Динамика — плагины (WASM, M7), конфиг остаётся декларативным.

  1. Скрипт возвращает одну таблицу — точку истины. Не «регистрация через side-effect API» (nihil.add_rule(...)), а return config. Побочные вызовы запрещены песочницей (раздел 7).
  2. Разрешена чистая генерация данных: циклы, конкатенация строк (сборка regex), арифметика. Всё, что детерминированно и завершается — можно.
  3. Расширяемость через version + append-only поля: новые опциональные поля добавляются без bump версии; bump — только при удалении/смене смысла поля (правила эволюции как в PROTOCOL.md, там это уже решили для сети — берём ту же дисциплину).
  4. «Нет скрытых лимитов» (SPEC правило для агентов 4): опасные значения разрешены, но проходят через предупреждение с оценкой и подтверждением (раздел 6).
  5. Две плоскости: автомат (regex-правила окон, последовательности клавиш) и значения (density, fov, комнаты, панели, поведение). Раздел 4 проводит границу явно.
-- nihil.lua — конфиг Nihil (черновик API, кандидат 💡).
-- Скрипт исполняется РОВНО один раз при загрузке; всё взаимодействие —
-- таблица, возвращённая в конце. Колбэков нет.
local config = {
-- Версия СХЕМЫ конфига (не композитора). Поля только добавляются;
-- bump = удаление или смена смысла поля.
version = 1,
-- Плоскость значений: в автомат не компилируется.
general = {
density_default = 200, -- px/m, SPEC вопрос 10
camera = {
vfov = 65, -- градусы (vfov, как Hypr3D-дамп)
eye_height = 1.6, -- метры, Y-вверх
},
vram = {
mode = "degrade", -- "degrade" | "refuse" (решено ✅)
warn_mb = 256, -- порог предупреждения, НЕ лимит
},
},
rooms = {
{
name = "main",
bounds = { min = { -20, -1, -20 }, max = { 20, 15, 20 } },
grid = { cell = 0.5, snap_deg = 15 },
camera_spawn = { pos = { 0, 1.6, 6 }, yaw = 180, pitch = 0 },
},
{
name = "work",
bounds = { min = { -6, -1, -6 }, max = { 6, 12, 6 } },
grid = { cell = 0.25, snap_deg = 15 },
camera_spawn = { pos = { 0, 1.6, 4.5 }, yaw = 180, pitch = -5 },
},
},
-- Правила окон: матч при мапе по app_id + initial title (SPEC раздел 4).
-- Порядок = приоритет: первое подошедшее правило выигрывает.
window_rules = {
{
match = { app_id = "^kitty$" },
density = 350,
spawn = { room = "main", face = "camera" },
},
{
-- initial title: живой счётчик заголовка не перезапускает правило
match = { app_id = "^firefox$", title = "YouTube" },
density = 150,
render_scale = 2, -- суперсемплинг x2
est_size = { 1920, 1080 }, -- для оценки VRAM (раздел 6)
spawn = { room = "main", pos = { 0, 1.8, -4 }, yaw = 0 },
filters = { mipmaps = true, aniso = 16 },
},
{
-- live_title: ЯВНАЯ опция правила — реагирует на смену заголовка
match = { app_id = "^org.telegram.desktop$", live_title = true },
density = 400,
render_scale = { auto = true }, -- множитель от дистанции/угла (SPEC раздел 3)
transparency = { multiplier = 0.9 },
},
{
-- Game Mode по правилу (SPEC: включение по regex на app_id)
match = { app_id = "^steam_app_" },
game_mode = true,
render_scale = 1, -- максимум производительности
est_size = { 2560, 1440 },
reapply = "reload", -- 💡 кандидат-поправка, раздел 5
},
{
match = { title = "Preferences" },
density = 500, -- маленькие диалоги — плотнее
spawn = { room = "current", face = "camera" },
},
},
-- Хоткеи: последовательности клавиш → компилируются в KeyDfa (раздел 4).
hotkeys = {
{
keys = "Super+Return",
on = { type = "spawn", command = "kitty" },
description = "терминал",
},
{
-- double-tap: два Shift в окне tap_ms (дефолт из SPEC)
keys = "Shift Shift",
tap_ms = 350,
on = { type = "spawn", command = "wofi --show drun" },
description = "запуск приложений",
},
{
-- chord-последовательность: Super+K, затем C
keys = "Super+K C",
tap_ms = 600,
on = { type = "mode", mode = "cluster" },
},
{
keys = "Super+F",
on = { type = "focus", target = "immersive" },
-- ✅ Implemented (2026-10-18, §7.6): realised as
-- `immersive = { hotkey = "super+f", cycle = {"true","framed","off"} }`
-- in the shipped config — the DRAFT's `{type="focus",
-- target="immersive"}` maps to it (cycle through the listed modes).
},
{
keys = "Super+L",
on = { type = "layout", name = "games" },
},
{
keys = "Super+W",
on = { type = "room", room = "work" },
},
{
keys = "Super+D",
on = { type = "density", value = 1500 }, -- «растянуть в телевизор»
},
},
-- Панели/шелл: якоря для layer-shell + nihil_shell_v1 (SPEC раздел 2).
panels = {
{
match = { namespace = "^waybar$" },
anchor = "world", -- в комнате, как окно
room = "main",
pos = { 0, 2.2, -5.5 },
yaw = 0,
},
{
match = { namespace = "^nihil:" }, -- свой протокол
anchor = "head", -- HUD, едет с камерой
offset = { 0, 0.25, -1.2 },
},
{
match = { namespace = "^quickbar$" },
anchor = "body", -- wrist-панель (как в Quest)
offset = { -0.25, -0.2, -0.5 }, -- вниз-влево от камеры
follow = { smoothing = 0.15 }, -- мягкое следование
show = { hotkey = "Super+P", gaze = true }, -- оба механизма сразу
},
},
-- Флаги поведения: плоскость значений.
behavior = {
drag = {
snap_modifier = "Shift", -- зажатие = квантование позиции
snap_deg = 15, -- углы до шага; у стены — нормаль стены
grid_overlay = true, -- видимая сетка при drag
ghost_preview = true, -- призрак целевой ячейки
guides = true, -- направляющие выравнивания к краям
},
hit_test = {
alpha_threshold = 0.5, -- прицел игнорирует alpha < порога
},
},
-- Объявления плагинов (WASM, M7): только данные; схему config
-- интерпретирует сам плагин, композитор её не парсит.
plugins = {
{
name = "nihil-worlds-sea",
version = "^1",
config = { map = "sea.glb", spawn = { 0, 1.6, 0 } },
},
},
-- Никогда не интерпретируется: заметки, тулинг, генераторы конфигов.
meta = {
author = "nobody",
},
}
-- Разрешена чистая генерация: Lua исполняется один раз, до `return`.
for _, app in ipairs({ "discord", "slack" }) do
table.insert(config.window_rules, {
match = { app_id = "^" .. app .. "$" },
density = 400,
spawn = { room = "work", face = "camera" },
filters = { mipmaps = true, aniso = 8 },
})
end
nihil.log.info("window_rules: " .. #config.window_rules)
return config

Примечания к примеру:

  • room = "current" — зарезервированное имя «активная комната» (не требует объявления в rooms).
  • spawn.face = "camera" — окно лицом к пользователю (дефолт поведения «окно без правила» из SPEC, здесь настраивается на правило).
  • spawn.anchor = "<имя якоря мира>" — поле-заглушка под M6 (миры: «экран на стене», «стол»); валидируется лениво, якорь может появиться с миром позже.
  • density = { readability = { min_angle = 15 } } — кандидат из SPEC раздел 6 («density по читаемости»), в пример не включён, чтобы не смешивать слои.

Новый член воркспейса: nihil-config (соответствует «крейты core, render, config, plugin-api, world» из SPEC 7.1). nihil-config владеет исполнением Lua, парсингом, валидацией и компиляцией автомата; nihil-core получает ресурс RuleEngine.

// nihil-config/src/lib.rs — эскиз
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Config {
/// Версия СХЕМЫ конфига.
pub version: u32,
#[serde(default)] pub general: General,
#[serde(default)] pub window_rules: Vec<WindowRule>,
#[serde(default)] pub hotkeys: Vec<Hotkey>,
#[serde(default)] pub rooms: Vec<RoomDef>,
#[serde(default)] pub panels: Vec<PanelDef>,
#[serde(default)] pub behavior: Behavior,
#[serde(default)] pub plugins: Vec<PluginDecl>,
/// Свободная форма, никогда не интерпретируется.
#[serde(default)] pub meta: BTreeMap<String, serde_json::Value>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct General {
pub density_default: f32, // px/m; SPEC вопрос 10
pub camera: CameraCfg,
pub vram: VramCfg,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct CameraCfg {
pub vfov_deg: f32, // (0, 180)
pub eye_height: f32, // м
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct VramCfg {
pub mode: VramMode, // Degrade | Refuse — решено ✅ в SPEC
pub warn_mb: u64, // порог предупреждения, не лимит
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct WindowRule {
#[serde(rename = "match")]
pub m: MatchDef,
pub density: Option<f32>,
pub render_scale: Option<RenderScale>,
/// Для оценки VRAM на этапе загрузки конфига.
pub est_size: Option<[u32; 2]>,
pub spawn: Option<SpawnDef>,
pub filters: Option<Filters>,
pub transparency: Option<Transparency>,
/// Game Mode по правилу (SPEC раздел 2).
#[serde(default)] pub game_mode: bool,
/// Когда правило переприменяется: Never | Map (дефолт, SPEC) | Reload (💡).
#[serde(default)] pub reapply: Reapply,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MatchDef {
/// regex по app_id.
pub app_id: Option<String>,
/// regex по initial title.
pub title: Option<String>,
/// Явная опция: матч по живому title (SPEC раздел 4).
#[serde(default)] pub live_title: bool,
}
/// `2` → Manual(2.0); `{ auto = true }` → Auto (множитель от дистанции/угла).
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum RenderScale {
Manual(f32),
Auto { auto: bool },
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct SpawnDef {
pub room: Option<String>, // имя комнаты или "current"
pub pos: Option<[f32; 3]>, // м
pub yaw: Option<f32>, // градусы
pub face: Face, // Camera | None
pub anchor: Option<String>, // якорь мира, M6, ленивая валидация
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct Filters {
pub mipmaps: bool,
pub aniso: u8,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct Transparency {
pub multiplier: f32, // множитель прозрачности на окно
pub hit_threshold: f32, // alpha-порог прицела
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Hotkey {
/// Парсится из строки: "Super+Return", "Shift Shift", "Super+K C".
pub keys: KeySeq,
/// Окно double-tap / пауза между клавишами chord'а.
#[serde(default = "default_tap_ms")]
pub tap_ms: u32,
pub on: Action,
#[serde(default)] pub description: String,
}
#[derive(Debug, Deserialize)]
#[serde(tag = "type")] // { type = "spawn", command = ... }
pub enum Action {
Spawn { command: String },
Mode { mode: ModeName }, // Spatial | Walk | Cluster | ...
Room { room: String },
Layout { name: String }, // именованные раскладки (SPEC раздел 2)
Focus { target: FocusTarget }, // next | immersive | ...
Density { value: f32 }, // density сфокусированного окна
/// Команда, объявленная плагином. Если плагина нет — warning, не отказ.
Plugin { command: String, #[serde(default)] args: serde_json::Value },
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RoomDef {
pub name: String,
pub bounds: BoundsDef, // min/max, XZ-бокс
pub grid: GridDef, // cell, snap_deg
pub camera_spawn: CameraSpawnDef, // pos, yaw, pitch
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PanelDef {
#[serde(rename = "match")]
pub m: NamespaceMatch, // regex по namespace layer-shell
pub anchor: AnchorKind, // World | Head | Body
#[serde(default)] pub room: Option<String>, // только для World
#[serde(default)] pub pos: Option<[f32; 3]>, // World: позиция в комнате
#[serde(default)] pub yaw: Option<f32>,
#[serde(default)] pub offset: Option<[f32; 3]>, // Head/Body: офсет от камеры
#[serde(default)] pub follow: FollowCfg, // Body: сглаживание
#[serde(default)] pub show: ShowCfg, // hotkey + gaze (оба сразу)
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct Behavior {
pub drag: DragCfg, // snap_modifier, snap_deg, grid_overlay,
// ghost_preview, guides
pub hit_test: HitTestCfg, // alpha_threshold
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PluginDecl {
pub name: String,
pub version: String, // semver-диапазон
/// Непрозрачная схема — интерпретируется плагином, не композитором.
#[serde(default)] pub config: serde_json::Value,
}

Соответствие существующим типам nihil-core: RoomDef → Room (nihil-core/src/room.rs), поля density/render_scale → WindowQuad (nihil-core/src/window.rs), PanelDef/поведение → PanelConfig и флаги drag.rs. Lua-таблица сериализуется в Config напрямую (mlua умеет serde-десериализацию значений; см. раздел 8).

4. Компиляция: что становится автоматом, а что данными

Section titled “4. Компиляция: что становится автоматом, а что данными”

Терминология SPEC «единый автомат» читается здесь как один компилируемый артефакт, собираемый одним конвейером и подменяемый одним атомарным действием; внутри артефакта — несколько автоматов. Буквальное слияние в один DFA (произведение языков оконных правил и клавиатурных последовательностей) возможно, но не рекомендуется — вопрос 1 раздела 9.

Поле конфига Куда попадает
window_rules[].match.app_id DFA №1: regex-automata, new_many
window_rules[].match.title DFA №2: regex-automata, new_many
window_rules[].match.live_title флаг рантайма (когда звать матчер)
hotkeys[].keys (+ tap_ms) DFA №3: KeyDfa с таймаутами
hotkeys[].on таблица действий, индексируется accept-состоянием
density, render_scale, est_size, spawn, filters, transparency, game_mode, reapply данные, подвешены к правилу по индексу
rooms, panels, behavior, general, plugins, meta данные, в автоматы не идут
// nihil-config/src/compile.rs — эскиз
pub struct CompiledConfig {
/// DFA по app_id: new_many(patterns), pattern_id → rule slot.
pub app_id_dfa: regex_automata::dfa::regex::Regex,
/// DFA по title: аналогично.
pub title_dfa: regex_automata::dfa::regex::Regex,
/// Клавиатурный автомат: последовательности, chords, double-tap.
pub key_dfa: KeyDfa,
/// Эффекты правил по индексу (всё, что не автомат).
pub rules: Vec<ResolvedRule>,
pub general: General,
pub rooms: Vec<ResolvedRoom>,
pub panels: Vec<ResolvedPanel>,
pub behavior: Behavior,
pub plugins: Vec<PluginDecl>,
/// Предупреждения, требующие подтверждения (VRAM и т.п.).
pub confirmations: Vec<Confirmation>,
}
pub struct KeyDfa {
states: Vec<KeyState>,
start: StateId,
}
pub struct KeyState {
/// (keysym, modmask) → state. Разреженная карта: алфавит большой,
/// полной таблицы переходов не строим.
transitions: HashMap<KeyInput, StateId>,
/// Индекс Action, если состояние принимающее.
accept: Option<ActionIdx>,
/// Сброс, если пауза между нажатиями превысила (double-tap, chord).
deadline: Option<Duration>,
}
/// Матч окна при мапе: два прохода по готовым таблицам, без аллокаций.
pub fn match_window(&self, app_id: &str, title: &str) -> Option<RuleIdx>;

Детали:

  • Матчинг окон. regex-automata dfa::regex::Regex::new_many строит один DFA на набор паттернов и возвращает номер совпавшего паттерна. Правило совпало, если совпали его паттерны app_id (если задан) И title (если задан). Порядок правил — приоритет: первое подходящее. Вопрос leftmost-first семантики new_many vs наш порядок — вопрос 3 раздела 9. Сериализация DFA (to_bytes_limited у нижележащего dfa::DFA) — уже в SPEC 7.2.
  • KeyDfa. Алфавит — (keysym, modmask). Каждая hotkey-последовательность — слово; DFA распознаёт объединение языков. Модификаторы внутри токена (Super+K) — один символ алфавита; пробел — следующий шаг последовательности; double-tap = повтор символа, помеченного deadline (= tap_ms). Чистый DFA не умеет время, поэтому время живёт как аннотация переходов, а рантайм интерпретирует его: это «timed DFA» (модель трансдюсера) — вопрос 2 раздела 9.
  • Рантайм. nihil-core получает ресурс RuleEngine(ArcSwap<CompiledConfig>). Ввод и рендер читают engine.load_full() — это атомарная загрузка указателя, наносекунды; аллокаций нет.

5. Горячая перезагрузка

Section titled “5. Горячая перезагрузка”

Требование SPEC: пересборка автомата в фоне, атомарная подмена, без пропущенного кадра.

  1. Триггер: inotify на каталоге конфига или nihilctl reload через IPC (IPC с первого дня — SPEC раздел 2).
  2. Сборка в фоне (не главный поток, не рендер-поток): песочница Lua → Config (строгий парс) → валидация (компиляция regex, кросс-ссылки, оценки VRAM/DFA) → CompiledConfig. Ошибка на любом шаге: старый конфиг продолжает работать, ошибка — в лог + IPC-событие.
  3. Подтверждения. Если есть confirmations (например VRAM-оценка выше порога) — артефакт НЕ подменяется до подтверждения; показывается оверлей композитора / уведомление / nihilctl confirm. До ответа работает старый конфиг. Неинтерактивный старт — вопрос 5 раздела 9.
  4. Подмена: одна запись ArcSwap::store(Arc<CompiledConfig>). Чтобы кадр не увидел «половину» конфигов, подмена ставится в очередь и применяется в первой системе PreUpdate следующего кадра — кадр всегда согласован, кадр не пропускается.
  5. Сбросы: текущая незавершённая последовательность хоткеев (например Super+K …) сбрасывается в стартовое состояние — иначе остаток интерпретировался бы новым автоматом.
  6. Что меняется сразу: хоткеи (DFA читается на каждом событии ввода), поведение, панели, комнаты, камера, general — со следующего события/кадра.
  7. Что не меняется: уже смапленные окна. Правило закреплено за окном до ремапа — это ✅ из SPEC раздела 4 (живой title не перезапускает правила). live_title-правила продолжают матчить живой title, но текущим автоматом.
  8. Поправка-кандидат 💡: reapply = "reload" на правиле — переприменить render_scale/filters/transparency к уже смапленным окнам, совпавшим с правилом, в момент подмены (с новым подтверждением VRAM). Дефолт "map" строго по SPEC. Нужно для сценария «уменьшил множитель в конфиге — перезагрузил — видеопамять освободилась немедленно», иначе ждать ремапа. Поля, определяющие геометрию (spawn, density), в "reload" НЕ входят: поза живого окна принадлежит state-файлу и руке пользователя, не конфигу.

Порядок приоритетов при ремапе окна: поза из state-файла (если ключ найден) → spawn правила → дефолт «перед камерой на ~3 м, лицом к пользователю». Подтвердить — вопрос 9 раздела 9.

6. Валидация и предупреждения

Section titled “6. Валидация и предупреждения”

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

Проверка Уровень Поведение
Синтаксис Lua / выход не-таблица отказ ошибка с номером строки
Битый regex (match.*, namespace) отказ ошибка: правило №, поле, паттерн, позиция
version > поддерживаемой предупреждение терпимый парс (неизвестные поля игнорируются с логом)
Неизвестное поле (при version ≤ поддерживаемой) отказ deny_unknown_fields: защита от опечаток
Неизвестный enum-вариант (mode, anchor, action.type) отказ защита от опечаток
Кросс-ссылки: room правила/hotkey → имя из rooms, layout → именованная раскладка, plugin-команда → объявленный плагин отказ (plugin — warning) Plugin-action без плагина: warning + игнор
vfov вне (0, 180), density ≤ 0, cell ≤ 0, tap_ms = 0 отказ физически бессмысленные значения
density < 50 или > 5000 предупреждение эргономика, не запрет
render_scale ≥ 4 или оценка VRAM > warn_mb предупреждение + подтверждение диалог с оценкой (ниже)
Размер DFA (сериализованный) > бюджета предупреждение + подтверждение детерминизация может взорваться; бюджет настраивается
aniso > максимума драйвера warning + clamp драйвер запрашивается при старте

Оценка VRAM (диалог подтверждения, SPEC раздел 3): на окно ≈ est_w × est_h × scale² × 4 байта × 2 буфера (+10% мипмапы). Если est_size не задан — берётся 1920×1080 (warning «оценка по дефолтному размеру; задайте est_size для точности»). Диалог показывает «на окно ~X МБ; при N типичных окнах ~Y МБ». После мапа реальных окон при нехватке — режим из general.vram.mode (degrade/refuse, решено ✅).

Неизвестные поля — терпеть или reject? Предлагается строгий по умолчанию (опечатка в densitty должна ловиться), терпимый при version > поддерживаемой (конфиг от более нового Nihil на старом — forward-compat, SPEC раздел 2 «Совместимость»). Свободные зоны: meta и plugins[].config.

  • Глобальный стол nihil: nihil.log.info/warn (в лог композитора), nihil.version (semver композитора — условная генерация конфига под версию, авторинг forward-compat). Больше ничего: никакого require, io, os, debug, dofile, loadfile.
  • Бюджеты: памяти (mlua set_memory_limit / Luau нативно), инструкций (hook), wall-clock таймаут ~5 с. Скрипт исполняется один раз — бюджеты дешёвые.
  • Выполнение — в фоновом потоке при загрузке/перезагрузке, никогда в кадре.

mlua — зрелые биндинги Rust↔Lua с фичами под Lua 5.4 / LuaJIT / Luau (feature luau), умеет serde-десериализацию Lua-значений прямо в Config, но песочница собирается вручную (белый список globals + instruction hook) — это код, который надо не забыть поддерживать; Luau как язык — интерпретатор Roblox’а (crate luau, менее battle-tested вне Roblox-мира), зато песочница by design (нет io/os в VM), плюс строгая типизация --!strict, которая даёт проверку конфигов редактором/CI ещё до запуска композитора, и syntax sugar (string interpolation, +=), немного отходящий от vanilla Lua. Практически замечание: mlua+feature luau даёт Luau-VM с Rust-API и serde — вероятная точка сходимости. Решение — вопрос SPEC 7.2 («выбор Lua для Rust»), здесь не фиксируется.

9. Открытые вопросы (для SPEC раздел 7)

Section titled “9. Открытые вопросы (для SPEC раздел 7)”
  1. «Единый автомат» (SPEC раздел 2): буквально один DFA — произведение языков оконных правил и клавиатурных последовательностей — или один артефакт CompiledConfig с несколькими автоматами (предложено здесь)?
  2. Тайминги double-tap: время живёт аннотациями переходов «timed DFA» — константа рантайма или поле правила (tap_ms)? Согласовать с «чистотой» DFA.
  3. Приоритет правил при пересечении: первое по порядку (предложено) vs leftmost-longest из new_many vs most-specific; нужен ли явный priority?
  4. reapply = "reload" (поправка к ✅ «матчинг только при мапе»): допустимо ли переприменять render_scale/filters без ремапа, или строго ждать мап?
  5. Подтверждение опасных значений при неинтерактивном старте/перезагрузке: кто показывает диалог (оверлей? CLI? notification) и что происходит до подтверждения — предложено: работает старый конфиг.
  6. Оценка VRAM: по est_size правила (предложено) или по первому реальному буферу окна (точнее, но позже)? Учитывать ли количество окон?
  7. Неизвестные поля: строгий по умолчанию + терпимость при version > поддерживаемой — подтвердить политику.
  8. Конфликты хоткеев (две последовательности с общим префиксом или полным совпадением): отклонять при компиляции или побеждает первое?
  9. Приоритет при ремапе: state-файл > spawn правила > дефолт (предложено) — подтвердить; связать с дизамбигуацией ключей (SPEC 7.1).
  10. Имена клавиш: xkb-имена (Super+Return, как в примере) — подтвердить; жесты для показа панелей (SPEC «и по клавише/жесту») — вне v1?
  11. Квота DFA: размер таблиц — предупреждение с подтверждением (предложено, по правилу «нет скрытых лимитов») или жёсткий предел?
  12. Сетевые настройки (общие комнаты, права из PROTOCOL.md) — секция в nihil.lua или отдельный файл? Не спроектированы (N0).
  13. plugins[].config —opaque Value; формат манифеста плагина (WIT, права, версии) — отдельный черновик (SPEC 7.1).
  • Жесты ввода (touch, controller), настройки миров/якорей (M6 — формат манифеста мира ещё открыт, SPEC 7.1).
  • Сетевые/мультиплеер настройки (N0, PROTOCOL.md).
  • Переопределение конфига плагинами: плагин добавляет правила — вопрос 8 SPEC («граница Lua/плагин»); предлагается: плагин возвращает такие же декларативные таблицы, конвейер компиляции общий. Детали — с API плагинов.