The Modules Format - #8304
Closed
keithharvey wants to merge 46 commits into
Closed
Conversation
keithharvey
force-pushed
the
fmt-llm
branch
2 times, most recently
from
July 16, 2026 02:54
8933c2d to
18847a1
Compare
Contributor
keithharvey
force-pushed
the
modules
branch
2 times, most recently
from
July 16, 2026 05:11
cbc46f5 to
4601e24
Compare
keithharvey
changed the base branch from
fmt-llm
to
sharing/05-game-modes-export
July 16, 2026 05:14
keithharvey
force-pushed
the
modules
branch
11 times, most recently
from
July 16, 2026 10:21
cbbe7ae to
778083b
Compare
keithharvey
force-pushed
the
sharing/05-game-modes-export
branch
from
July 16, 2026 10:53
182ddb2 to
d095e4e
Compare
…ring
Renames spec/builders/{spring->engine}_{,un}synced_builder.lua + their
builder_specs, Builders.Spring->Builders.EngineSynced,
Builders.SpringUnsynced->Builders.EngineUnsynced, @Class names, and all
call sites. The renamed builders own every Engine-related test edit so no
two prereqs touch one file:
- engine_synced_builder.lua: gamedata/system.lua defs mock aliases
Engine.{Shared,Synced,Unsynced} + BAR to the _G.Spring/_G.BAR mocks
- engine_unsynced_builder.lua: widget sandbox gets env.Engine; capture
spies install on Engine.Shared.*
Prefix branch -> lands in fmt once, every leaf+mig inherits it.
Recovered from origin/mig-spring-split@4e25bfe82a (branch lost in a rename);
files transplanted verbatim (formatting is normalized by run_fmt).
Add Utilities, I18N, Debug, Lava, and GetModOptionsCopy to the System tables in luaui/system.lua and luarules/system.lua so that widgets and gadgets can access them after detach-bar-modules moves them off the Spring table. Also create .emmyrc.json (the EmmyLua analyzer config) with the detached modules in the globals list, plus type stubs for LSP/CLI support. The .emmyrc.json content matches what vscode-recommended-extensions ships, with 5 extra globals (Utilities/Debug/Lava/GetModOptionsCopy/I18N) that only become real top-level identifiers after detach-bar-modules runs. When vscode-recommended-extensions has already merged, -Xtheirs in the cherry-pick keeps this version (the superset). # Conflicts: # .emmyrc.json
Restructures the 20 files under luaui/Tests/, luaui/TestsExamples/,
plus the headless-only common/testing/infologtest.lua, from bare-
global hook declarations to a return-table shape. Updates the
dbg_test_runner widget to read test hooks from the returned table.
Motivation: the pre-existing shape required the test files to run
under setfenv(chunk, testEnvironment) and define `function test()`,
`function setup()`, etc. as bare module-level globals that setfenv
redirected into the environment. That works at runtime but emmylua
can't model the sandboxing — it sees 20+ files declaring project-
wide globals like `test`, `setup`, `skip`, `cleanup`. To keep
emmylua happy, .emmyrc.json had to blacklist both test directories
under workspace.ignoreDir — a kludge on clearly-ours code. Lives on
its own leaf so the convention change can be discussed in isolation.
Minimal shape change per file — just prepend `local` to each top-
level `function` declaration, and append a final `return { ... }`
block listing whichever lifecycle hooks (skip/setup/test/cleanup)
that file actually defines. Original indentation and formatting
preserved (no stylua reformatting noise — the fmt transform runs
after this one in the mig pipeline).
Runner patch — luaui/Widgets/dbg_test_runner.lua, loadTestFromFile:
- capture the return value of pcall(chunk)
- require it to be a table
- merge its keys into testEnvironment so runTestInternal still
reads bare `skip`/`setup`/`test`/`cleanup` under setfenv
Vendored LuaCATS annotations for busted/luassert to provide IntelliSense for the unit-test surface. Lives on its own leaf so the discussion around 'vendoring LuaCATS types' can happen in isolation — prior pushback on the same direction in an earlier unit-testing PR makes this the right place to litigate it rather than burying it in a broader env commit. Why vendored instead of declared as a Lux dep: Lux does not yet support pulling LuaCATS annotations from library deps, and quick attempts to wire this up in Lux failed. Upstream tracking issue: lumen-oss/lux#953 — once that lands, these directories should be deleted in favor of declaring busted as a normal Lux dev-dep. Sources (pinned SHAs): - types/busted/ https://github.com/LuaCATS/busted @ 5ed85d0e016a5eb5eca097aa52905eedf1b180f1 - types/luassert/ https://github.com/LuaCATS/luassert @ d3528bb679302cbfdedefabb37064515ab95f7b9 See types/busted/provenance.md and types/luassert/provenance.md for per-directory upstream refs + license status.
…ixes Human-curated environment that, together with the LLM type-triage pass (fmt-llm), drives emmylua_check to zero on the migrated tree. Per-change rationale lives in PR beyond-all-reason#7447 review comments. - .emmyrc.json globals + diagnostics; types/* stubs; busted mock; CI gate - forward-decl / assertEqual declarations; reverted orphaned kikito loader - rationale-comment strip; types/IntegrationTests rename - deterministic pins for type-triage leftovers the LLM mishandles on big files (multi_attack opts, HighlightUnit forward-decl, ripairs suppress, gui_pip gameFrame use-before-declare) - json.lua forward-decl tidy (relocate null, drop dead decode_scan*) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
IsDevModeCached (upstream beyond-all-reason#6918) references `utilities` inside its own table constructor, where the local is not yet in scope — Lua resolves those reads as GLOBALS, so the first real call would index nil. Dormant today only because nothing calls it. Forward-declare the local so the closure captures it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated by BAR-Devtools scripts/sharing-module/generate.sh from move_map.tsv: git mv + quote-anchored include-path rewrites, plus the .busted ROOT addition for module-local specs. No logic changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
modules/sharing/module.lua declares the manifest; api.lua is the public
contract — the only surface code outside the module may consume, reached
via ModuleHandler.Get("sharing"). Entries resolve lazily and load once
per Lua state, so synced consumers never pull unsynced-only files.
External widgets/gadgets/tests switch from deep include paths to the
contract; modoptions.lua and mode enums stay direct data includes
(lobby context, root-level vocabulary).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extract the resource deny gates + terminal compute out of resource/synced.lua into policies/resource/ (one pure policy per file, filename order, first result wins) and the unit share gate + compute out of unit/synced.GetPolicy into policies/unit/. Transfer executors move to actions/ as ActionDescriptors — the only effectful layer. The synced libraries keep their public API as thin delegates so existing specs prove behavior is unchanged; controllers consume the auto-registered action registry via module_handler. Shared gate helpers land in modules/sharing/helpers.lua. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
After the descriptor extraction, resource/synced.lua and unit/synced.lua were thin delegate shells. Dissolve them into their real responsibilities: policy_evaluation.lua (pipeline entry points) and resource/factor_cache.lua / unit/factor_cache.lua (the per-team factor caches). Controllers and specs point at the real homes; transfer execution goes through the action registry everywhere. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Modules ship their own modoptions.lua fragment (same entry format as the root file, including their section entry); ModuleHandler.ModOptions() merges fragments in module-name order and the game's modoptions.lua appends them. The 25 sharing options + Sharing section move into modules/sharing/modoptions.lua, deleting every sharing reference from the root file. module_handler logging falls back to print so the lobby/unitsync LuaParser (no Spring global) can parse modoptions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mode presets travel with the module that owns the modoptions they lock: modes/sharing/* live at modules/sharing/modes/. The root modes/ system stays the vocabulary (enums, helpers) and aggregation point; ModuleHandler.ModeDirs() surfaces module preset directories and the game-modes export scans them alongside root modes/<category>/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Waterfill solver, share stats, and the manual share ledger move to
modules/economy/ with their own manifest and contract; sharing declares
requires = { "economy" } and consumes them via ModuleHandler.Get — the
first real exercise of cross-module requires/provides.
shared_config stays behind as modules/sharing/config.lua: it reads
sharing's modoptions and tech state, so an economy module owning it
would have a fake boundary. The solver is now tax-agnostic — callers
with a tax policy pass their own resolver (sharing passes
config.getTeamTaxRate); the default is tax-free. gui_top_bar and
gui_teamstats consume ShareStats from economy's contract instead of
sharing's, which shrinks sharing's api to things that are actually
sharing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The numbered per-policy files (010_/020_/100_) used the filesystem as control flow and paid descriptor boilerplate four times over for one policy. Each category is now a single pipeline file — policies/resource.lua, policies/unit.lua — expressed with the builder's new Pipeline layer: gates in declaration order, one terminal Compute, Build() emitting the same plain PolicyDescriptor[] the loader always consumed. Includes hoisted once per file; evaluation semantics, stage names, and results unchanged (same specs prove it). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The api is organized by domain, never by file layout: Enums, Unsynced
(the widget facade: .Resources, .Units incl. Units.ShareUnits), Units
(the both-state unit surface: validation, factors, cached pair policy),
Take, and AdvPlayerList.{Helpers,ApiExtensions} as a nested namespace.
Flat keys that echoed the pre-module filenames (UnitUnsynced,
UnitShared, TakeComms, AdvPlayerList*) are gone; nested entries resolve
lazily like everything else in the contract.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…esolution)
The manifest declares the state partition explicitly — provides as a
plain path (state-agnostic) or { shared, synced, unsynced } — and
ModuleHandler.Get merges shared + current state into one flat api, so a
consumer holds exactly the surface that exists where it stands: wrong-
state access is nil at the first index, never a crash three calls deep.
The consumer never picks a state (it has one), mirroring how the engine
exposes state-appropriate API implicitly.
sharing splits into api.lua (shared: Enums, Units, Take) and
api_unsynced.lua (Resources, Units + ShareUnits graft, AdvPlayerList) —
the OG spike shipped exactly this pair as api_team_transfer.lua + the
synced service. Per-state contracts are plain eager tables; the lazy
metatable existed only to keep one file safe in both states, so it is
gone everywhere (economy's contract simplifies too), and the root
unsynced.lua facade is inlined into the unsynced contract.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-pipeline errors Pipeline() takes no category argument — a pipeline's identity is its filename (policies/<category>.lua), stamped by LoadPolicies: one source of truth, no magic strings to drift against the file. Lookups go through Enums.PolicyCategory and a missing pipeline is a loud error instead of a silent empty evaluation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… action The shared libs were harboring decision code: resource/shared.lua and unit/shared.lua carried CreateDenyPolicy/CombineResourcePolicy/ BuildUnitPolicyResult/ResolveSharingModes and — worse — cached GetCachedPolicyResult rebuilds whose gates hand-mirrored the pipeline gates in policies/. Evictions: result constructors + mode resolution to helpers.lua; both cached pair rebuilds to policy_evaluation.lua, where they sit beside the live pipelines they mirror (one file to review both decision paths); ValidateUnits to actions/unit_transfer.lua as the action's declared `validate` precondition (ActionDescriptor grows the optional field — the beyond-all-reason#8226 prevalidation idea carried to its home). shared.lua files are now serialization + transfer math + category matching only. The unsynced contract grafts the cached reads onto Resources/Units, so widget call sites are unchanged; the controller validates through the action registry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Kill the hand-authored beyond-all-reason#8226 return-table: action files register via an injected registrar — Actions.RegisterValidate (optional, first) and Actions.RegisterExecute (required, exactly one) — and return nothing; the runtime descriptor is assembled by the loader, never authored. Identity is the filename (actions/unit_transfer.lua -> unit_transfer), the same rule as policies/<category>.lua. parameters/ValidateActionArgs/ ActionParameter die now: zero runtime callers, and {type="table"} is strictly weaker than the LuaCATS annotations emmylua already checks; a DERIVED schema (generated from annotations) returns if a data-driven dispatcher (mission_api/CampaignAPI) ever creates a boundary the type checker cannot see. One idiom framework-wide: policy pipelines also register (:Register() terminal, sink injected per file) instead of returning. The registrar reaches files by loader env-injection with __index fallthrough — the widget-handler idiom (local Actions = Actions); explicit named calls on an explicit local, NOT anonymous setfenv globals (the dbg_test_runner fragility). Registration includes are deliberately uncached so brackets re-fire; registrar state is plain assignment (synced strips rawset). Boundary leak fixed: advplayerlist no longer includes action files directly — registry access only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The engine's VFS.Include is uncached (LuaVFS.cpp loads + pcalls every call), which is real — but ModuleHandler.Include only deduped within one handler instance, and module_handler.lua is itself re-included per consumer, so 'load once per Lua state' was never what it did. Delete the wrapper: contracts use plain VFS.Include (familiar, honest), Get()/ LoadActions/LoadPolicies stay memoized per handler, and true load-once-per-state is the engine RFC's require() where it belongs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The per-team resource snapshot (current/storage/pull/income/expense/
shareSlider from engine reads) is economy vocabulary, and the waterfill
solver was already typed against TeamResourceData — economy depending on
a shape housed in sharing was inverted. It moves to modules/economy/,
exports on economy's contract, and sharing's context factory + resource
controller consume it via Get("economy") like the rest of the boundary.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PolicyBuilder.new/When/Allied/Enemy/Returns/Deny had spec-only references — and worse, no registration path: since registration-style landed, LoadPolicies consumes only pipeline files, so a standalone descriptor from new():Build() cannot be registered at all. Its modder story was superseded by Gate/Compute. policy_builder.lua is now the pipeline builder, full stop. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Named-local-then-register-at-the-tail split the declaration from its registration — and a forgotten Register is a silent nothing until load. Inline the function literals into the Register calls (the describe/it shape, and what pipelines already do with Gate): the registration IS the declaration, so it cannot be forgotten. Annotations sit on the literals and stay fully checked. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BuildUnitPolicyResult had exactly one consumer — policies/unit.lua — so it lives there as a local, not in the helpers grab-bag. What stays in helpers stays for structural reasons: pipeline files register and export nothing by design, so anything shared between the live pipeline and the cached rebuild/factor caches (ResolveSharingModes, TeamActive, IsNonPlayerTeam, the resource result constructors) needs a home outside the pipeline file, and helpers.lua is that home. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The stricter analyzer's first pass over our 21 authored files found 19 diagnostics and one buried body: the single-descriptor builder deleted earlier had been silently RESURRECTED by a stale rerere resolution during a branch replay — dead code, invisible to specs (its specs died with it), caught only by undefined-doc-param breadcrumbs. Excised again; rerere is now disabled for this repo and its cache purged, because auto-applied resolutions recorded against superseded overlay content are exactly wrong for a regenerated branch. The rest: @cast (not @as) for loader-built partial tables; the spec VFS shim widened to engine arity (its 1-param SubDirs/FileExists shadowed the real signatures under 0.24's overload handling); stale doc params; unused locals underscored or dropped; justified suppressions commented (Spring truthiness in lobby LuaParser, _G in the synced-state spec). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Test-runner and synced sandboxes don't expose setmetatable, so the weak-keyed cache crashed the module include chain under the integration tests. Callers only ever pass UnitDefs, so weak keys bought nothing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The four top-level pack/unpack helpers pushed gui_chat's main chunk past Lua 5.1's 200-local ceiling, so the widget failed to load. Each helper has exactly one caller (Get/SetConfigData); define them there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The runner's whitelist env omitted them, so any module code with metatable-based classes (policy_builder pipelines, module_handler's registration includes) crashed when loaded from a test file. Every real environment (widget, gadget, synced) exposes both; the omission was incidental. Reverts the flat-copy workaround in module_handler. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
module_handler resolves its include-fallback env via _G, then pcall(getfenv, 1). The sandbox has neither, so registration files got __index = nil and lost VFS. getfenv(1) now yields the sandbox env, which carries everything module code needs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Manual resource sharing depended on gadget:ResourceExcess (RecoilEngine#2642) to drive ProcessEconomy, which also refreshes the policy factor cache. On engines without the port the cache stayed at its pre-game snapshot (taxedSendable=0) and every share was denied. Fall back to GameFrame for the cadence there, and guard the AddTeamResourceExcessStats call that would crash the first tick. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
keithharvey
force-pushed
the
sharing/05-game-modes-export
branch
2 times, most recently
from
July 17, 2026 08:01
f6079ed to
a75c0ba
Compare
Collaborator
Author
|
Superseded by #8408 — recreated with its head on beyond-all-reason instead of the fork (no fork-headed PRs; branch content identical, review history stays readable here). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🧩 Chain PR 3 of 3 — stacked on #5704 (The Sharing Tab)
Warning
Draft, stacked. Based on
sharing/05-game-modes-export— the top of #5704's split stack — so the diff below is exactly this PR's own work: the 16 commits frommodules: module framework + loader hooksonward. The sharing feature itself is reviewed in #5704's split PRs; this PR reviews the module format.Important
This branch is deterministically regenerated, same discipline as the sharing split: the ~80-file relocation commit is generated from a move map against
sharing_tab's tip, and the hand-authored overlay commits are cherry-picked on top — so when #5704 moves under review,just bar::sharing-module rebuild && verifyreplays the branch and conflicts can only appear in the small overlay, never the move commit. Regeneration is byte-identical (git rev-parse HEAD^{tree}equal before/after). Tooling lives besidebar::sharing-splitin BAR-Devtools and may fold into it.My [no LLM editor] Description
Intro
So you find yourself needing to encapsulate state gameside in an expressive way that isn't fighting other hook-based gadget architectures for supremacy. You want to put all of your modifications to game behavior in one place so it's easy to understand and discover. How do you organize this? I had a modular factoring of Sharing that I had originally described then coded for sharing_tab like a year ago, then saw what CampaignAPI was dealing with and did a Leo pointing at the TV when I saw what they were up to.
Background Recap
This section is basically a quick recap of Game Controllers & Policies, so if you've read that already, skip it.
Capabilities and concepts this branch leverages from upstream branches worth understanding before you dive into this:
Going to copy a mermaid diagram I stole from my deeper dive of these topics over in Transfer Library and change it to include modules:
flowchart TD Engine[Engine] subgraph Synced subgraph SL["Internal Service Layer/Module"] direction TB Controller["behavior_controller<br/>(game_unit_transfer_controller, …)<br/>executes commands within<br/>bounds set by PolicyResult"] Context["Context<br/>(cached)"] Policy[Policy] Result[PolicyResult] Controller --> Context --> Policy --> Result Result -.->|bounds execution| Controller end Gadgets["External gadgets"] end subgraph Unsynced UI[UI] end Command["«command»<br/>GG.* action request<br/>(independent data type)"] classDef iface fill:none,stroke:#888,stroke-width:2px,stroke-dasharray:6 4; class Command iface Engine --> Controller Result -->|published cache| Gadgets Result -->|published cache| UI Gadgets -.->|send| Command UI -.->|send| Command Command -.->|request| ControllerModules
Ok, so let's talk about this.
leaning heavily on the type system for correctness between files
has exactly one way to do something
that exactly one way is explicit and typed
module.luaacts as a manifest, think "package.json" in node appsapi.luamakes public an API (bags of methods) to shared (synced and unsynced) contexts, this is equivalent to "index.ts" in node apps:api_unsynced.luadoes the same for the unsynced context:not redundant - namespaces and file names don't repeat themselves. Files are relevant to their directory peers.
modules/sharing/actions/unit_transfer.luahas 0 ambiguityindividual files are concise
Policies
Policies constrain runtime behavior and drive the UI.
For example,
sharing/policies/unit.luaDeclaration order is evaluation order, first result wins, Compute always answers - the file registers its pipeline and returns nothing, and the filename is the category. A policy can insert its own gate anywhere in the order as long as it conforms.
Actions
Writing an action file is like writing a widget. You define your functions, you register them, you return nothing.
sharing/actions/unit_transfer.lua. Notice how the ctx is injecting useful data from our pipeline into our function.ctx(short for context, sorry -- I like brevity in lexical scoped variables) provides module-scoped data primitives. But the framework itself provides a baseline primitive to inherrit from. For exampleUnitTransferContext inherits from PolicyActionContext,ResourcePolicyResult inherits from PolicyResult, and so on. In this way we can explicitly classify overlap between modules.Domain Namespacing
mod options get broken up
One of the big wins here is modoptions get split up,
modules/sharing/modoptions.luais now a thing, root modoptions still returns one flat list - it just assembles it from module fragments. One of @WatchTheFort's biggest fears is mod option bloat, so we move them all to individual modules that need them. This should also benefit the packageability of mods.modes too
Currently, the only
ModeCategorythat exists (i.e. the Sharing Tab's top-of-tab mode dropdown is justmodes where category=Sharing). So it makes a lot of sense to move those same modes to the module that owns them atmodules/sharing/modes/.specs
Having the specs live in the module just makes sense. Put them next to the code they're working.
modules can be back-ported
Once you have this thing self-contained like this, it's easy to rip these things back out to the engine as exemplars.
Campaign API
Campaign API is currently an eventing system plus a behavioral override sidecar. The events and scheduler could live as a module that works the same as every other module then we could flip it on or off. But importantly, instead of a sidecar implementation, I'm arguing that we should refactor those behavioral subsystems and make them expose their own internal state as an easy to configure API, via policies or whatever makes sense for their factoring.
Conclusion
Whew. Sorry. It's a lot. But I do think THIS PR is not boring. This one has a lot of great ideas in it that kind of become apparent (to me anyway) under this organizational structure. Hopefully with ideas from both CampaignAPI and this branch, we can make expressing game behavior considerably easier over time.
Summary (LLM-generated, claude-fable-5)
Gives the sharing stack its final form: an encapsulated module format at root-level
modules/, with opinionated auto-loaded subdirectories, manifests + per-state contracts between modules, and the policy/action patterns from #5704 promoted to framework.modules/module_handler.lua: module discovery (module.luamanifests withrequires/provides, validated with loud errors), contract resolution (ModuleHandler.Get(name)), and registration-style loading foractions/andpolicies/— files callActions.RegisterValidate/RegisterExecuteor end pipelines with:Register()via loader-injected registrars (the widget-handler idiom: explicit named calls on an explicit local) and return nothing; identity is the filename; the runtime descriptor is assembled by the loader, never hand-authored. No hand-written parameter schemas — controllers are statically typed; a derived-from-LuaCATS schema returns when a data-driven dispatcher (mission_api/CampaignAPI) creates a boundary the type checker can't see.provides = { shared, synced, unsynced }andGet()merges shared + the current Lua state into one flat api — a widget never sees synced-only keys and vice versa; wrong-state access is nil at the first index. Contracts are plain eager tables (no lazy metatables).modules/*/gadgets|widgets|rml_widgetsgame-side — the shim for engine-native module loading (Recoil RFC to follow); the layout is the contract.modules/sharing/as a domain-scoped tree (resource/,unit/,take/,tech/,policy_views/, root-levelenums.lua/helpers.lua— the module name is the namespace, nolib/). Mode presets ship with the module (modules/sharing/modes/), and the module owns its lobby options viamodules/sharing/modoptions.lua, aggregated by rootmodoptions.lua.policies/resource.lua/policies/unit.luaexpress The Sharing Tab #5704's exact gate→compute control flow through a fluent builder (Policies.Pipeline():Gate(...):Compute(...):Register()) emitting plain descriptor lists — declaration order is evaluation order, pure functions only, category = filename. Decision code lives with the policy layer (policy_evaluation.luaholds both the live pipelines and the cached-factor rebuilds); unit validation is the action's declaredvalidateprecondition;shared.luafiles are serialization + transfer math only.modules/economy/(waterfill solver, share stats, manual share ledger, the team resource snapshot) as the first cross-module dependency: sharing declaresrequires = {"economy"}and consumes it via contract; the solver is tax-agnostic (sharing injects its tax resolver).sharing_tab's.🤖 Built with Claude Code (human-directed); every commit spec-verified as above.