Skip to content

Commit 858d4f7

Browse files
authored
Merge pull request #289 from Integration-Automation/feat/layered-config-batch
Add layered configuration resolver
2 parents 35c53ba + 757a227 commit 858d4f7

15 files changed

Lines changed: 391 additions & 1 deletion

File tree

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
## Table of Contents
1515

16+
- [What's new (2026-06-21) — Layered Configuration Resolver](#whats-new-2026-06-21--layered-configuration-resolver)
1617
- [What's new (2026-06-21) — Server-Sent Events (SSE) Client Parser](#whats-new-2026-06-21--server-sent-events-sse-client-parser)
1718
- [What's new (2026-06-21) — Dotenv (.env) Parsing](#whats-new-2026-06-21--dotenv-env-parsing)
1819
- [What's new (2026-06-21) — RFC 9457 Problem Details Parsing](#whats-new-2026-06-21--rfc-9457-problem-details-parsing)
@@ -133,6 +134,12 @@
133134

134135
---
135136

137+
## What's new (2026-06-21) — Layered Configuration Resolver
138+
139+
Compose config with `defaults < file < env < CLI` precedence. Full reference: [`docs/source/Eng/doc/new_features/v81_features_doc.rst`](docs/source/Eng/doc/new_features/v81_features_doc.rst).
140+
141+
- **`LayeredConfig` / `deep_merge` / `SourceTrace`** (`AC_resolve_config`, `AC_explain_config`): `json_patch.merge_patch` merges two docs, `config_sync` is last-write-wins, `AssetStore` is flat-per-env — none compose an ordered precedence stack with deep merge or report which layer won each key. `add_layer(name, mapping, priority)` then `resolve()` deep-merges (nested dicts recursively, scalars/lists replaced); `explain("db.host")` names the winning layer. Layers are caller-supplied (env passed in, never `os.environ` implicitly). Pure-stdlib, deterministic.
142+
136143
## What's new (2026-06-21) — Server-Sent Events (SSE) Client Parser
137144

138145
Consume `text/event-stream` responses. Full reference: [`docs/source/Eng/doc/new_features/v80_features_doc.rst`](docs/source/Eng/doc/new_features/v80_features_doc.rst).

README/README_zh-CN.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
## 目录
1414

15+
- [本次更新 (2026-06-21) — 分层配置解析器](#本次更新-2026-06-21--分层配置解析器)
1516
- [本次更新 (2026-06-21) — Server-Sent Events (SSE) 客户端解析器](#本次更新-2026-06-21--server-sent-events-sse-客户端解析器)
1617
- [本次更新 (2026-06-21) — Dotenv (.env) 解析](#本次更新-2026-06-21--dotenv-env-解析)
1718
- [本次更新 (2026-06-21) — RFC 9457 Problem Details 解析](#本次更新-2026-06-21--rfc-9457-problem-details-解析)
@@ -132,6 +133,12 @@
132133

133134
---
134135

136+
## 本次更新 (2026-06-21) — 分层配置解析器
137+
138+
`defaults < file < env < CLI` 优先级组合配置。完整参考:[`docs/source/Zh/doc/new_features/v81_features_doc.rst`](../docs/source/Zh/doc/new_features/v81_features_doc.rst)
139+
140+
- **`LayeredConfig` / `deep_merge` / `SourceTrace`**(`AC_resolve_config``AC_explain_config`):`json_patch.merge_patch` 只合并两份文档,`config_sync` 是 last-write-wins,`AssetStore` 是每环境扁平 —— 都无法组成有序优先级堆叠并深度合并,也无法报告每个键由哪层胜出。`add_layer(name, mapping, priority)``resolve()` 深度合并(嵌套 dict 递归、标量/list 替换);`explain("db.host")` 标明胜出层。各层由调用端提供(env 由外部传入,绝不隐含 `os.environ`)。纯标准库、确定。
141+
135142
## 本次更新 (2026-06-21) — Server-Sent Events (SSE) 客户端解析器
136143

137144
消费 `text/event-stream` 响应。完整参考:[`docs/source/Zh/doc/new_features/v80_features_doc.rst`](../docs/source/Zh/doc/new_features/v80_features_doc.rst)

README/README_zh-TW.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
## 目錄
1414

15+
- [本次更新 (2026-06-21) — 分層設定解析器](#本次更新-2026-06-21--分層設定解析器)
1516
- [本次更新 (2026-06-21) — Server-Sent Events (SSE) 用戶端解析器](#本次更新-2026-06-21--server-sent-events-sse-用戶端解析器)
1617
- [本次更新 (2026-06-21) — Dotenv (.env) 解析](#本次更新-2026-06-21--dotenv-env-解析)
1718
- [本次更新 (2026-06-21) — RFC 9457 Problem Details 解析](#本次更新-2026-06-21--rfc-9457-problem-details-解析)
@@ -132,6 +133,12 @@
132133

133134
---
134135

136+
## 本次更新 (2026-06-21) — 分層設定解析器
137+
138+
`defaults < file < env < CLI` 優先序組合設定。完整參考:[`docs/source/Zh/doc/new_features/v81_features_doc.rst`](../docs/source/Zh/doc/new_features/v81_features_doc.rst)
139+
140+
- **`LayeredConfig` / `deep_merge` / `SourceTrace`**(`AC_resolve_config``AC_explain_config`):`json_patch.merge_patch` 只合併兩份文件,`config_sync` 是 last-write-wins,`AssetStore` 是每環境扁平 —— 都無法組成有序優先序堆疊並深度合併,也無法回報每個鍵由哪層勝出。`add_layer(name, mapping, priority)``resolve()` 深度合併(巢狀 dict 遞迴、純量/list 取代);`explain("db.host")` 標明勝出層。各層由呼叫端提供(env 由外部傳入,絕不隱含 `os.environ`)。純標準函式庫、具決定性。
141+
135142
## 本次更新 (2026-06-21) — Server-Sent Events (SSE) 用戶端解析器
136143

137144
消費 `text/event-stream` 回應。完整參考:[`docs/source/Zh/doc/new_features/v80_features_doc.rst`](../docs/source/Zh/doc/new_features/v80_features_doc.rst)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
Layered Configuration Resolver
2+
==============================
3+
4+
``json_patch.merge_patch`` merges exactly two documents, ``config_sync``
5+
resolves by last-write-wins timestamp, and ``AssetStore`` is flat per
6+
environment. None of them compose an ordered ``defaults < file < env < CLI``
7+
precedence stack with a deep dict merge, nor report *which layer won each key*.
8+
This adds that 12-factor resolver.
9+
10+
Pure standard library (``copy``); imports no ``PySide6``. Layers are plain
11+
mappings supplied by the caller (the env layer is passed in, never read from
12+
``os.environ`` implicitly), so resolution is deterministic in CI.
13+
14+
Headless API
15+
------------
16+
17+
.. code-block:: python
18+
19+
from je_auto_control import LayeredConfig, deep_merge
20+
21+
cfg = (LayeredConfig()
22+
.add_layer("defaults", {"db": {"host": "local", "port": 5432}})
23+
.add_layer("file", file_values, priority=10)
24+
.add_layer("env", env_values, priority=20))
25+
26+
settings = cfg.resolve() # {"db": {"host": ..., "port": ...}}
27+
host = cfg.get("db.host")
28+
trace = cfg.explain("db.host") # SourceTrace(value=..., layer="env")
29+
30+
``add_layer`` registers a named layer; higher ``priority`` wins (priority
31+
defaults to insertion order, so later layers override earlier ones).
32+
``resolve`` deep-merges every layer in ascending priority — nested dicts are
33+
merged recursively while scalars and lists are replaced. ``get`` reads a dotted
34+
key from the resolved config with a default; ``explain`` returns a
35+
``SourceTrace`` naming the winning layer for a dotted key (raising ``KeyError``
36+
when absent). ``deep_merge`` is exposed as a standalone two-mapping helper.
37+
38+
Executor commands
39+
-----------------
40+
41+
``AC_resolve_config`` deep-merges a ``layers`` list (each ``{name, mapping,
42+
priority?}``) into ``{config}``. ``AC_explain_config`` returns ``{trace}`` (the
43+
value and winning layer) for a dotted ``key``. Both are exposed as MCP tools
44+
(``ac_resolve_config`` / ``ac_explain_config``) and as Script Builder commands
45+
under **Data**.

docs/source/Eng/eng_index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ Comprehensive guides for all AutoControl features.
103103
doc/new_features/v78_features_doc
104104
doc/new_features/v79_features_doc
105105
doc/new_features/v80_features_doc
106+
doc/new_features/v81_features_doc
106107
doc/ocr_backends/ocr_backends_doc
107108
doc/observability/observability_doc
108109
doc/operations_layer/operations_layer_doc
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
分層設定解析器
2+
============
3+
4+
``json_patch.merge_patch`` 只合併兩份文件,``config_sync`` 以 last-write-wins 時間戳解析,
5+
``AssetStore`` 則是每環境的扁平結構。它們都無法組成一個有序的 ``defaults < file < env < CLI`` 優先序
6+
堆疊並做深度 dict 合併,也無法回報*每個鍵由哪一層勝出*。本功能補上這個 12-factor 解析器。
7+
8+
純標準函式庫(``copy``);不匯入 ``PySide6``。各層為呼叫端提供的純 mapping(env 層由外部傳入,絕不
9+
隱含讀取 ``os.environ``),因此解析在 CI 中具決定性。
10+
11+
無頭 API
12+
--------
13+
14+
.. code-block:: python
15+
16+
from je_auto_control import LayeredConfig, deep_merge
17+
18+
cfg = (LayeredConfig()
19+
.add_layer("defaults", {"db": {"host": "local", "port": 5432}})
20+
.add_layer("file", file_values, priority=10)
21+
.add_layer("env", env_values, priority=20))
22+
23+
settings = cfg.resolve() # {"db": {"host": ..., "port": ...}}
24+
host = cfg.get("db.host")
25+
trace = cfg.explain("db.host") # SourceTrace(value=..., layer="env")
26+
27+
``add_layer`` 註冊一個具名層;``priority`` 越高越勝出(預設為插入順序,因此後加的層覆蓋先前的)。
28+
``resolve`` 依優先序由低到高深度合併每一層 —— 巢狀 dict 遞迴合併,而純量與 list 直接取代。``get`` 以
29+
點分鍵從解析後設定讀取並帶預設值;``explain`` 回傳 ``SourceTrace``,標明點分鍵的勝出層(不存在時拋
30+
``KeyError``)。``deep_merge`` 另以獨立的雙 mapping 輔助函式提供。
31+
32+
執行器命令
33+
----------
34+
35+
``AC_resolve_config`` 把 ``layers`` 清單(每筆 ``{name, mapping, priority?}``)深度合併成
36+
``{config}``。``AC_explain_config`` 對點分 ``key`` 回傳 ``{trace}``(值與勝出層)。兩者皆以 MCP 工具
37+
(``ac_resolve_config`` / ``ac_explain_config``)以及 Script Builder 中 **Data** 分類下的命令提供。

docs/source/Zh/zh_index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ AutoControl 所有功能的完整使用指南。
103103
doc/new_features/v78_features_doc
104104
doc/new_features/v79_features_doc
105105
doc/new_features/v80_features_doc
106+
doc/new_features/v81_features_doc
106107
doc/ocr_backends/ocr_backends_doc
107108
doc/observability/observability_doc
108109
doc/operations_layer/operations_layer_doc

je_auto_control/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,10 @@
274274
from je_auto_control.utils.dotenv import (
275275
dotenv_values, dump_dotenv, load_dotenv, parse_dotenv,
276276
)
277+
# Layered config resolver (defaults < file < env < CLI, with provenance)
278+
from je_auto_control.utils.layered_config import (
279+
LayeredConfig, SourceTrace, deep_merge,
280+
)
277281
# Outbound CloudEvents emitter
278282
from je_auto_control.utils.events import (
279283
EventEmitter, post_cloudevent, to_cloudevent,
@@ -860,6 +864,7 @@ def start_autocontrol_gui(*args, **kwargs):
860864
"rank_automation_candidates",
861865
"Asset", "AssetStore", "AssetValue", "active_environment",
862866
"dotenv_values", "dump_dotenv", "load_dotenv", "parse_dotenv",
867+
"LayeredConfig", "SourceTrace", "deep_merge",
863868
"EventEmitter", "post_cloudevent", "to_cloudevent",
864869
"WebhookChannel", "WebhookResult", "notify_webhook", "set_default_poster",
865870
"json_extract", "json_query", "json_query_one",

je_auto_control/gui/script_builder/command_schema.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1678,6 +1678,25 @@ def _add_resilience_specs(specs: List[CommandSpec]) -> None:
16781678
),
16791679
description="Parse a text/event-stream blob into events.",
16801680
))
1681+
specs.append(CommandSpec(
1682+
"AC_resolve_config", "Data", "Layered Config: Resolve",
1683+
fields=(
1684+
FieldSpec("layers", FieldType.STRING,
1685+
placeholder='[{"name": "defaults", "mapping": {}}, '
1686+
'{"name": "env", "mapping": {}, '
1687+
'"priority": 10}]'),
1688+
),
1689+
description="Deep-merge ordered config layers into one config.",
1690+
))
1691+
specs.append(CommandSpec(
1692+
"AC_explain_config", "Data", "Layered Config: Explain Key",
1693+
fields=(
1694+
FieldSpec("layers", FieldType.STRING,
1695+
placeholder='[{"name": "defaults", "mapping": {}}]'),
1696+
FieldSpec("key", FieldType.STRING, placeholder="db.host"),
1697+
),
1698+
description="Show the value and winning layer for a dotted config key.",
1699+
))
16811700
specs.append(CommandSpec(
16821701
"AC_rate_limit", "Flow", "Rate Limit (Token Bucket)",
16831702
fields=(

je_auto_control/utils/executor/action_executor.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3050,6 +3050,31 @@ def _parse_sse(text: str) -> Dict[str, Any]:
30503050
return {"events": [event.to_dict() for event in parse_event_stream(text)]}
30513051

30523052

3053+
def _build_layered_config(layers: Any):
3054+
"""Build a LayeredConfig from a list of {name, mapping, priority?} dicts."""
3055+
import json
3056+
from je_auto_control.utils.layered_config import LayeredConfig
3057+
if isinstance(layers, str):
3058+
layers = json.loads(layers)
3059+
config = LayeredConfig()
3060+
for layer in layers:
3061+
config.add_layer(layer["name"], layer.get("mapping", {}),
3062+
layer.get("priority"))
3063+
return config
3064+
3065+
3066+
def _resolve_config(layers: Any) -> Dict[str, Any]:
3067+
"""Adapter: deep-merge config layers into a resolved {config}."""
3068+
return {"config": _build_layered_config(layers).resolve()}
3069+
3070+
3071+
def _explain_config(layers: Any, key: str) -> Dict[str, Any]:
3072+
"""Adapter: report the value and winning layer for a dotted config key."""
3073+
trace = _build_layered_config(layers).explain(key)
3074+
return {"trace": {"key": trace.key, "value": trace.value,
3075+
"layer": trace.layer}}
3076+
3077+
30533078
def _percentiles(samples: Any, qs: Any = None) -> Dict[str, Any]:
30543079
"""Adapter: exact percentiles of a numeric sample list (or JSON string)."""
30553080
import json
@@ -4134,6 +4159,8 @@ def __init__(self):
41344159
"AC_parse_dotenv": _parse_dotenv,
41354160
"AC_load_dotenv": _load_dotenv,
41364161
"AC_parse_sse": _parse_sse,
4162+
"AC_resolve_config": _resolve_config,
4163+
"AC_explain_config": _explain_config,
41374164
"AC_unified_diff": _unified_diff,
41384165
"AC_apply_unified": _apply_unified,
41394166
"AC_three_way_merge": _three_way_merge,

0 commit comments

Comments
 (0)