esp-modkit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +35 -0
  3. data/LICENSE +21 -0
  4. data/README.md +117 -0
  5. data/docs/architecture.md +125 -0
  6. data/docs/authoring-guide.md +206 -0
  7. data/docs/getting-started.md +183 -0
  8. data/docs/reference/api/active-project.md +22 -0
  9. data/docs/reference/api/agent.md +24 -0
  10. data/docs/reference/api/docs-generator.md +20 -0
  11. data/docs/reference/api/http-server.md +46 -0
  12. data/docs/reference/api/index.md +38 -0
  13. data/docs/reference/api/introspection.md +17 -0
  14. data/docs/reference/api/mcp-installer.md +26 -0
  15. data/docs/reference/api/mcp-server.md +27 -0
  16. data/docs/reference/api/mw-builder.md +14 -0
  17. data/docs/reference/api/mw-data-files.md +20 -0
  18. data/docs/reference/api/mw-dialogue-dsl.md +58 -0
  19. data/docs/reference/api/mw-i18n.md +20 -0
  20. data/docs/reference/api/mw-linter.md +18 -0
  21. data/docs/reference/api/mw-loader.md +26 -0
  22. data/docs/reference/api/mw-openmw-config.md +15 -0
  23. data/docs/reference/api/mw-operations.md +24 -0
  24. data/docs/reference/api/mw-preflight.md +17 -0
  25. data/docs/reference/api/mw-reference-index.md +21 -0
  26. data/docs/reference/api/mw-scaffolder.md +13 -0
  27. data/docs/reference/api/mw-script-blob.md +31 -0
  28. data/docs/reference/api/mw-script-extractor.md +17 -0
  29. data/docs/reference/api/operations.md +25 -0
  30. data/docs/reference/api/plugins.md +24 -0
  31. data/docs/reference/api/preferences.md +13 -0
  32. data/docs/reference/api/project-marker.md +23 -0
  33. data/docs/reference/api/providers.md +22 -0
  34. data/docs/reference/api/recents.md +17 -0
  35. data/docs/reference/api/ui.md +21 -0
  36. data/docs/reference/api/vcs.md +17 -0
  37. data/docs/reference/api/watcher.md +11 -0
  38. data/docs/reference/commands.md +271 -0
  39. data/docs/walkthrough.md +193 -0
  40. data/exe/esp +10 -0
  41. data/lib/esp/active_project.rb +71 -0
  42. data/lib/esp/agent.rb +104 -0
  43. data/lib/esp/cli/docs.rb +44 -0
  44. data/lib/esp/cli/i18n.rb +67 -0
  45. data/lib/esp/cli/mcp.rb +52 -0
  46. data/lib/esp/cli/plugins.rb +42 -0
  47. data/lib/esp/cli/refs.rb +137 -0
  48. data/lib/esp/cli/support.rb +87 -0
  49. data/lib/esp/cli.rb +317 -0
  50. data/lib/esp/docs_generator.rb +148 -0
  51. data/lib/esp/http_server.rb +232 -0
  52. data/lib/esp/introspection.rb +151 -0
  53. data/lib/esp/mcp_installer.rb +122 -0
  54. data/lib/esp/mcp_server.rb +465 -0
  55. data/lib/esp/mw/builder.rb +71 -0
  56. data/lib/esp/mw/data_files.rb +67 -0
  57. data/lib/esp/mw/dialogue_dsl.rb +209 -0
  58. data/lib/esp/mw/i18n.rb +113 -0
  59. data/lib/esp/mw/linter.rb +103 -0
  60. data/lib/esp/mw/loader.rb +130 -0
  61. data/lib/esp/mw/openmw_config.rb +138 -0
  62. data/lib/esp/mw/operations.rb +374 -0
  63. data/lib/esp/mw/preflight.rb +161 -0
  64. data/lib/esp/mw/reference_index.rb +182 -0
  65. data/lib/esp/mw/scaffolder.rb +197 -0
  66. data/lib/esp/mw/script_blob.rb +87 -0
  67. data/lib/esp/mw/script_extractor.rb +85 -0
  68. data/lib/esp/mw/tes3conv.rb +38 -0
  69. data/lib/esp/operations.rb +285 -0
  70. data/lib/esp/plugins.rb +75 -0
  71. data/lib/esp/preferences.rb +63 -0
  72. data/lib/esp/project_marker.rb +99 -0
  73. data/lib/esp/providers/anthropic.rb +74 -0
  74. data/lib/esp/providers/ollama.rb +102 -0
  75. data/lib/esp/providers/openai.rb +91 -0
  76. data/lib/esp/providers.rb +76 -0
  77. data/lib/esp/recents.rb +74 -0
  78. data/lib/esp/ui.rb +144 -0
  79. data/lib/esp/vcs.rb +112 -0
  80. data/lib/esp/version.rb +11 -0
  81. data/lib/esp/watcher.rb +55 -0
  82. data/lib/esp.rb +85 -0
  83. data/locales/en.yml +164 -0
  84. data/locales/fr.yml +10 -0
  85. metadata +241 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0a70a9625b2c9f54539ee7e5a561275fd9020e90996112022a40406484278805
