Nihil: черновик API конфига — кандидат 💡
Nihil: черновик API конфига — кандидат 💡
Section titled “Nihil: черновик API конфига — кандидат 💡”Статус: кандидат 💡, не решение. Спроектировано по SPEC раздел 4 (решение ✅, реализация 💡). Связанные вопросы SPEC: 8 (граница «Lua декларирует / плагин исполняет»), 10 (масштаб мира / дефолт density). Сетевые настройки (PROTOCOL.md) здесь не рассматриваются — отдельный черновик, когда N0.
Инвариант из SPEC, соблюдённый везде ниже: Lua исполняется ровно один раз при загрузке и возвращает декларативные данные. Никаких колбэков на события. Динамика — плагины (WASM, M7), конфиг остаётся декларативным.
1. Принципы
Section titled “1. Принципы”- Скрипт возвращает одну таблицу — точку истины. Не «регистрация через
side-effect API» (
nihil.add_rule(...)), аreturn config. Побочные вызовы запрещены песочницей (раздел 7). - Разрешена чистая генерация данных: циклы, конкатенация строк (сборка regex), арифметика. Всё, что детерминированно и завершается — можно.
- Расширяемость через
version+ append-only поля: новые опциональные поля добавляются без bump версии; bump — только при удалении/смене смысла поля (правила эволюции как в PROTOCOL.md, там это уже решили для сети — берём ту же дисциплину). - «Нет скрытых лимитов» (SPEC правило для агентов 4): опасные значения разрешены, но проходят через предупреждение с оценкой и подтверждением (раздел 6).
- Две плоскости: автомат (regex-правила окон, последовательности клавиш) и значения (density, fov, комнаты, панели, поведение). Раздел 4 проводит границу явно.
2. nihil.lua: полный пример
Section titled “2. nihil.lua: полный пример”-- 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 по читаемости»), в пример не включён, чтобы не смешивать слои.
3. Rust: модель данных
Section titled “3. Rust: модель данных”Новый член воркспейса: 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-automatadfa::regex::Regex::new_manyстроит один DFA на набор паттернов и возвращает номер совпавшего паттерна. Правило совпало, если совпали его паттерны app_id (если задан) И title (если задан). Порядок правил — приоритет: первое подходящее. Вопрос leftmost-first семантикиnew_manyvs наш порядок — вопрос 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: пересборка автомата в фоне, атомарная подмена, без пропущенного кадра.
- Триггер: inotify на каталоге конфига или
nihilctl reloadчерез IPC (IPC с первого дня — SPEC раздел 2). - Сборка в фоне (не главный поток, не рендер-поток): песочница Lua →
Config(строгий парс) → валидация (компиляция regex, кросс-ссылки, оценки VRAM/DFA) →CompiledConfig. Ошибка на любом шаге: старый конфиг продолжает работать, ошибка — в лог + IPC-событие. - Подтверждения. Если есть
confirmations(например VRAM-оценка выше порога) — артефакт НЕ подменяется до подтверждения; показывается оверлей композитора / уведомление /nihilctl confirm. До ответа работает старый конфиг. Неинтерактивный старт — вопрос 5 раздела 9. - Подмена: одна запись
ArcSwap::store(Arc<CompiledConfig>). Чтобы кадр не увидел «половину» конфигов, подмена ставится в очередь и применяется в первой системеPreUpdateследующего кадра — кадр всегда согласован, кадр не пропускается. - Сбросы: текущая незавершённая последовательность хоткеев (например Super+K …) сбрасывается в стартовое состояние — иначе остаток интерпретировался бы новым автоматом.
- Что меняется сразу: хоткеи (DFA читается на каждом событии ввода), поведение, панели, комнаты, камера, general — со следующего события/кадра.
- Что не меняется: уже смапленные окна. Правило закреплено за окном до
ремапа — это ✅ из SPEC раздела 4 (живой title не перезапускает правила).
live_title-правила продолжают матчить живой title, но текущим автоматом. - Поправка-кандидат 💡:
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.
7. Песочница Lua
Section titled “7. Песочница Lua”- Глобальный стол
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 с. Скрипт исполняется один раз — бюджеты дешёвые. - Выполнение — в фоновом потоке при загрузке/перезагрузке, никогда в кадре.
8. mlua vs Luau (без решения)
Section titled “8. mlua vs Luau (без решения)”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)”- «Единый автомат» (SPEC раздел 2): буквально один DFA — произведение языков
оконных правил и клавиатурных последовательностей — или один артефакт
CompiledConfigс несколькими автоматами (предложено здесь)? - Тайминги double-tap: время живёт аннотациями переходов «timed DFA» —
константа рантайма или поле правила (
tap_ms)? Согласовать с «чистотой» DFA. - Приоритет правил при пересечении: первое по порядку (предложено) vs
leftmost-longest из
new_manyvs most-specific; нужен ли явныйpriority? reapply = "reload"(поправка к ✅ «матчинг только при мапе»): допустимо ли переприменять render_scale/filters без ремапа, или строго ждать мап?- Подтверждение опасных значений при неинтерактивном старте/перезагрузке: кто показывает диалог (оверлей? CLI? notification) и что происходит до подтверждения — предложено: работает старый конфиг.
- Оценка VRAM: по
est_sizeправила (предложено) или по первому реальному буферу окна (точнее, но позже)? Учитывать ли количество окон? - Неизвестные поля: строгий по умолчанию + терпимость при
version> поддерживаемой — подтвердить политику. - Конфликты хоткеев (две последовательности с общим префиксом или полным совпадением): отклонять при компиляции или побеждает первое?
- Приоритет при ремапе: state-файл > spawn правила > дефолт (предложено) — подтвердить; связать с дизамбигуацией ключей (SPEC 7.1).
- Имена клавиш: xkb-имена (
Super+Return, как в примере) — подтвердить; жесты для показа панелей (SPEC «и по клавише/жесту») — вне v1? - Квота DFA: размер таблиц — предупреждение с подтверждением (предложено, по правилу «нет скрытых лимитов») или жёсткий предел?
- Сетевые настройки (общие комнаты, права из PROTOCOL.md) — секция в
nihil.luaили отдельный файл? Не спроектированы (N0). plugins[].config—opaque Value; формат манифеста плагина (WIT, права, версии) — отдельный черновик (SPEC 7.1).
10. Вне рамок v1
Section titled “10. Вне рамок v1”- Жесты ввода (touch, controller), настройки миров/якорей (M6 — формат манифеста мира ещё открыт, SPEC 7.1).
- Сетевые/мультиплеер настройки (N0, PROTOCOL.md).
- Переопределение конфига плагинами: плагин добавляет правила — вопрос 8 SPEC («граница Lua/плагин»); предлагается: плагин возвращает такие же декларативные таблицы, конвейер компиляции общий. Детали — с API плагинов.