refactor: maintainability pass — dedup, central env config, cmd helpers, tests, ARCHITECTURE.md

User asked the code stays easy to fix/extend as the feature surface grows.
This commit refactors what was duplicated, centralises what was scattered,
adds tests where there were none, and writes the architecture doc that
contributors will need.

Deduplication
  - tts::play_wav extracted to tts/mod.rs as pub(crate). Was identical in
    piper.rs and silero.rs (~25 lines × 2). Both backends now call super::play_wav.

Centralised env config
  - new crates/jarvis-core/src/runtime_config.rs — single doc-file for every
    JARVIS_* / GROQ_* / OLLAMA_* env var. Includes:
      - ENV_* constants with doc comments
      - get(name) / get_bool(name, default) / get_parse(name, default) helpers
      - feature-flag wrappers: llm_tts_enabled(), llm_router_enabled(),
        llm_router_threshold()
      - log_effective_config() prints active values on startup
  - migrated llm_fallback (JARVIS_LLM_TTS), llm_router (JARVIS_LLM_ROUTER,
    JARVIS_LLM_ROUTER_THRESHOLD) to use the new helpers. Pattern set for
    future migrations.

Lua boilerplate killer
  - new crates/jarvis-core/src/lua/api/cmd.rs exposing:
      jarvis.cmd.ok(msg?)         — play_ok + speak + {chain=false}
      jarvis.cmd.chain_ok(msg?)   — same but chain=true
      jarvis.cmd.error(msg?)      — play_error + speak + {chain=false}
      jarvis.cmd.not_found(msg?)  — play_not_found + speak + {chain=false}
      jarvis.cmd.silent_ok / silent_error
  - refactored 5 packs (daily_briefing/off, memory_pack/list, pomodoro/stop,
    habit_nudge/stop_all, scheduler/clear) — each lost 3-4 lines of repetitive
    play_*/speak/return boilerplate. Pattern for future packs documented in
    ARCHITECTURE.md.

Tests (was 32, now 49)
  - long_term_memory: 8 new tests for normalize_key, search_in (rank, limit,
    empty), build_context_from (empty + populated), serde round-trips for
    MemoryRecord and Store. Extracted pure logic (search_in, build_context_from)
    into pub(crate) functions to enable testing without global state.
  - profiles: 6 new tests for Profile::allows_command (empty/whitelist/blacklist/
    deny-wins-over-allow), serde round-trip with all fields + minimal-fields
    tolerance via #[serde(default)].
  - runtime_config: 2 tests for get_bool / get_parse defaults.
  - All 49 tests pass.

New mood/energy log pack
  - resources/commands/mood_log/ with 2 commands:
      mood.record  "запиши настроение 7" / "сегодня мне грустно" → stores
                   timestamped entry via jarvis.memory.remember
      mood.recap   "как прошла неделя" → LLM summarises last 30 entries
  - Showcase: composes memory + llm + cmd helpers in <30 lines per script.

ARCHITECTURE.md (new, 250 lines)
  - Crate layout, data flow diagram (mic→action 10 steps), per-module
    responsibility table, configuration layers, TTS pipeline diagram,
    Lua sandbox details with API quick-ref, background-services overview,
    "how to add a pack/feature/TTS backend" recipes, test coverage map,
    build instructions with MSVC env, git workflow with Forgejo NO_PROXY trick.
  - Aimed at someone who just cloned the repo and needs to fix a bug fast.

Build: cargo build --release -p jarvis-app -p jarvis-gui both green.
Tests: 49/49 jarvis-core unit tests pass.
This commit is contained in:
Bossiara13 2026-05-15 16:06:18 +03:00
parent 45243c3e3c
commit 6225198821
22 changed files with 891 additions and 93 deletions

View file

@ -0,0 +1,39 @@
# Mood / energy log — daily journal entries via voice.
# Each entry stored via jarvis.memory with key = "mood_<unix_ts>" so it survives restart.
[[commands]]
id = "mood.record"
type = "lua"
script = "record.lua"
sandbox = "standard"
timeout = 5000
[commands.phrases]
ru = [
"запиши настроение",
"настроение",
"запиши настроение на",
"сегодня мне",
"мой день был",
"мне сейчас",
]
en = ["log mood", "my mood is", "I feel"]
ua = ["запиши настрій", "мій настрій"]
[[commands]]
id = "mood.recap"
type = "lua"
script = "recap.lua"
sandbox = "standard"
timeout = 15000
[commands.phrases]
ru = [
"как прошла неделя",
"недельный отчет настроения",
"сводка настроения",
"что я писал про настроение",
]
en = ["mood recap", "weekly mood summary", "how was my week"]
ua = ["підсумок настрою"]

View file

@ -0,0 +1,33 @@
-- Pull all mood_* entries, ask LLM to summarize trends.
local all = jarvis.memory.all()
local moods = {}
for _, rec in ipairs(all) do
if rec.key:sub(1, 5) == "mood_" then
table.insert(moods, rec.value)
end
end
if #moods == 0 then
return jarvis.cmd.not_found("Записей о настроении пока нет.")
end
-- Trim to most-recent-ish 30 entries (memory.all isn't ordered, but mood_<ts> keys sort)
table.sort(moods)
local recent = {}
local start = math.max(1, #moods - 30 + 1)
for i = start, #moods do
table.insert(recent, moods[i])
end
local joined = table.concat(recent, "\n")
local summary = jarvis.llm({
{ role = "system", content = "Ты — психотерапевт-помощник. На основе записей пользователя о настроении дай короткую сводку (3-5 предложений) на русском: общий тон, тренды, выдели хороший/плохой день. Без банальностей, без советов." },
{ role = "user", content = "Записи:\n" .. joined }
}, { max_tokens = 250, temperature = 0.5 })
if not summary or summary == "" then
return jarvis.cmd.error("Не получилось обобщить. Возможно, нет связи с LLM.")
end
return jarvis.cmd.ok(summary)

View file

@ -0,0 +1,37 @@
-- "Запиши настроение 7" / "сегодня мне грустно" / "настроение восемь, продуктивно"
local phrase = (jarvis.context.phrase or "")
local body = jarvis.text.strip_trigger(phrase:lower(), {
"запиши настроение на",
"запиши настроение",
"недельный отчет",
"настроение",
"сегодня мне",
"мой день был",
"мне сейчас",
"log mood",
"my mood is",
"i feel",
"запиши настрій",
})
body = body:gsub("^[%s,:%.]+", ""):gsub("%s+$", "")
if body == "" then
return jarvis.cmd.error("Что записать про настроение?")
end
local t = jarvis.context.time
local stamp = string.format("%04d-%02d-%02d %02d:%02d",
t.year, t.month, t.day, t.hour, t.minute)
local key = "mood_" .. (t.timestamp or os.time())
local value = stamp .. "" .. body
local ok, err = pcall(function()
jarvis.memory.remember(key, value)
end)
if not ok then
jarvis.log("warn", "mood.record: " .. tostring(err))
return jarvis.cmd.error("Не получилось записать.")
end
return jarvis.cmd.ok("Записал.")