4
+ data.tar.gz: 8d9342e2c02d123e7247e81198a8d01e46785b94871685716bb7898c97a76a02
5
+ SHA512:
6
+ metadata.gz: 1bde927727d1388c8c41cebff7640739cfb807809e68768c33982823b982b6da0fc05832c0dd3cf289a18317f2199295dc6a8e76175246a189543636a5b966a7
7
+ data.tar.gz: 30c973f51eaa4693f6cb12cfecff8517a30bb58c92c49862d26a3d7dd6da6b2c200b03e6d2d870c8b77c925e64d15cd106a93d5b48496b44c738721b31f50c7e
data/CHANGELOG.md ADDED
@@ -0,0 +1,35 @@
1
+ # Changelog
2
+
3
+ All notable changes to `esp-modkit` are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/), and the project aims to
5
+ adhere to [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ### Added
10
+ - **Packaged as the `esp-modkit` gem.** `gem install esp-modkit` puts the
11
+ `esp` command on PATH — no toolchain clone required. The gem ships the
12
+ library, locale catalogues, generated reference docs, and the narrative
13
+ guides. (step 24, Half A)
14
+ - `esp doctor` — reports Ruby/gem version, whether `tes3conv` is on PATH,
15
+ and whether the vanilla references index exists, so a fresh install can
16
+ self-check its prerequisites.
17
+
18
+ ### Changed
19
+ - The gem's executable (`exe/esp`) is plain Ruby that loads via RubyGems;
20
+ the repo-local `bin/esp` bash launcher (rbenv + Bundler pinning) stays for
21
+ in-repo development only.
22
+
23
+ ### Removed
24
+ - `bin/mw` — the renamed-to-`esp` deprecation shim from step 22.5. Anyone
25
+ still invoking `mw` must switch to `esp`. AI clients with a stale
26
+ `.mcp.json` should re-run `esp mcp install`.
27
+
28
+ ### Prerequisites
29
+ - **tes3conv is not bundled** (it is an external Rust binary, not on
30
+ Homebrew). Install it from
31
+ https://github.com/Greatness7/tes3conv/releases or via
32
+ `cargo install --git https://github.com/Greatness7/tes3conv`, then run
33
+ `esp doctor` to confirm. The Homebrew tap installs it for you.
34
+ - **Native extensions** (`sqlite3`, `zstd-ruby`) compile on install — a C
35
+ toolchain is required.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Corey Ellis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # esp — author Morrowind plugins as source code
2
+
3
+ `esp` is a Ruby toolchain for authoring Morrowind plugins **without the
4
+ Construction Set**. You write records in **JSON, Ruby, Python,
5
+ JavaScript, or TypeScript**; `esp` builds them to an `.esp` via
6
+ [tes3conv], manages scripts, translations, and dialogue, and lints
7
+ against the real vanilla game data. Every operation renders as human
8
+ text or structured JSON, so a CLI, an HTTP API, an MCP server, and an AI
9
+ agent all drive the same pipeline.
10
+
11
+ ## Why — source is truth, the `.esp` is output
12
+
13
+ The Construction Set is a direct-manipulation binary editor: the `.esp`
14
+ *is* the document. `esp` inverts that. **You author intent as
15
+ human-readable source and the `.esp` is a disposable build artifact** —
16
+ the relationship a compiler has with its object files. You never
17
+ hand-edit the binary; you edit source and rebuild.
18
+
19
+ That one inversion buys the whole software-engineering toolchain:
20
+ `git diff` / blame / branch / review, procedural generation, lint as a
21
+ build step, CI, HTTP/MCP automation, and AI authoring. The cost is the
22
+ loss of direct manipulation — spatial placement and visual browsing.
23
+ [**ESPresso**](docs/architecture.md) is the planned AI-first GUI built on
24
+ this backend, where you describe intent, an agent writes diffable
25
+ source, and you review the git diff.
26
+
27
+ ## Quickstart
28
+
29
+ Install once:
30
+
31
+ ```sh
32
+ gem install esp-modkit # puts `esp` on your PATH
33
+ # tes3conv is a required prerequisite, not on Homebrew: download a release
34
+ # https://github.com/Greatness7/tes3conv/releases
35
+ # or: cargo install --git https://github.com/Greatness7/tes3conv
36
+ esp doctor # confirm Ruby, tes3conv, and the references index
37
+ esp refs unpack # unpack vanilla ESMs to ~/.config/esp/references/ (one-time)
38
+ esp refs index # build the SQLite index there (one-time)
39
+ ```
40
+
41
+ Native extensions (`sqlite3`, `zstd-ruby`) compile on install, so a C
42
+ toolchain is needed. To work from a checkout instead of the gem, clone the
43
+ repo, `bundle install`, and put `bin/esp` on your `PATH` (the launcher pins
44
+ this repo's Ruby + Gemfile).
45
+
46
+ Then per-project — anywhere on disk:
47
+
48
+ ```sh
49
+ mkdir -p ~/morrowind/MyFirstMod && cd ~/morrowind/MyFirstMod
50
+ esp init # writes .esp/project.json + git init + mods/ + dist/
51
+ esp new MyMod # scaffold mods/MyMod/
52
+ esp lint MyMod # check references against vanilla data
53
+ esp build MyMod # -> dist/MyMod.esp (in *this* project)
54
+ esp install MyMod # OpenMW: register via data= + content=
55
+ esp install MyMod --to-data-files # Original engine: copy into Morrowind/Data Files/
56
+ ```
57
+
58
+ `esp` walks up from the current directory to find the `.esp/project.json`
59
+ marker, so commands work from any subdir of your project tree —
60
+ no flags needed inside a project, `--root PATH` if you're scripting
61
+ across several, or `export ESP_PROJECT_ROOT=…` to pin one per shell.
62
+
63
+ **New here? Read the [walkthrough](docs/walkthrough.md)** — one plugin
64
+ from empty to built, with the thesis shown in practice.
65
+
66
+ ## Three frontends, one pipeline
67
+
68
+ Every command has a `--json` mode whose payload is identical across all
69
+ three frontends:
70
+
71
+ | Frontend | Command | For |
72
+ |---|---|---|
73
+ | CLI | `esp <cmd>` | humans + scripts |
74
+ | HTTP | `esp serve` | ESPresso, local automation |
75
+ | MCP | `esp mcp serve` (`esp mcp install` to wire up) | AI clients (Claude Code / Desktop) |
76
+
77
+ ## Command surface
78
+
79
+ ```
80
+ doctor check prerequisites (Ruby, tes3conv, refs index)
81
+ init [NAME] new project (marker + git init + mods/ + dist/)
82
+ new MOD [--format rb|py|js|ts] scaffold a mod
83
+ build [MOD] [--locale fr] [--all] [--install] build -> dist/MOD[.locale].esp
84
+ watch MOD rebuild on change
85
+ lint MOD dangling-ref check (needs refs index)
86
+ extract-scripts MOD hoist inline script text to files
87
+ unpack PLUGIN [NAME] .esp -> mods/NAME/NAME.json
88
+ install MOD [--copy-to PATH | --to-data-files] OpenMW + optional file copy
89
+ refs unpack | index | find Q vanilla data: build + query
90
+ i18n check MOD missing/orphan locale keys
91
+ docs build | introspect regenerate / dump docs
92
+ serve | mcp serve HTTP API / MCP server
93
+ ```
94
+
95
+ ## Documentation
96
+
97
+ - **[Walkthrough](docs/walkthrough.md)** — empty → built, end to end.
98
+ - **[Getting started](docs/getting-started.md)** — install, paths on
99
+ macOS, wiring `esp` into an AI client.
100
+ - **[Authoring guide](docs/authoring-guide.md)** — source formats,
101
+ `text_source`, dialogue DSL, translation, linting.
102
+ - **[Architecture](docs/architecture.md)** — layered design, the
103
+ roadmap, why each piece exists.
104
+ - **[Command reference](docs/reference/commands.md)** *(auto-generated)*
105
+ - **[API reference](docs/reference/api/index.md)** *(auto-generated)*
106
+
107
+ Regenerate the auto pages with `bin/esp docs build`; a pre-commit hook
108
+ fails if they drift.
109
+
110
+ ## Development
111
+
112
+ ```sh
113
+ bundle exec rake # RuboCop + Minitest (must be green)
114
+ bundle exec rake admin # local quality reports (coverage, lint, stats)
115
+ ```
116
+
117
+ [tes3conv]: https://github.com/Greatness7/tes3conv
@@ -0,0 +1,125 @@
1
+ # Architecture
2
+
3
+ ## Layered design
4
+
5
+ ```mermaid
6
+ flowchart TB
7
+ subgraph frontends["Frontends"]
8
+ direction LR
9
+ CLI(CLI)
10
+ GUI(GUI — planned)
11
+ MCP(MCP — planned)
12
+ HTTP(HTTP — planned)
13
+ end
14
+
15
+ subgraph service["Service layer (returns structured results)"]
16
+ direction LR
17
+ Builder
18
+ Linter
19
+ ScriptExtractor
20
+ ReferenceIndex
21
+ I18n
22
+ end
23
+
24
+ subgraph core["Core primitives"]
25
+ direction LR
26
+ Loader
27
+ Preflight
28
+ ScriptBlob
29
+ Tes3conv
30
+ OpenmwConfig
31
+ end
32
+
33
+ subgraph protocol["Source-program protocol"]
34
+ direction LR
35
+ InProcess["Ruby (in-process)"]
36
+ Subprocess["Spawn (Python / JS / TS)"]
37
+ end
38
+
39
+ frontends --> service
40
+ service --> core
41
+ core --> protocol
42
+ ```
43
+
44
+ Today **CLI**, **HTTP** (`esp serve`), and **MCP** (`esp mcp serve`) are
45
+ all live. The HTTP layer is a thin WEBrick wrapper over the same
46
+ service-layer modules — every endpoint payload matches the
47
+ corresponding `esp <cmd> --json` shape. The MCP server exposes the same
48
+ operations as tools over stdio JSON-RPC for AI clients. The **GUI**
49
+ (ESPresso, a separate repo) talks to `esp serve` instead of spawning a CLI
50
+ per call — including `POST /agent`, which streams a Claude tool-use loop
51
+ (`Esp::Agent`) over Server-Sent Events, dispatching the same tool surface
52
+ MCP exposes.
53
+
54
+ ## Why JSON is the universal contract
55
+
56
+ The build pipeline:
57
+
58
+ ```mermaid
59
+ flowchart LR
60
+ Src["source file<br/>(.json / .rb / .py / .js / .ts)"]
61
+ --> Loader
62
+ --> Records["records<br/>(Array&lt;Hash&gt;)"]
63
+ --> I18n["I18n.resolve!<br/>(@t: sentinels)"]
64
+ --> Preflight
65
+ --> Tmp[JSON tempfile]
66
+ --> tes3conv
67
+ --> Esp["dist/&lt;Mod&gt;.esp"]
68
+ ```
69
+
70
+ Anywhere along that chain you can substitute a different producer.
71
+ Today's authoring formats — JSON, Ruby, Python, JavaScript, TypeScript
72
+ — all converge on the same `Array<Hash>` shape. A future language
73
+ (Lua? Rust?) gets added by registering an interpreter command in
74
+ `Esp::Mw::Loader::INTERPRETERS`.
75
+
76
+ ## Why two paths for i18n
77
+
78
+ `.rb` sources can call `t("key")` directly because the Ruby loader
79
+ runs in-process; we inject a singleton method on the eval module.
80
+
81
+ Subprocess loaders (Python/JS/TS) can't call back into Ruby. They emit
82
+ `"@t:key"` sentinel strings instead, which the build resolves
83
+ post-load via a single recursive walk. Same `Esp::Mw::I18n` instance
84
+ services both paths, so miss tracking is unified.
85
+
86
+ JSON has no execution model at all, so it uses sentinels too.
87
+
88
+ ## Why a SQLite reference index
89
+
90
+ Vanilla data unpacks to ~200 MB of JSON across three ESMs and ~69 k
91
+ records. `grep` is slow and brittle (record boundaries aren't lines).
92
+ The index is a 4-column SQLite table built once with `esp refs index`,
93
+ queried in milliseconds for lookups by id / type / SQL `LIKE` pattern.
94
+ The linter uses it for foreign-id resolution; future tooling will too.
95
+
96
+ ## Why golden tests for ZSTD blob synthesis
97
+
98
+ The Script record's variables list and bytecode placeholder are
99
+ embedded as ZSTD-framed binary blobs that tes3conv accepts only in a
100
+ very specific layout. Getting the bytes wrong is silent: builds appear
101
+ to succeed and OpenMW just logs warnings. The test suite sweeps every
102
+ vanilla Script record (~1.2 k) and asserts that our synthesis +
103
+ libzstd-backed decode round-trip byte-identically.
104
+
105
+ ## Self-documenting outputs
106
+
107
+ Every CLI command has a `--json` mode whose payload is the same data
108
+ the human text branch yields. `esp docs introspect` dumps the full
109
+ command tree and module surface as one JSON object — that's the
110
+ mechanism the markdown docs in `docs/reference/` are generated from,
111
+ and the same mechanism the MCP server uses to register its tools.
112
+
113
+ ## Roadmap status
114
+
115
+ **Phase A — the toolchain** (the "compiler") and **Phase B — production
116
+ toolchain + AI backbone** are done: the full authoring/build pipeline,
117
+ the MCP/`Operations` surface, internal tool i18n, OS-aware game-install
118
+ integration, and a central load manifest.
119
+
120
+ **Phase C — ESPresso** (the AI-first GUI, in the separate `espresso`
121
+ repo) is in
122
+ progress: the Tauri+SvelteKit shell and the agent workspace (chat →
123
+ `POST /agent` SSE → `Esp::Agent`) are built; git-diff review, visual
124
+ panels, and packaging (bundling a Ruby runtime) are next. The
125
+ local-only `.claude/ROADMAP.md` holds the authoritative step list.
@@ -0,0 +1,206 @@
1
+ # Authoring guide
2
+
3
+ ## Source formats
4
+
5
+ Each mod lives in `mods/<Mod>/` with exactly one source file. Pick the
6
+ language you're most comfortable with — they all produce the same ESP.
7
+
8
+ | Extension | Loader | Notes |
9
+ |---|---|---|
10
+ | `<Mod>.json` | in-process | Array of record hashes, matches tes3conv's JSON shape |
11
+ | `<Mod>.rb` | in-process | Last expression must be the records array; symbol keys auto-converted |
12
+ | `<Mod>.py` | subprocess (`python3`) | Print a JSON array of records to stdout |
13
+ | `<Mod>.js` / `<Mod>.mjs` | subprocess (`node`) | Same contract |
14
+ | `<Mod>.ts` | subprocess (`deno run --quiet`) | Same contract |
15
+
16
+ For subprocess loaders the interpreter runs with `cwd` set to the mod's
17
+ source directory, so the script can read sibling files like
18
+ `scripts/foo.mwscript` with plain relative paths. The script's own path
19
+ is passed as the last command-line arg.
20
+
21
+ To add another language, add an entry to `Esp::Mw::Loader::INTERPRETERS` in
22
+ `lib/esp/mw/loader.rb`. The contract is just "print a JSON array of records
23
+ to stdout."
24
+
25
+ ## Per-mod self-containment
26
+
27
+ Everything that belongs to a mod — its source file, scripts, dialogue
28
+ drafts, design docs, i18n catalogues — lives **inside that mod's
29
+ folder**. The tool repo doesn't own user content; cross-mod files
30
+ belong wherever the user organises them. A typical mod layout:
31
+
32
+ ```
33
+ mods/<Mod>/
34
+ ├── <Mod>.rb # or <Mod>.json, <Mod>.py, etc.
35
+ ├── design/ # docs, voice guides, narrative notes
36
+ ├── scripts/ # .mwscript files referenced via text_source
37
+ ├── i18n/ # locale catalogues (see Translation below)
38
+ └── README.md
39
+ ```
40
+
41
+ ## MWScript: `text_source`
42
+
43
+ Script records carry their source code in `text`. For real scripts this
44
+ is verbose and unreadable inside a JSON array, so the build accepts an
45
+ alternative:
46
+
47
+ ```json
48
+ {
49
+ "type": "Script",
50
+ "id": "MyScript",
51
+ "text_source": "scripts/MyScript.mwscript"
52
+ }
53
+ ```
54
+
55
+ At build time the preflight pass reads `scripts/MyScript.mwscript`,
56
+ parses out variable declarations, and regenerates the SCHD header and
57
+ SCVR/SCDT blobs in the byte layout tes3conv expects. The `text` field
58
+ is set to the file's contents; `bytecode` is a placeholder (OpenMW
59
+ recompiles from text on load).
60
+
61
+ Going the other direction: if you `esp unpack` someone else's mod, every
62
+ script lives inline. Run `esp extract-scripts <Mod>` to hoist those out
63
+ to `scripts/<id>.mwscript` files. The transformation is lossless —
64
+ rebuilding produces an identical ESP.
65
+
66
+ ## Dialogue (DSL, Ruby sources only)
67
+
68
+ Hand-writing DIAL and DialogueInfo records as JSON is the most painful
69
+ single thing the Construction Set is good at. The Ruby DSL replaces
70
+ the bookkeeping (per-info IDs, prev/next chains, repeated speaker
71
+ filters) so the source reads like the dialogue itself:
72
+
73
+ ```ruby
74
+ records = [
75
+ { type: 'Header', flags: '', file_type: 'Esp', author: 'me',
76
+ version: 1.3, num_objects: 0, masters: [] }
77
+ ]
78
+
79
+ records.concat(
80
+ dialogue do
81
+ speaker 'Hrisskar Flat-Foot' do
82
+ topic 'Flat-Foot' do
83
+ info t('hrisskar.flat_foot.intro')
84
+ info t('hrisskar.flat_foot.legion'), pc_faction: 'Imperial Legion'
85
+ info t('hrisskar.flat_foot.warning'),
86
+ pc_faction: 'Imperial Legion',
87
+ pc_rank: 3,
88
+ result_script: 'StartScript AFSN_TrackerScript'
89
+ end
90
+ end
91
+
92
+ topic 'AFSN_Tracker', type: :journal do
93
+ info 'Quest started.', journal_index: 10
94
+ info 'Hrisskar agreed.', journal_index: 20
95
+ end
96
+ end
97
+ )
98
+
99
+ records
100
+ ```
101
+
102
+ What the DSL does for you:
103
+
104
+ - **One `Dialogue` record per `topic`**, carrying `dialogue_type`
105
+ (Topic / Greeting / Journal / Persuasion / Voice).
106
+ - **`DialogueInfo` records chained in author order** via prev_id /
107
+ next_id. Order is filter precedence in Morrowind — first match
108
+ wins, so write fallbacks last.
109
+ - **`speaker "Name" do … end`** scopes a speaker filter to every info
110
+ inside; an explicit `speaker:` on an info overrides.
111
+ - **Predictable IDs**: `"<topic>_<idx>"`. Git diffs stay readable.
112
+ - **i18n**: `t("key.path")` works inside the block — same catalogue
113
+ as the rest of the mod.
114
+
115
+ Supported info kwargs (all optional):
116
+
117
+ | Kwarg | Maps to |
118
+ |---|---|
119
+ | `speaker:` | `speaker_id` |
120
+ | `race:` | `speaker_race` |
121
+ | `class:` *(or `class_:`)* | `speaker_class` |
122
+ | `faction:` | `speaker_faction` |
123
+ | `cell:` | `speaker_cell` |
124
+ | `sex:` | `data.speaker_sex` (`:any` / `:female` / `:male`) |
125
+ | `speaker_rank:` | `data.speaker_rank` |
126
+ | `pc_faction:` | `player_faction` |
127
+ | `pc_rank:` | `data.player_rank` |
128
+ | `disposition:` | `data.disposition` |
129
+ | `journal_index:` | `data.disposition` (for Journal topics) |
130
+ | `sound:` | `sound_path` |
131
+ | `result_script:` | `script_text` (inline MWScript) |
132
+
133
+ **Not yet covered** (deferred to a later step): the structured
134
+ `filters` array (function calls / variable comparisons / global checks),
135
+ `result_script_source:` (currently raises NotImplementedError — use
136
+ inline text for now), and the Greeting 0–9 priority cascade.
137
+
138
+ ## Translation (i18n)
139
+
140
+ Strings can be authored in two complementary ways:
141
+
142
+ - **In `.rb` sources** call `t("key.path")` directly — the loader
143
+ exposes the helper as a singleton method on the eval module, so it
144
+ resolves at evaluation time.
145
+ - **In `.json` / `.py` / `.js` / `.ts` sources** write
146
+ `"@t:key.path"` sentinel strings anywhere a string value is
147
+ expected. The build resolves them recursively post-load.
148
+
149
+ Catalogues live at `mods/<Mod>/i18n/<locale>.yaml`. Keys are dot-pathed
150
+ nested-hash, with lookup falling back to the default locale (`en`) on
151
+ miss:
152
+
153
+ ```yaml
154
+ # mods/MyMod/i18n/en.yaml
155
+ meta:
156
+ description: "A friend in Seyda Neen"
157
+ activator:
158
+ stump: "Hollow Stump"
159
+ ```
160
+
161
+ ```yaml
162
+ # mods/MyMod/i18n/fr.yaml
163
+ meta:
164
+ description: "Un ami à Seyda Neen"
165
+ activator:
166
+ stump: "Souche Creuse"
167
+ ```
168
+
169
+ Build with the default locale:
170
+
171
+ ```sh
172
+ bin/esp build MyMod # -> dist/MyMod.esp (English)
173
+ bin/esp build MyMod --locale fr # -> dist/MyMod.fr.esp (French)
174
+ ```
175
+
176
+ Missing keys fall back to `en` with a warning in the build logs.
177
+ `esp i18n check MyMod` reports missing and orphan keys per locale.
178
+
179
+ ## Linting
180
+
181
+ `esp lint <Mod>` checks Npc and scripted-object records for dangling
182
+ references — script / race / class / faction IDs that don't exist in
183
+ the mod itself or in any indexed vanilla ESM. Two severities:
184
+
185
+ - `ERROR` — ref points at nothing the mod or vanilla defines.
186
+ - `WARN` — ref resolves in a vanilla ESM that isn't listed in the
187
+ mod's `Header.masters`. OpenMW will still load it but log warnings.
188
+
189
+ Non-zero exit on any error — wire it into CI.
190
+
191
+ ## Authoring with AI
192
+
193
+ Since every command accepts `--json` and `esp docs introspect` dumps the
194
+ full capability surface, an AI agent can:
195
+
196
+ 1. Call `esp docs introspect` to learn the available commands.
197
+ 2. Read records via `esp refs find <id> --show --json`.
198
+ 3. Author a mod source file (Ruby/Python/JSON/whatever).
199
+ 4. Validate via `esp lint <Mod> --json`.
200
+ 5. Build via `esp build <Mod> --json`.
201
+
202
+ `esp mcp serve` exposes this loop as first-class MCP tools; see
203
+ [getting-started](getting-started.md#wiring-esp-into-an-ai-client-mcp)
204
+ for wiring it into an AI client, and the
205
+ [walkthrough](walkthrough.md#same-pipeline-three-frontends) for the loop
206
+ in action.