Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

## Table of Contents

- [What's new (2026-06-22) — Typed Configuration Schema](#whats-new-2026-06-22--typed-configuration-schema)
- [What's new (2026-06-22) — OTLP/JSON Span Export](#whats-new-2026-06-22--otlpjson-span-export)
- [What's new (2026-06-22) — Canonical Log Lines & Structured Logging](#whats-new-2026-06-22--canonical-log-lines--structured-logging)
- [What's new (2026-06-22) — Conditional HTTP Requests & Cache Validators](#whats-new-2026-06-22--conditional-http-requests--cache-validators)
Expand Down Expand Up @@ -147,6 +148,12 @@

---

## What's new (2026-06-22) — Typed Configuration Schema

Validate config into a typed object. Full reference: [`docs/source/Eng/doc/new_features/v95_features_doc.rst`](docs/source/Eng/doc/new_features/v95_features_doc.rst).

- **`ConfigSchema` / `ConfigField` / `validate_config` / `coerce`** (`AC_validate_config`): `assets._coerce` coerces one value and `json_schema` validates structure, but nothing bound a resolved config dict into a typed object with required-field enforcement and choice constraints. This coerces types (`str`/`int`/`float`/`bool`), applies defaults, enforces required/choices, and returns `{ok, config, errors}` — a stdlib pydantic-settings analog. Pure-stdlib, deterministic.

## What's new (2026-06-22) — OTLP/JSON Span Export

Export spans the way a collector ingests them. Full reference: [`docs/source/Eng/doc/new_features/v94_features_doc.rst`](docs/source/Eng/doc/new_features/v94_features_doc.rst).
Expand Down
7 changes: 7 additions & 0 deletions README/README_zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

## 目录

- [本次更新 (2026-06-22) — 具类型的配置结构](#本次更新-2026-06-22--具类型的配置结构)
- [本次更新 (2026-06-22) — OTLP/JSON Span 导出](#本次更新-2026-06-22--otlpjson-span-导出)
- [本次更新 (2026-06-22) — 标准日志行与结构化日志](#本次更新-2026-06-22--标准日志行与结构化日志)
- [本次更新 (2026-06-22) — 条件式 HTTP 请求与缓存验证子](#本次更新-2026-06-22--条件式-http-请求与缓存验证子)
Expand Down Expand Up @@ -146,6 +147,12 @@

---

## 本次更新 (2026-06-22) — 具类型的配置结构

把配置验证成具类型的对象。完整参考:[`docs/source/Zh/doc/new_features/v95_features_doc.rst`](../docs/source/Zh/doc/new_features/v95_features_doc.rst)。

- **`ConfigSchema` / `ConfigField` / `validate_config` / `coerce`**(`AC_validate_config`):`assets._coerce` 只转换单一值,`json_schema` 只验证结构,但没有东西把已解析配置 dict 绑定成具类型对象并做必填强制与选项约束。本功能转换类型(`str`/`int`/`float`/`bool`)、应用默认、强制必填/选项,返回 `{ok, config, errors}` —— 标准库版 pydantic-settings。纯标准库、确定。

## 本次更新 (2026-06-22) — OTLP/JSON Span 导出

以 collector 摄取的格式导出 span。完整参考:[`docs/source/Zh/doc/new_features/v94_features_doc.rst`](../docs/source/Zh/doc/new_features/v94_features_doc.rst)。
Expand Down
7 changes: 7 additions & 0 deletions README/README_zh-TW.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

## 目錄

- [本次更新 (2026-06-22) — 具型別的設定結構](#本次更新-2026-06-22--具型別的設定結構)
- [本次更新 (2026-06-22) — OTLP/JSON Span 匯出](#本次更新-2026-06-22--otlpjson-span-匯出)
- [本次更新 (2026-06-22) — 標準日誌行與結構化日誌](#本次更新-2026-06-22--標準日誌行與結構化日誌)
- [本次更新 (2026-06-22) — 條件式 HTTP 請求與快取驗證子](#本次更新-2026-06-22--條件式-http-請求與快取驗證子)
Expand Down Expand Up @@ -146,6 +147,12 @@

---

## 本次更新 (2026-06-22) — 具型別的設定結構

把設定驗證成具型別的物件。完整參考:[`docs/source/Zh/doc/new_features/v95_features_doc.rst`](../docs/source/Zh/doc/new_features/v95_features_doc.rst)。

- **`ConfigSchema` / `ConfigField` / `validate_config` / `coerce`**(`AC_validate_config`):`assets._coerce` 只轉換單一值,`json_schema` 只驗證結構,但沒有東西把已解析設定 dict 綁定成具型別物件並做必填強制與選項約束。本功能轉換型別(`str`/`int`/`float`/`bool`)、套用預設、強制必填/選項,回傳 `{ok, config, errors}` —— 標準函式庫版 pydantic-settings。純標準函式庫、具決定性。

## 本次更新 (2026-06-22) — OTLP/JSON Span 匯出

以 collector 攝取的格式匯出 span。完整參考:[`docs/source/Zh/doc/new_features/v94_features_doc.rst`](../docs/source/Zh/doc/new_features/v94_features_doc.rst)。
Expand Down
41 changes: 41 additions & 0 deletions docs/source/Eng/doc/new_features/v95_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
Typed Configuration Schema
==========================

``assets._coerce`` coerces a single value and ``json_schema`` validates JSON
structure, but nothing bound a resolved config dict into a typed object with
required-field enforcement and choice constraints. This validates a mapping
against declared fields, coercing types and reporting actionable errors — a
stdlib analog of pydantic-settings.

Pure standard library (``dataclasses``); imports no ``PySide6``. Validation is a
pure function (mapping in, report out), so it is fully deterministic in CI.

Headless API
------------

.. code-block:: python

from je_auto_control import ConfigSchema, ConfigField, validate_config

schema = ConfigSchema({
"port": ConfigField("int", required=True),
"env": ConfigField("str", default="dev", choices=["dev", "prod"]),
"debug": ConfigField("bool", default=False),
})
report = schema.validate({"port": "8080", "debug": "yes"})
# {"ok": True, "config": {"port": 8080, "env": "dev", "debug": True}, "errors": []}

``ConfigField`` declares a ``type`` (``str`` / ``int`` / ``float`` / ``bool``),
optional ``default``, ``required`` flag, ``choices``, and an ``env`` hint.
``ConfigSchema.validate`` coerces each present value, applies defaults, enforces
required fields and choices, and returns ``{ok, config, errors}`` (errors as
``{field, error}``). ``ConfigSchema.from_dict`` builds a schema from a plain
spec, ``validate_config`` does spec-plus-mapping in one call, and ``coerce``
exposes the value coercion (booleans accept ``true``/``yes``/``on`` etc.).

Executor command
----------------

``AC_validate_config`` validates a ``config`` mapping against a ``schema`` spec
and returns ``{ok, config, errors}``. It is exposed as the MCP tool
``ac_validate_config`` and as a Script Builder command under **Data**.
1 change: 1 addition & 0 deletions docs/source/Eng/eng_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ Comprehensive guides for all AutoControl features.
doc/new_features/v92_features_doc
doc/new_features/v93_features_doc
doc/new_features/v94_features_doc
doc/new_features/v95_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
35 changes: 35 additions & 0 deletions docs/source/Zh/doc/new_features/v95_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
具型別的設定結構
==============

``assets._coerce`` 只轉換單一值,``json_schema`` 只驗證 JSON 結構,但沒有任何東西能把已解析的設定 dict
綁定成具型別的物件,並做必填欄位強制與選項約束。本功能依宣告的欄位驗證一個 mapping,轉換型別並回報可
據以行動的錯誤 —— 標準函式庫版的 pydantic-settings。

純標準函式庫(``dataclasses``);不匯入 ``PySide6``。驗證為純函式(輸入 mapping、輸出報告),因此在 CI
中完全具決定性。

無頭 API
--------

.. code-block:: python

from je_auto_control import ConfigSchema, ConfigField, validate_config

schema = ConfigSchema({
"port": ConfigField("int", required=True),
"env": ConfigField("str", default="dev", choices=["dev", "prod"]),
"debug": ConfigField("bool", default=False),
})
report = schema.validate({"port": "8080", "debug": "yes"})
# {"ok": True, "config": {"port": 8080, "env": "dev", "debug": True}, "errors": []}

``ConfigField`` 宣告 ``type``(``str`` / ``int`` / ``float`` / ``bool``)、選用的 ``default``、``required``
旗標、``choices`` 與 ``env`` 提示。``ConfigSchema.validate`` 轉換每個存在的值、套用預設、強制必填與選項,
回傳 ``{ok, config, errors}``(錯誤為 ``{field, error}``)。``ConfigSchema.from_dict`` 從純 spec 建立結構,
``validate_config`` 一次完成 spec 加 mapping,``coerce`` 公開值轉換(布林接受 ``true``/``yes``/``on`` 等)。

執行器命令
----------

``AC_validate_config`` 依 ``schema`` spec 驗證 ``config`` mapping,回傳 ``{ok, config, errors}``。它以 MCP
工具 ``ac_validate_config`` 以及 Script Builder 中 **Data** 分類下的命令提供。
1 change: 1 addition & 0 deletions docs/source/Zh/zh_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ AutoControl 所有功能的完整使用指南。
doc/new_features/v92_features_doc
doc/new_features/v93_features_doc
doc/new_features/v94_features_doc
doc/new_features/v95_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
5 changes: 5 additions & 0 deletions je_auto_control/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,10 @@
from je_auto_control.utils.layered_config import (
LayeredConfig, SourceTrace, deep_merge,
)
# Typed config schema validation (coerce + required + choices)
from je_auto_control.utils.config_schema import (
ConfigField, ConfigSchema, coerce, validate_config,
)
# URI-scheme secret/value reference resolver (env:// / file:// / secret://)
from je_auto_control.utils.secret_ref import (
RefResolver, SecretRefError, is_ref, resolve_ref, resolve_refs_in,
Expand Down Expand Up @@ -921,6 +925,7 @@ def start_autocontrol_gui(*args, **kwargs):
"Asset", "AssetStore", "AssetValue", "active_environment",
"dotenv_values", "dump_dotenv", "load_dotenv", "parse_dotenv",
"LayeredConfig", "SourceTrace", "deep_merge",
"ConfigField", "ConfigSchema", "coerce", "validate_config",
"RefResolver", "SecretRefError", "is_ref", "resolve_ref", "resolve_refs_in",
"redact_config", "redact_secret_text",
"EventEmitter", "post_cloudevent", "to_cloudevent",
Expand Down
10 changes: 10 additions & 0 deletions je_auto_control/gui/script_builder/command_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -1676,6 +1676,16 @@ def _add_resilience_specs(specs: List[CommandSpec]) -> None:
),
description="Recursively resolve references inside a JSON structure.",
))
specs.append(CommandSpec(
"AC_validate_config", "Data", "Config Schema: Validate",
fields=(
FieldSpec("schema", FieldType.STRING,
placeholder='{"port": {"type": "int", "required": true}}'),
FieldSpec("config", FieldType.STRING,
placeholder='{"port": "8080"}'),
),
description="Validate a config mapping against a typed schema spec.",
))
specs.append(CommandSpec(
"AC_redact_config", "Security", "Redaction: Redact Config",
fields=(
Expand Down
6 changes: 6 additions & 0 deletions je_auto_control/utils/config_schema/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Typed configuration schema validation for AutoControl."""
from je_auto_control.utils.config_schema.config_schema import (
ConfigField, ConfigSchema, coerce, validate_config,
)

__all__ = ["ConfigField", "ConfigSchema", "coerce", "validate_config"]
103 changes: 103 additions & 0 deletions je_auto_control/utils/config_schema/config_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""Typed, schema-validated configuration (a stdlib pydantic-settings analog).

``assets._coerce`` coerces a single value and ``json_schema`` validates JSON
structure, but nothing bound a resolved config dict into a typed object with
required-field enforcement and choice constraints. This validates a mapping
against declared fields, coercing types and reporting actionable errors.

Pure standard library (``dataclasses``); imports no ``PySide6``. Validation is a
pure function (mapping in, report out), so it is fully deterministic in CI.
"""
from dataclasses import dataclass, field as dataclass_field
from typing import Any, Dict, List, Mapping, Optional, Sequence

_MISSING = object()


@dataclass
class ConfigField:
"""A single typed configuration field."""

type: str = "str"
default: Any = _MISSING
required: bool = False
choices: Optional[Sequence[Any]] = None
env: Optional[str] = None


def _to_bool(value: Any) -> bool:
if isinstance(value, bool):
return value
text = str(value).strip().lower()
if text in ("true", "1", "yes", "on"):
return True
if text in ("false", "0", "no", "off"):
return False
raise ValueError(f"not a boolean: {value!r}")


def coerce(value: Any, kind: str) -> Any:
"""Coerce ``value`` to ``kind`` (``str`` / ``int`` / ``float`` / ``bool``)."""
if kind == "int":
return int(value)
if kind == "float":
return float(value)
if kind == "bool":
return _to_bool(value)
if kind == "str":
return str(value)
return value


@dataclass
class ConfigSchema:
"""A set of named :class:`ConfigField` definitions."""

fields: Dict[str, ConfigField] = dataclass_field(default_factory=dict)

@classmethod
def from_dict(cls, spec: Mapping[str, Mapping[str, Any]]) -> "ConfigSchema":
"""Build a schema from a ``{name: {type, default, required, ...}}`` spec."""
fields = {name: ConfigField(
type=raw.get("type", "str"),
default=raw.get("default", _MISSING),
required=bool(raw.get("required", False)),
choices=raw.get("choices"),
env=raw.get("env")) for name, raw in spec.items()}
return cls(fields)

def _resolve_field(self, name: str, definition: ConfigField,
mapping: Mapping[str, Any]) -> Any:
"""Return ``(status, payload)``: ``ok``/value, ``error``/dict, ``skip``."""
if name in mapping:
try:
value = coerce(mapping[name], definition.type)
except (ValueError, TypeError):
return "error", {"field": name,
"error": f"cannot coerce to {definition.type}"}
if definition.choices is not None and value not in definition.choices:
return "error", {"field": name, "error": "not an allowed choice"}
return "ok", value
if definition.default is not _MISSING:
return "ok", definition.default
if definition.required:
return "error", {"field": name, "error": "required"}
return "skip", None

def validate(self, mapping: Mapping[str, Any]) -> Dict[str, Any]:
"""Validate ``mapping``; return ``{ok, config, errors}``."""
config: Dict[str, Any] = {}
errors: List[Dict[str, str]] = []
for name, definition in self.fields.items():
status, payload = self._resolve_field(name, definition, mapping)
if status == "ok":
config[name] = payload
elif status == "error":
errors.append(payload)
return {"ok": not errors, "config": config, "errors": errors}


def validate_config(spec: Mapping[str, Mapping[str, Any]],
mapping: Mapping[str, Any]) -> Dict[str, Any]:
"""Validate ``mapping`` against a schema ``spec`` dict in one call."""
return ConfigSchema.from_dict(spec).validate(mapping)
12 changes: 12 additions & 0 deletions je_auto_control/utils/executor/action_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3000,6 +3000,17 @@ def _trace_extract(headers: Any) -> Dict[str, Any]:
return {"context": ctx.to_dict() if ctx is not None else None}


def _validate_config(schema: Any, config: Any) -> Dict[str, Any]:
"""Adapter: validate a config mapping against a schema spec."""
import json
from je_auto_control.utils.config_schema import validate_config
if isinstance(schema, str):
schema = json.loads(schema)
if isinstance(config, str):
config = json.loads(config)
return validate_config(schema, config)


def _resolve_ref(ref: str) -> Dict[str, Any]:
"""Adapter: resolve an env:// / file:// / secret:// reference."""
from je_auto_control.utils.secret_ref import resolve_ref
Expand Down Expand Up @@ -4401,6 +4412,7 @@ def __init__(self):
"AC_baggage_format": _baggage_format,
"AC_canonical_log": _canonical_log,
"AC_spans_to_otlp": _spans_to_otlp,
"AC_validate_config": _validate_config,
"AC_resolve_ref": _resolve_ref,
"AC_resolve_refs": _resolve_refs,
"AC_redact_config": _redact_config,
Expand Down
18 changes: 17 additions & 1 deletion je_auto_control/utils/mcp_server/tools/_factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -3710,6 +3710,22 @@ def config_redaction_tools() -> List[MCPTool]:
]


def config_schema_tools() -> List[MCPTool]:
return [
MCPTool(
name="ac_validate_config",
description=("Validate a 'config' mapping against a 'schema' spec "
"({name: {type, default, required, choices}}); coerces "
"types. Returns {ok, config, errors}."),
input_schema=schema(
{"schema": {"type": "object"}, "config": {"type": "object"}},
["schema", "config"]),
handler=h.validate_config,
annotations=READ_ONLY,
),
]


def secret_ref_tools() -> List[MCPTool]:
return [
MCPTool(
Expand Down Expand Up @@ -5345,7 +5361,7 @@ def media_assert_tools() -> List[MCPTool]:
feature_flag_tools, provenance_tools, json_contract_tools, chaos_tools,
slo_tools, percentiles_tools, bulkhead_tools, http_cassette_tools,
trace_context_tools, baggage_tools, canonical_log_tools, otlp_export_tools,
secret_ref_tools, config_redaction_tools,
secret_ref_tools, config_schema_tools, config_redaction_tools,
data_profile_tools, http_problem_tools, dotenv_tools,
sse_client_tools, layered_config_tools, data_drift_tools,
dataset_diff_tools, referential_tools, link_header_tools, multipart_tools,
Expand Down
5 changes: 5 additions & 0 deletions je_auto_control/utils/mcp_server/tools/_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1746,6 +1746,11 @@ def spans_to_otlp(spans, resource_attrs=None):
return _spans_to_otlp(spans, resource_attrs)


def validate_config(schema, config):
from je_auto_control.utils.executor.action_executor import _validate_config
return _validate_config(schema, config)


def resolve_ref(ref):
from je_auto_control.utils.executor.action_executor import _resolve_ref
return _resolve_ref(ref)
Expand Down
Loading
Loading