libs/runtime-overrides.lua

159 lines

1-- {{{ runtime-overrides.lua
2-- One run's command-line choices, materialized to a file in RAM so that every
3-- short-lived child process of run.sh resolves them identically.
4--
5-- General description (for a CEO): run.sh launches a brand-new little program for
6-- each stage of the pipeline and shuts it down before the next. A choice the
7-- operator typed once -- "use THIS embedding model" -- only reached the stages
8-- run.sh happened to hand it to; the others quietly fell back to the default
9-- written in the config file. This module is a shared notepad in fast memory:
10-- run.sh writes the run's choices on it once at the start, and every stage reads
11-- the same note. It is rewritten from scratch on every run, so yesterday's note
12-- can never be mistaken for today's. An empty note (or a missing key) means "the
13-- operator chose nothing special here" -- so readers fall back to config.lua,
14-- exactly as before.
15--
16-- Format: a Lua file that returns a table, read back via dofile -- the same
17-- mechanism config.lua itself uses -- so there is no JSON dependency to carry.
18-- Why this design over an environment variable: an env var dies with the shell
19-- and reaches only children run.sh explicitly exports it to; a file is readable
20-- from any process, any working directory, and is inspectable with `cat`. The
21-- one hazard a file has and an env var does not -- staleness across runs -- is
22-- removed by run.sh overwriting it at startup (see scripts/write-run-overrides).
23-- }}}
24
25local M = {}
26
27-- {{{ Module state
28-- project_root: where tmp/ (and thus the notepad) lives. cache/loaded: the
29-- decoded table is read from disk at most once per process, then reused.
30local project_root = nil
31local cache = nil
32local loaded = false
33-- }}}
34
35-- {{{ local function resolve_root()
36-- Resolve the project root: an explicit set_project_root wins; otherwise infer
37-- it from package.path (the "/libs/?.lua" entry every caller installs), and only
38-- then fall back to the hard-coded path so a stray direct invocation still works.
39local function resolve_root()
40 if project_root then return project_root end
41 local path = package.path:match("([^;]+)/libs/%?%.lua")
42 project_root = path or "/mnt/mtwo/programming/ai-stuff/neocities-modernization"
43 return project_root
44end
45-- }}}
46
47-- {{{ function M.set_project_root(path)
48-- Idempotent: re-setting the same root is a no-op so callers can set it on every
49-- access without throwing away the per-process cache (and re-reading the file).
50function M.set_project_root(path)
51 if path == project_root then return end
52 project_root = path
53 cache = nil
54 loaded = false
55end
56-- }}}
57
58-- {{{ function M.path()
59-- The notepad lives in tmp/shared-memory/ (the /dev/shm RAM tier): wiped on
60-- reboot -- exactly right, since one run's choices have no meaning after that
61-- run ends. It is data, not code, so the noexec shared-memory tier is the right
62-- home; Lua loadfile reads it as text, which noexec permits.
63function M.path()
64 return resolve_root() .. "/tmp/shared-memory/run-overrides.lua"
65end
66-- }}}
67
68-- {{{ local function serialize_value(value)
69-- Only the scalar kinds a CLI flag can carry are supported. Anything else is a
70-- programming error at the call site, so we error loudly rather than emit a file
71-- that would dofile() into something surprising.
72local function serialize_value(value)
73 local kind = type(value)
74 if kind == "string" then
75 return string.format("%q", value)
76 elseif kind == "number" or kind == "boolean" then
77 return tostring(value)
78 end
79 error("runtime-overrides: cannot serialize a value of type " .. kind)
80end
81-- }}}
82
83-- {{{ function M.write(overrides)
84-- Overwrite the notepad with exactly THIS run's overrides. Called once by run.sh
85-- at startup, so the file is never older than the current run -- a previous
86-- run's --model cannot survive into one that omits it. An empty table is a valid,
87-- meaningful result ("return {}"): a present-but-empty notepad says "this run set
88-- nothing special", and every reader then falls back to config.lua.
89--
90-- The tmp/shared-memory/ directory (the /dev/shm RAM tier, wiped on reboot) must already exist;
91-- run.sh creates it just before calling the writer. If it does not, io.open
92-- returns nil and we error loudly rather than silently lose the note -- a missing
93-- notepad would reintroduce exactly the config-fallback bug this module fixes.
94function M.write(overrides)
95 overrides = overrides or {}
96 local lines = {}
97 for key, value in pairs(overrides) do
98 lines[#lines + 1] = string.format(" [%q] = %s,", key, serialize_value(value))
99 end
100 local body = table.concat({
101 "-- Auto-generated per run by run.sh (scripts/write-run-overrides).",
102 "-- This run's command-line choices, so every stage resolves them the same",
103 "-- way. Overwritten every run; safe to delete (the next run rewrites it).",
104 "return {",
105 table.concat(lines, "\n"),
106 "}",
107 "",
108 }, "\n")
109
110 local file, err = io.open(M.path(), "w")
111 if not file then
112 error("runtime-overrides: cannot write " .. M.path() .. ": " .. tostring(err))
113 end
114 file:write(body)
115 file:close()
116
117 -- Refresh the in-process view so a writer that also reads sees its own write.
118 loaded = false
119 cache = nil
120end
121-- }}}
122
123-- {{{ local function load()
124-- Read + decode the notepad at most once per process. A missing file (no run.sh
125-- wrote one -- e.g. a stage launched by hand) or a malformed one both resolve to
126-- an empty table, i.e. "no overrides", which is the safe, config-default answer.
127local function load()
128 if loaded then return cache end
129 loaded = true
130 local ok, result = pcall(dofile, M.path())
131 if ok and type(result) == "table" then
132 cache = result
133 else
134 cache = {}
135 end
136 return cache
137end
138-- }}}
139
140-- {{{ function M.get(key)
141-- Return the override for key, or nil when it was not set. An empty string is
142-- treated as "not set" so run.sh can pass `--model ""` (no override) without the
143-- writer needing to special-case it.
144function M.get(key)
145 local value = load()[key]
146 if value == nil or value == "" then return nil end
147 return value
148end
149-- }}}
150
151-- {{{ function M.all()
152-- The whole decoded table, for callers that want to inspect every override.
153function M.all()
154 return load()
155end
156-- }}}
157
158return M
159