// inkhaven project configuration
// HJSON: comments, unquoted keys, optional commas.

{
  // Primary writing language of this project. Drives Snowball stemmers
  // for the Places / Characters highlight overlay AND the default F7
  // grammar-check prompt. Accepts any name handled by the Snowball
  // algorithm picker — arabic, danish, dutch, english, finnish, french,
  // german, greek, hungarian, italian, norwegian, portuguese, romanian,
  // russian, spanish, swedish, tamil, turkish.
  //
  // Empty string falls back to `editor.stemming.languages`.
  language: english

  embeddings: {
    // fastembed model. Defaults to a multilingual model with strong Russian
    // support. Other options: MultilingualE5Base, MultilingualE5Large, BGEM3.
    model: MultilingualE5Small
    chunk_size: 800
    chunk_overlap: 0.15
  }

  llm: {
    // Provider used by the AI pane when no explicit override is passed.
    // Cycle via Ctrl+B L to pick a different one without leaving the TUI.
    //
    // genai (the multi-provider client) routes by model name: `gpt-*`
    // / `o1-*` → OpenAI, `claude-*` → Anthropic, `gemini-*` → Gemini,
    // `grok-*` → xAI, `deepseek-*` → DeepSeek, anything else → Ollama.
    // Add / rename providers freely — only the model string + env var
    // matter to the dispatcher.
    default: gemini
    providers: {
      gemini: {
        model: gemini-2.5-pro
        api_key_env: GEMINI_API_KEY
      }
      claude: {
        model: claude-sonnet-4-5
        api_key_env: ANTHROPIC_API_KEY
      }
      openai: {
        // `gpt-4o-mini` for cheaper / faster; `gpt-5-pro` once
        // generally available. genai picks Responses-API vs
        // Chat-Completions adapter automatically from the model name.
        model: gpt-4o
        api_key_env: OPENAI_API_KEY
      }
      deepseek: {
        model: deepseek-chat
        api_key_env: DEEPSEEK_API_KEY
      }
      grok: {
        // xAI's Grok. Hits api.x.ai/v1 via genai's OpenAI-compatible
        // Xai adapter.
        model: grok-2-latest
        api_key_env: XAI_API_KEY
      }
      // Local Ollama at the default endpoint (http://localhost:11434).
      // No API key is needed — omit `api_key_env`. genai picks the Ollama
      // adapter from the model name. Pull a model first with e.g.
      // `ollama pull llama3.2`, then set `default: ollama` above.
      ollama: {
        model: llama3.2
      }
    }
  }

  editor: {
    theme: default
    tab_width: 2
    wrap: true
    // Seconds of editor inactivity before the current paragraph is
    // autosaved. Set to 0 to disable idle autosave; Ctrl+S, Ctrl+Q, and
    // paragraph-switch autosaves still fire.
    autosave_seconds: 5

    // Auto-close brackets and quotes.
    //  * Typing `(`, `[`, `{`, `"`, `'` inserts the matching close-char
    //    and parks the cursor between them.
    //  * Typing the matching close-char when it's already the next
    //    character on the line steps over it instead of duplicating.
    //  * Enter inside `()`, `[]` or `{}` expands to three indented
    //    lines (cursor lands on the middle one). Indent size = `tab_width`.
    //  * Backspace on the inside of a freshly-typed pair removes
    //    both halves.
    // Set false to disable the whole behaviour.
    auto_close_pairs: true

    // Snowball stemmer languages used to expand the Places / Characters
    // highlight overlay. With `russian` configured a paragraph titled
    // "Москва" also lights up "Москве", "Москвою", etc.; with `english`
    // a paragraph titled "cities" matches "city".
    //
    // Supported: arabic, danish, dutch, english, finnish, french, german,
    // greek, hungarian, italian, norwegian, portuguese, romanian, russian,
    // spanish, swedish, tamil, turkish. Empty list disables stemming and
    // falls back to exact-phrase matching.
    stemming: {
      languages: [
        "english"
        "russian"
      ]
    }
  }

  hierarchy: {
    // false => Book → Chapter → Subchapter → Paragraph (default).
    // true  => Subchapter may nest arbitrarily before terminating in Paragraph.
    unbounded_subchapters: false
  }

  // Background HNSW flush cadence in seconds. Acts as a safety net —
  // every explicit save already syncs. The tick is a no-op when the
  // index has no writes since the last sync (dirty-flag short-circuit),
  // so a generous interval costs nothing during idle. 0 disables.
  sync_interval_seconds: 600

  // Bund scripting sandbox. Inkhaven exposes the Bund language
  // (`inkhaven bund <code>`, plus later phases for save-hooks and
  // user-authored scripts) and applies a category-based deny list
  // to the registered words.
  //
  // Default (no entries below): destructive categories
  // `fs_write`, `net`, `shell`, `code_eval` are denied; the other
  // categories (`store_read`, `store_write`, `fs_read`) are
  // allowed. Currently inkhaven only ships `store_read` words.
  //
  // Resolution: enabled_words > disabled_words > disabled_categories.
  scripting: {
    // Additional categories to deny on top of the built-in defaults.
    // Choices: store_read, store_write, fs_read, fs_write, net,
    // shell, code_eval.
    disabled_categories: []
    // Specific word names to deny regardless of their category.
    disabled_words: []
    // Specific word names to allow even when their category is denied.
    // Useful for "give me one tool out of an otherwise-blocked family".
    enabled_words: []
    // Categories to opt INTO from the built-in default-deny list.
    // The defaults deny `fs_write`, `net`, `shell`, `code_eval`,
    // and `keymap`; entries listed here are subtracted from that
    // baseline so a user can grant a single family without
    // unlocking everything with `no_default_deny`.
    //   Example: enabled_categories: ["keymap"]
    enabled_categories: []
    // Power-user switch: when true, the built-in default deny list
    // (fs_write, net, shell, code_eval) is cleared and only
    // `disabled_*` above apply. Off in stock projects.
    no_default_deny: false
    // Bund script run once after the VM is built, after stdlib
    // registration, and after policy application. Natural home for
    // hook lambdas. Hook signatures:
    //
    //   hook.on_create   ( uuid kind -- )
    //   hook.on_save     ( uuid -- )
    //   hook.on_rename   ( uuid new_title -- )
    //   hook.on_snapshot ( parent_uuid snapshot_uuid -- )
    //   hook.on_delete   ( uuid -- )   (fires once per deleted id)
    //
    // Lambda syntax in bundcore-vanilla:
    //   "name" { body } register
    //
    // Example — print a line whenever a paragraph is saved:
    //   bootstrap: '''
    //     "hook.on_save" { drop "saved" println } register
    //   '''
    bootstrap: ""
  }

  // Auto-backup on TUI exit. The TUI checks `.inkhaven-backup.json` for
  // the last successful backup timestamp; if older than `max_age`, the
  // store is closed, a splash with a progress bar is shown, and a fresh
  // zip is written to `out_dir`. `inkhaven backup --out <dir>` triggers
  // the same flow manually at any time.
  //
  // - `out_dir`: empty string (default) → resolved at runtime to
  //   `<parent-of-project>/inkhaven-backups/<project-basename>/`
  //   (sibling directory next to the project — visible in any file
  //   manager so backups are easy to copy off to external storage).
  //   Absolute paths used verbatim; relative paths resolve against the
  //   project root if you really want backups inside the project.
  // - `max_age`: humantime — "1d", "12h", "30m", "1w" all work. Set to
  //   "0s" to disable the on-exit hook entirely (manual
  //   `inkhaven backup` still works).
  backup: {
    out_dir: ""
    max_age: "7d"
    // 1.2.6+: when a backup finishes (manual Ctrl+B Shift+B or the
    // exit-hook auto-backup), hold the splash with a
    // "Press any key to continue…" prompt so the destination path
    // is readable. Set false to keep the 1.2.5 auto-dismiss.
    wait_for_key_after_backup: true
  }

  // Ctrl+B O multi-format hookup. Each format in `extra_formats`
  // is generated alongside the PDF using the same combined Typst
  // source. Supported (case-insensitive): "markdown", "tex" (via
  // the tylax converter), "epub" (markdown intermediate, zipped).
  // Unknown entries log a WARN and are skipped. Per-format errors
  // are reported on the status bar but never abort the take —
  // the PDF the user asked for is already on disk before extras
  // run. Empty list (the default) = PDF-only, same as 1.2.2.
  output: {
    extra_formats: []
    // 1.2.6+ — hold each extras step on screen for this many
    // milliseconds during the Ctrl+B O splash so the
    // markdown → tex → epub transitions are visible. Set to
    // 0 to disable the artificial pause.
    extras_step_pause_ms: 400
    // 1.2.6+ — when true, the final all-✓ extras frame holds
    // until any key press (same shape as
    // typst_compile.wait_for_key_after_compile). Default
    // false so batch `Ctrl+B O` doesn't trap on a key prompt;
    // flip to true for screenshots / demos.
    extras_wait_for_key: false
  }

  // Writing-progress goals — fuels the status-bar widget (today
  // / streak / per-book pace) and the Ctrl+V G modal (sparkline,
  // status-ladder bar, deadline forecasting). All fields are
  // optional; commenting them out / leaving zero / leaving empty
  // disables that particular goal but still records events so
  // the modal has history to show.
  //
  // - daily_words: project-wide target. The status-bar shows
  //   `today N/M words` when set.
  // - streak_grace_per_week: missed days forgiven inside a
  //   rolling 7-day window before the streak breaks. 0 = strict,
  //   1 = one rest day allowed per week.
  // - books: per-book targets keyed by book SLUG (matches the
  //   second `[..]` field in `inkhaven list`). `target_words` +
  //   `deadline` (ISO date YYYY-MM-DD) drives the required-pace
  //   line in the modal.
  // - status_ladder: trailing-7-days promotion targets keyed by
  //   status name lowercased (ready / final / third / second /
  //   first / napkin). Modal shows `→ ready: N/M this week`.
  goals: {
    daily_words: 0
    streak_grace_per_week: 1
    books: {
      // story: { target_words: 80000, deadline: "2026-12-31" }
    }
    status_ladder: {
      // ready:  1
      // final:  3
      // third:  5
    }
    // 1.2.4+: when a save crosses a paragraph's per-paragraph
    // `target_words` (set via Ctrl+V T or
    // `ink.paragraph.set_target`), bump its status one ladder
    // rung. Idempotent per (paragraph, status). Manual Ctrl+B R
    // resets the bookkeeping. Set false to keep promotions
    // manual.
    auto_promote_on_target: true
  }

  // Typewriter-style sound effects. Two events emit a synthesised
  // click: pressing Enter in the editor ("end of line, hit Enter") and
  // the editor pane losing focus ("remove page from machine"). The
  // sounds are generated at runtime — no audio assets needed. Toggle
  // live with Ctrl+B E; the change is written back here.
  //
  // - `enabled`: false by default so new users aren't surprised by
  //   audio on first launch.
  // - `volume`: master multiplier, clamped to 0.0–1.0.
  sound: {
    enabled: false
    volume: 0.6
  }

  // 1.2.6+ — AI-pane behaviour knobs that aren't tied to a
  // specific provider. Defaults match the pre-1.2.6 behaviour
  // for projects that don't override them.
  //
  // - `per_paragraph_memory` (false) — when true, every
  //   Paragraph-scope AI exchange (F12, Ctrl+B G, Ctrl+B P,
  //   one-shot prompts with Paragraph scope, etc) is recorded
  //   onto the paragraph's `Node.ai_memory`. Subsequent
  //   Paragraph-scope prompts on the same paragraph see prior
  //   turns as the model's chat history. Turn budget capped
  //   by `per_paragraph_memory_max_turns`.
  // - `per_paragraph_memory_max_turns` (10) — soft cap on
  //   how many (user, assistant) pairs travel as history.
  //   Oldest turns drop first.
  // - `reseed_prompt_examples` (true) — re-write the
  //   `Prompts/*.example` seed paragraphs on every TUI open
  //   (idempotent — skips entries that already exist as
  //   non-example versions).
  // - `diff_review_on_apply` (true) — pop a side-by-side
  //   diff modal before any buffer-replacing AI apply (`r`,
  //   `g` chords from the result pane). `a` accepts, `r`
  //   rejects, `e` accepts + refocuses the editor.
  ai: {
    per_paragraph_memory:           false
    per_paragraph_memory_max_turns: 10
    reseed_prompt_examples:         true
    diff_review_on_apply:           true
  }

  // 1.2.6+ — story timeline. Off by default. Flip `enabled` to
  // `true` to unlock the timeline surfaces: `Ctrl+V e` event
  // picker, `Ctrl+V Shift+T` swim-lane view, `Ctrl+V Shift+E`
  // new event from any pane, `Ctrl+V Shift+I` edit event
  // timing, and the `inkhaven event add/list/show` CLI. The
  // calendar preset decides how `--start` strings are parsed
  // and rendered:
  //
  // - `gregorian` — real-world Y.M.D (e.g. `2026.1.1`).
  // - `sols`      — single-unit Sol N since day zero
  //   (e.g. `Sol 13`).
  // - `custom`    — define units + seasons + format string
  //   yourself. See Documentation/Tutorials/31-story-timeline.md
  //   for the full custom-calendar schema.
  //
  // `default_track` names the swim lane an event lands on when
  // its `--track` is omitted.
  timeline: {
    enabled: false
    default_track: "main"
    calendar: {
      preset: "gregorian"
    }
    // TIMELINE-2-INTEGRATION — the refactored timeline critique
    // (y / Y / Ctrl+Y / F12 chords, `inkhaven event critique`).
    // Two retained, timeline-internal checks emit to the Output pane;
    // travel-time / date / pacing now live in WORLD-4/5 +
    // INNER_SOCRATES-1. See Documentation/Tutorials/31-story-timeline.md.
    critique: {
      enabled: true
      orphan: {
        enabled: true
        min_orphan_age_days: 0          // emit immediately
        min_significance: "low"         // 'low' | 'moderate' | 'high'
      }
      fuzzy_overlap: {
        enabled: true
        min_suspicion: "moderate"       // 'low' | 'moderate' | 'high'
        cluster_min_size: 3
      }
      elaboration: {
        enabled: true                   // LLM elaboration when a provider exists
        max_calls_per_run: 20
        confirm_above_calls: 10
      }
      legacy_flag_deprecation: {
        warn_on_use: true               // warn when `critique --legacy` is used
      }
    }
  }

  // Typst wrap function templates used by Book assembly (Ctrl+B A).
  // The strings below are inlined verbatim into the per-book
  // `globals.typ` paragraph the first time a user book is created —
  // edit them here BEFORE running `add book` (or copy your customised
  // wrap_* defs into Typst → <bookname> → globals.typ in the TUI).
  //
  // An empty string falls back to inkhaven's baked-in default for that
  // wrap (`heading(level: ...) ; body` for chapter/subchapter; passthrough
  // for book / paragraph). Leave the defaults until you know what
  // headings, page breaks, and layout you want.
  typst_templates: {
    wrap_book: ""
    wrap_chapter: ""
    wrap_subchapter: ""
    wrap_paragraph: ""
    // Image wrappers — picked by the assembler based on the Image
    // node's parent kind. Empty string falls back to a sensible
    // baked-in default (page-break frontispiece / chapter art /
    // centered section image / inline figure).
    wrap_image_book: ""
    wrap_image_chapter: ""
    wrap_image_subchapter: ""
    wrap_image_inline: ""
  }

  // HJSON-driven `settings.typ`. `Ctrl+B A` synthesises the
  // generated header in artefacts/<book>/settings.typ from these
  // three stanzas — change values here instead of hand-editing the
  // Typst paragraph. The user's `Typst → <book> → settings.typ`
  // paragraph still applies BELOW the auto-generated header as a
  // free-form override (anything HJSON doesn't yet expose).
  //
  // typst_page → #set page(...)
  // typst_fonts → #set text(...) + #show raw: set text(font: ...)
  //               (typst 0.11+ — `raw` itself has no `font:` parameter)
  // typst_layout → #set par(...) and optional #set heading(numbering)
  //
  // Font defaults are typst's own bundled fonts so a fresh
  // project compiles cleanly on a vanilla host with no extra
  // font installs. Override below to taste — the synthesised
  // header always pairs your pick with the bundled font as a
  // fallback array, so a missing primary font on the host
  // still produces a PDF (just with a `warning: unknown font
  // family` from typst).
  typst_page: {
    paper: "us-letter"
    margin_top: "2.5cm"
    margin_bottom: "2.5cm"
    margin_inside: "3cm"
    margin_outside: "2cm"
    page_numbering: "1"
    columns: 1
  }
  typst_fonts: {
    body: "Linux Libertine"
    body_size: "11pt"
    monospace: "DejaVu Sans Mono"
    language: "en"
  }
  typst_layout: {
    justify: true
    leading: "0.7em"
    paragraph_indent: ""
    heading_numbering: ""
  }

  // Image-node behaviour (F3 import, Enter-on-Image preview).
  // - `preview_enabled`: when true, Enter on an Image row pops a
  //   ratatui-image preview that uses kitty / sixel / iterm2 /
  //   half-block protocols depending on terminal support. Flip to
  //   false on slow ssh sessions or terminals where the half-block
  //   fallback is too noisy — the status-bar info line then takes
  //   over.
  // - `allowed_extensions`: import filter (F3 picker). Anything not
  //   here keeps the prose treatment.
  // - `max_size_bytes`: soft guard against importing huge files.
  images: {
    preview_enabled: true
    allowed_extensions: ["png", "jpg", "jpeg", "gif", "webp", "svg"]
    max_size_bytes: 33554432
  }

  // `typst compile` integration used by Ctrl+B B (build) and
  // Ctrl+B O (take). When the compile fails, inkhaven opens a fresh
  // AI chat with this system prompt and the captured stderr — the
  // analysis streams into the AI pane automatically.
  //
  // - `error_system_prompt`: empty string falls back to inkhaven's
  //   baked-in default (knows the inkhaven file layout: globals.typ,
  //   book/index.typ, etc.). Override to tighten the prompt to your
  //   own conventions or to a different language.
  typst_compile: {
    error_system_prompt: ""
  }

  keys: {
    save:             Ctrl+s
    search:           Ctrl+/
    ai_prompt:        Ctrl+i
    next_pane:        Tab
    prev_pane:        Shift+Tab
    page_up:          PageUp
    page_down:        PageDown

    // The meta-prefix chord. Pressing this puts the TUI into meta mode;
    // the next keystroke is treated as an action selector:
    //   B/C/S/P  → add book / chapter / subchapter / paragraph
    //   D        → delete the cursor's node (subtree)
    //   ↑/↓      → reorder current node within its siblings
    //   Esc      → cancel meta mode
    // Replaces the old `Ctrl+Shift+*` chords which terminals often eat.
    meta_prefix:      Ctrl+b

    // Bund-meta chord — parallel to meta_prefix, drives scripting:
    //   R   → run the open .bund buffer against Adam
    //   N   → new script under the Scripts system book
    //   E   → eval a one-shot Bund expression (modal prompt)
    //   Esc → cancel
    // Set to an empty string to disable (some users reserve Ctrl+Z
    // for their terminal multiplexer).
    bund_prefix:      Ctrl+z

    // User overlay for meta- and bund-sub-chords. Each entry is
    // `{ chord, action, scope? }`:
    //
    //   chord:  "<prefix> <suffix>" shorthand, e.g. "Ctrl+b y"
    //   action: dotted action name. Examples:
    //             tree.morph_type    editor.save        global.open_credits
    //             ai.clear_chat      bund.run_buffer    none (disable)
    //   scope:  "any" (default) / "editor" / "tree" / "ai"
    //
    // Overlay entries take precedence over the built-in defaults.
    // Hard-blocked: Ctrl+Q (hard-quit), meta_prefix / bund_prefix
    // (configured above, not here).
    //
    // Examples (uncomment to use):
    //   bindings: [
    //     // rebind Ctrl+B Y to type-cycle in every pane
    //     { chord: "Ctrl+b y", action: "tree.morph_type" }
    //     // disable Ctrl+B W (typewriter mode)
    //     { chord: "Ctrl+b w", action: "none" }
    //     // give Bund eval a shorter chord
    //     { chord: "Ctrl+z m", action: "bund.run_buffer" }
    //   ]
    bindings: []
  }

  prompts_file: prompts.hjson

  // Where per-book artefacts (rendered PDFs, build intermediates, …)
  // land. Empty string → resolved at runtime to
  // `<parent-of-project>/inkhaven-artefacts/<project-basename>/`
  // (sibling directory next to the project, mirroring `backup.out_dir`
  // so both user-visible outputs sit next to the project in a file
  // manager). Override with an absolute path for a custom location, or
  // a relative path to keep artefacts inside the project tree (legacy
  // behaviour — remember they're ephemeral and don't belong in backups).
  artefacts_directory: ""

  // Visual theme — every value is a hex RGB triple like "#RRGGBB" (or the
  // short "#RGB" form). Empty strings fall back to the shipped Catppuccin
  // Mocha defaults baked into the binary, so you only need to set the
  // fields you want to override.
  //
  // Tip: terminals that don't speak truecolor (TERM=xterm-256color is fine)
  // render the closest 256-colour neighbour automatically.
  theme: {
    // Pane chrome — base background / text and line-number / current-line
    // accents used by the editor.
    pane_bg:           "#1e1e2e"
    pane_fg:           "#cdd6f4"
    line_number_fg:    "#6c7086"
    current_line_bg:   "#313244"

    // Pane borders. Focused / unfocused get used everywhere; the editor
    // additionally switches between dirty / saved / read-only to telegraph
    // the open paragraph's state at a glance.
    border_focused:    "#cba6f7"
    border_unfocused:  "#45475a"
    border_dirty:      "#f9e2af"
    border_saved:      "#a6e3a1"
    border_readonly:   "#94e2d5"

    // Floating windows (Add / Delete / Rename / FindReplace / QuickRef /
    // FilePicker / Help / PromptPicker). All share these three colours.
    modal_bg:          "#181825"
    modal_fg:          "#cdd6f4"
    modal_border:      "#cba6f7"

    // Places / Characters / Artefacts overlay tints. Words that match a
    // paragraph title nested under the Places (cyan) / Characters (amber)
    // / Artefacts (peach) book light up in these colours inside the
    // editor. Notes matches get a plain underline tinted with the
    // `notes_underline_fg` colour below (default = pane_fg so the
    // underline reads as text).
    places_fg:           "#89dceb"
    characters_fg:       "#f9e2af"
    artefacts_fg:        "#fab387"
    notes_underline_fg:  "#cdd6f4"

    // In-buffer Ctrl+F search highlights (every match / the current one).
    search_match_bg:   "#f38ba8"
    search_current_bg: "#f5c2e7"

    // Tree pane chrome: colour of the ▸ marker on the currently-open
    // paragraph row.
    tree_open_marker:  "#a6e3a1"

    // Tree pane row colour by kind. The cursor REVERSED highlight and
    // the open-paragraph marker still take precedence on the active row.
    tree_book_fg:        "#f5c2e7"
    tree_chapter_fg:     "#89b4fa"
    tree_subchapter_fg:  "#94e2d5"
    tree_paragraph_fg:   "#cdd6f4"
    tree_image_fg:       "#fab387"

    // Editor pane header — the trailing `L… C…` cursor read-out uses
    // this colour so it pops out of the title.
    editor_position_fg:  "#89dceb"

    // AI pane header — the `scope=` (F9) and `infer=` (F10) chips use
    // these colours so the active modes are visible at a glance.
    ai_scope_fg:         "#fab387"
    ai_infer_fg:         "#94e2d5"

    // Foreground colour applied to characters that differ from the
    // pre-grammar-check baseline after `T` in the AI pane overwrites the
    // buffer with the model's corrected paragraph. Stays visible until
    // save (implicit accept) or paragraph switch.
    grammar_change_fg: "#f38ba8"

    // Typst syntax highlight palette. Drives the colours applied to
    // tree-sitter-typst's tagged regions in the editor buffer.
    syntax_heading:     "#cba6f7"
    syntax_bold:        "#f9e2af"
    syntax_italic:      "#94e2d5"
    syntax_string:      "#a6e3a1"
    syntax_number:      "#fab387"
    syntax_comment:     "#6c7086"
    syntax_keyword:     "#cba6f7"
    syntax_function:    "#89dceb"
    syntax_operator:    "#94e2d5"
    syntax_list_marker: "#cba6f7"
    syntax_raw:         "#fab387"
    syntax_tag:         "#89b4fa"
    syntax_quote:       "#9399b2"
  }
}
