Layered Configuration Resolver
json_patch.merge_patch merges exactly two documents, config_sync
resolves by last-write-wins timestamp, and AssetStore is flat per
environment. None of them compose an ordered defaults < file < env < CLI
precedence stack with a deep dict merge, nor report which layer won each key.
This adds that 12-factor resolver.
Pure standard library (copy); imports no PySide6. Layers are plain
mappings supplied by the caller (the env layer is passed in, never read from
os.environ implicitly), so resolution is deterministic in CI.
Headless API
from je_auto_control import LayeredConfig, deep_merge
cfg = (LayeredConfig()
.add_layer("defaults", {"db": {"host": "local", "port": 5432}})
.add_layer("file", file_values, priority=10)
.add_layer("env", env_values, priority=20))
settings = cfg.resolve() # {"db": {"host": ..., "port": ...}}
host = cfg.get("db.host")
trace = cfg.explain("db.host") # SourceTrace(value=..., layer="env")
add_layer registers a named layer; higher priority wins (priority
defaults to insertion order, so later layers override earlier ones).
resolve deep-merges every layer in ascending priority — nested dicts are
merged recursively while scalars and lists are replaced. get reads a dotted
key from the resolved config with a default; explain returns a
SourceTrace naming the winning layer for a dotted key (raising KeyError
when absent). deep_merge is exposed as a standalone two-mapping helper.
Executor commands
AC_resolve_config deep-merges a layers list (each {name, mapping,
priority?}) into {config}. AC_explain_config returns {trace} (the
value and winning layer) for a dotted key. Both are exposed as MCP tools
(ac_resolve_config / ac_explain_config) and as Script Builder commands
under Data.