User strategies
A user's strategies are files under user_data/strategies, not rows in a blob — the layout, the two trees, the scan that compiles every script, and its named diagnostics.
crates/vike-studio-core/src/user_strategies/mod.rs opens with the rule the whole module exists
for: a user's strategies are files, not rows in a blob. It is the loader for
<project>/user_data/strategies/, the resolver that turns one of its presets into constructed
params, and the one-time migration off the Studio's studio_strategies.json. The directory it
scans is the user-content root vike_model::state_path::PROJECT_USER_DATA_DIR defines.
Why files
The header records what the JSON array denied a person: a script in it could not be opened in an editor, diffed, committed, copied to another machine or sent to somebody. Two properties made it lossy rather than inconvenient. One corrupt byte lost the library, because the blob loader answered a parse failure with an empty list — right for a window layout, wrong for the only copy of somebody's work — while one file per strategy costs exactly the strategy the bad byte is in. And it was filed under the wrong owner, at the hist store's root, beside re-downloadable market data whose lifecycle is "delete it and re-ingest". Migration is the move.
The layout
user_data/strategies/rhai/<name>/<name>.rhai the ENTRY file — its stem IS the strategy name
user_data/strategies/rhai/<name>/*.toml presets, flat beside the entry
user_data/strategies/rhai/<name>/presets/*.toml …or filed, for the user whose sweep left forty
user_data/strategies/rust/<builtin>/*.toml presets for a strategy whose code is in the BINARYFour rules the loader depends on. The entry file matches its folder, so a folder holding several
scripts is never ambiguous — the others are helpers, read by nothing here. A .toml beside the
entry is a preset for that strategy, so rm -r <name>/ removes the whole thing and two strategies'
presets can never be confused. A preset is the params table, flat. And a folder under rust/
needs no entry file when its name is a built-in strategy: the code is in the binary, resolved by
name through the harness registry's strategy_by_name, and the folder holds nothing but presets.
load.rs's header admits that treating such a folder as a missing entry file was a real bug the
module used to have, and that migrate.rs predicted it.
state_path.rs's STRATEGIES_SUBDIR doc gives the reason for the language split: the two have
different lifecycles — a .rhai file is read at startup, a .rs file must be compiled — and the
tree is artifact first, language second. user_rust_strategies_dir is explicit that the rust/
tree is meaningful only in a source checkout; on an installed binary it is inert, and that is not
a defect. See Authoring in Rust.
vike-cli init scaffolds the tree with three examples — sma_cross, rsi_meanrev and ema_trend,
each with presets — plus rust/my_experiment/. By default it writes only what is absent, so an
edited file is never touched; --reset is the one exception, restoring the shipped samples and
overwriting edits to them (a strategy you wrote is not in that table, so nothing here can delete
it) — vike-cli init. VIKE_USER_DATA_DIR names the root outright and
skips the walk, a separate variable from VIKE_SETTINGS_DIR on purpose.
The scan
load_user_strategies(strategies_root) scans both trees, rhai/ then rust/, into one
LoadReport, because a strategy is addressed by name and the two trees share one namespace.
load_rhai_strategies(root) is the Rhai tree alone and cannot see the rust/ presets. Neither
ever fails; every failure is a row. load.rs's header states three decisions that look like
details and are not:
- An absent root is not an error; an unreadable one is. A fresh install has no
user_data/, so "nothing there" stays silent; a root that exists and cannot be listed is a permissions bug wearing the "not configured yet" answer, and it gets its ownRootUnreadablerow. That is the distinction the credential store draws, for the same reason. - Every script is compiled during the scan, through
run.rs'sbuild_strategy— the same call a Run makes — so "it loaded" and "it will run" are one claim rather than a failure moved to the moment the user presses Run. - A
rust/folder named after a built-in needs no entry file.StrategyBodyis the type that says which kind a loaded folder is:Rhai { entry, source }, proven to compile, orNative, whoseUserStrategy::nameis the registry's own spelling however the folder was cased.
Folders are sorted by name before anything loads, dot-entries are skipped as tool droppings, and names are claimed case-folded across both trees. A folder that loaded nothing claims no name, so a later folder of the same name is a real strategy rather than a duplicate of a failure.
The diagnostics
LoadDiagnostic has one variant per mistake a person actually makes, each carrying the path and
the fix, and Severity draws the line at "did the user lose a strategy": BadPreset and
ShadowedPreset are warnings that leave the strategy runnable, everything else is an error.
| variant | what happened |
|---|---|
RootUnreadable | the root exists but cannot be listed — fix its permissions |
MissingEntry | a rhai/ folder holds no <name>.rhai; the message lists the .rhai files it did find, so a misnaming explains itself |
NativePresetsMisfiled | presets for a built-in filed under rhai/; the fix is to move the folder to strategies/rust/, which is why this is its own row rather than a MissingEntry wording |
UnregisteredNative | a rust/ folder holding presets whose name is no built-in — they can reach nothing |
UnreadableEntry | the entry file is there but is not readable as UTF-8 text |
CompileFailed | RhaiStrategy::compile rejected it, via build_strategy; the script author's message verbatim |
BadPreset | a .toml could not be read, parsed or used as a params table; the strategy loads without it |
ShadowedPreset | the same preset name flat and under presets/; the flat one wins as the documented primary position |
DuplicateName | two folders resolve to one name — SmaCross/ and smacross/, which coexist only on Linux, or rhai/grid/ beside rust/grid/; the rhai/ claim wins |
StrayScript | a script directly in a tree's root; the message names the folder it belongs in |
A rust/ folder with no presets and no registry name is not reported
load_rust_folder is the one place the loader returns nothing without a diagnostic, deliberately:
a folder naming no built-in and holding no presets is a source-checkout Rust strategy that has not
been registered yet — the shipped my_experiment/ template's exact state — and nothing reads that
tree at runtime. The moment it holds presets, they are addressed by a name that resolves to
nothing, and that is a loss the UnregisteredNative row reports.
The compile log
render_compile_log(report, stamp) renders one scan as the text a caller appends to
user_data/logs/compile.log: one ok line per loaded strategy naming its entry file (or
built-in) and its presets, one ERROR/WARN line per diagnostic, and a summary count. Passes are
logged too, because a user debugging "why is my strategy not in the list" needs to see the ones
that are — an absent name is the answer. stamp is caller-supplied and the function reads no
clock, one stamp per line so grep <name> still answers "when". The module performs no logging and
opens no log file; the composition root decides where the text goes. user_logs_dir's doc adds
that $VIKE_LOG_DIR must not redirect this folder, because that variable moves the app log.
A loaded strategy's spec() is the bridge to a Run — the Rhai source, or the registry name with
an empty params table — and applying one of its presets is Presets.
The Studio's own restorable state, and the blob this tree replaces, are on
Saved strategies and workspace.
Loading indicators
How user_data/indicators/*.rhai becomes a process-wide set every strategy can call — the load report, its five diagnostics, the once-per-process install, and which binaries do it.
Presets
How a strategy's preset.toml becomes the params it is constructed with — the flat-table rule, the two roads a Rhai and a built-in strategy take, and the keys that are named rather than dropped.