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-21) — Feature Flags](#whats-new-2026-06-21--feature-flags)
- [What's new (2026-06-21) — Text Diff, Patch & Three-Way Merge](#whats-new-2026-06-21--text-diff-patch--three-way-merge)
- [What's new (2026-06-21) — Calendar Recurrence Rules (RRULE)](#whats-new-2026-06-21--calendar-recurrence-rules-rrule)
- [What's new (2026-06-21) — Statistics & A/B Significance](#whats-new-2026-06-21--statistics--ab-significance)
Expand Down Expand Up @@ -120,6 +121,12 @@

---

## What's new (2026-06-21) — Feature Flags

Toggle behavior with targeting & rollout. Full reference: [`docs/source/Eng/doc/new_features/v68_features_doc.rst`](docs/source/Eng/doc/new_features/v68_features_doc.rst).

- **`FlagStore` / `evaluate_flag` / `is_enabled` / `assign_variant`** (`AC_evaluate_flag`, `AC_flag_enabled`): `decision_table` is one-shot DMN and `ab_locator` is locator A/B — neither is a product flag store with sticky % rollout. This adds an OpenFeature-shaped engine: targeting rules (`eq`/`in`/`semver_*`…), weighted variants, kill switch, and consistent-hash bucketing (`sha256(key.salt.context_key)`) so a subject is **sticky**. Returns `{value, variant, reason}` (`TARGETING_MATCH`/`SPLIT`/`DISABLED`/`ERROR`). Pure-stdlib, deterministic.

## What's new (2026-06-21) — Text Diff, Patch & Three-Way Merge

Apply and merge text diffs. Full reference: [`docs/source/Eng/doc/new_features/v67_features_doc.rst`](docs/source/Eng/doc/new_features/v67_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-21) — 功能旗标](#本次更新-2026-06-21--功能旗标)
- [本次更新 (2026-06-21) — 文本 Diff、应用与三方合并](#本次更新-2026-06-21--文本-diff应用与三方合并)
- [本次更新 (2026-06-21) — 日历周期规则(RRULE)](#本次更新-2026-06-21--日历周期规则rrule)
- [本次更新 (2026-06-21) — 统计与 A/B 显著性](#本次更新-2026-06-21--统计与-ab-显著性)
Expand Down Expand Up @@ -119,6 +120,12 @@

---

## 本次更新 (2026-06-21) — 功能旗标

以目标规则与推出切换行为。完整参考:[`docs/source/Zh/doc/new_features/v68_features_doc.rst`](../docs/source/Zh/doc/new_features/v68_features_doc.rst)。

- **`FlagStore` / `evaluate_flag` / `is_enabled` / `assign_variant`**(`AC_evaluate_flag`、`AC_flag_enabled`):`decision_table` 是一次性 DMN,`ab_locator` 是定位器 A/B —— 两者都不是带黏性 % 推出的产品旗标存储库。本功能补上 OpenFeature 形状引擎:目标规则(`eq`/`in`/`semver_*`…)、加权变体、kill switch,以及一致哈希分桶(`sha256(key.salt.context_key)`)使主体具**黏性**。返回 `{value, variant, reason}`(`TARGETING_MATCH`/`SPLIT`/`DISABLED`/`ERROR`)。纯标准库、确定。

## 本次更新 (2026-06-21) — 文本 Diff、应用与三方合并

应用并合并文本 diff。完整参考:[`docs/source/Zh/doc/new_features/v67_features_doc.rst`](../docs/source/Zh/doc/new_features/v67_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-21) — 功能旗標](#本次更新-2026-06-21--功能旗標)
- [本次更新 (2026-06-21) — 文字 Diff、套用與三方合併](#本次更新-2026-06-21--文字-diff套用與三方合併)
- [本次更新 (2026-06-21) — 行事曆週期規則(RRULE)](#本次更新-2026-06-21--行事曆週期規則rrule)
- [本次更新 (2026-06-21) — 統計與 A/B 顯著性](#本次更新-2026-06-21--統計與-ab-顯著性)
Expand Down Expand Up @@ -119,6 +120,12 @@

---

## 本次更新 (2026-06-21) — 功能旗標

以目標規則與推出切換行為。完整參考:[`docs/source/Zh/doc/new_features/v68_features_doc.rst`](../docs/source/Zh/doc/new_features/v68_features_doc.rst)。

- **`FlagStore` / `evaluate_flag` / `is_enabled` / `assign_variant`**(`AC_evaluate_flag`、`AC_flag_enabled`):`decision_table` 是一次性 DMN,`ab_locator` 是定位器 A/B —— 兩者都不是帶黏性 % 推出的產品旗標儲存庫。本功能補上 OpenFeature 形狀引擎:目標規則(`eq`/`in`/`semver_*`…)、加權變體、kill switch,以及一致雜湊分桶(`sha256(key.salt.context_key)`)使主體具**黏性**。回傳 `{value, variant, reason}`(`TARGETING_MATCH`/`SPLIT`/`DISABLED`/`ERROR`)。純標準函式庫、具決定性。

## 本次更新 (2026-06-21) — 文字 Diff、套用與三方合併

套用並合併文字 diff。完整參考:[`docs/source/Zh/doc/new_features/v67_features_doc.rst`](../docs/source/Zh/doc/new_features/v67_features_doc.rst)。
Expand Down
58 changes: 58 additions & 0 deletions docs/source/Eng/doc/new_features/v68_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
Feature Flags
=============

``decision_table`` is a one-shot DMN evaluator and ``ab_locator`` measures
locator outcomes — neither is a product feature-flag store with sticky
percentage rollout. This adds an OpenFeature-shaped flag engine: typed flags
with targeting rules, weighted variants, a kill switch, and consistent-hash
bucketing so a given subject always lands in the same variant.

Pure standard library (``hashlib`` + ``re`` + ``json``); deterministic; imports
no ``PySide6``.

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

.. code-block:: python

from je_auto_control import FlagStore, evaluate_flag, is_enabled

store = FlagStore.from_dict({"flags": {
"new-checkout": {
"variants": {"on": True, "off": False},
"default_variant": "off", "off_variant": "off",
"targeting": [
{"conditions": {"country": {"op": "in", "value": ["US", "CA"]}},
"serve": "on"},
{"conditions": {"plan": {"op": "eq", "value": "premium"}},
"serve": {"rollout": {"on": 50, "off": 50}}},
],
"fallthrough": {"rollout": {"on": 10, "off": 90}},
},
}})

evaluate_flag(store, "new-checkout", {"country": "US"})
# {"flag_key": "new-checkout", "variant": "on", "value": True,
# "reason": "TARGETING_MATCH"}

if is_enabled(store, "new-checkout", {"targeting_key": "user-123"}):
...

Evaluation order mirrors OpenFeature/Unleash/LaunchDarkly: a disabled flag
serves ``off_variant`` (reason ``DISABLED``); an unknown flag returns the
caller default (reason ``ERROR``); targeting rules are tried in order
(``TARGETING_MATCH``); otherwise the fallthrough applies (``DEFAULT`` /
``SPLIT``). Targeting operators include ``eq``/``ne``/``lt``/``gt``/``in``/
``not_in``/``contains`` and ``semver_*``. Percentage rollout is a
consistent-hash bucket of ``sha256("{key}.{salt}.{context_key}")`` so a subject
is **sticky** — it always gets the same variant. ``percentage_bucket`` and
``assign_variant`` are exposed for direct use.

Executor commands
-----------------

``AC_evaluate_flag`` takes ``flags`` (a store mapping or JSON string), a
``key`` and optional ``context``; it returns ``{value, variant, reason}``.
``AC_flag_enabled`` returns ``{enabled}``. Both are exposed as MCP tools
(``ac_evaluate_flag`` / ``ac_flag_enabled``) and as Script Builder commands
under **Flow**.
1 change: 1 addition & 0 deletions docs/source/Eng/eng_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ Comprehensive guides for all AutoControl features.
doc/new_features/v65_features_doc
doc/new_features/v66_features_doc
doc/new_features/v67_features_doc
doc/new_features/v68_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
50 changes: 50 additions & 0 deletions docs/source/Zh/doc/new_features/v68_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
功能旗標(Feature Flags)
========================

``decision_table`` 是一次性的 DMN 評估器,``ab_locator`` 衡量定位器成效 —— 兩者都不是帶有黏性
百分比推出的產品功能旗標儲存庫。本功能補上 OpenFeature 形狀的旗標引擎:具型別的旗標、目標規則、
加權變體、kill switch,以及一致雜湊分桶,讓同一主體永遠落在同一變體。

純標準函式庫(``hashlib`` + ``re`` + ``json``);具決定性;不匯入 ``PySide6``。

無頭 API
--------

.. code-block:: python

from je_auto_control import FlagStore, evaluate_flag, is_enabled

store = FlagStore.from_dict({"flags": {
"new-checkout": {
"variants": {"on": True, "off": False},
"default_variant": "off", "off_variant": "off",
"targeting": [
{"conditions": {"country": {"op": "in", "value": ["US", "CA"]}},
"serve": "on"},
{"conditions": {"plan": {"op": "eq", "value": "premium"}},
"serve": {"rollout": {"on": 50, "off": 50}}},
],
"fallthrough": {"rollout": {"on": 10, "off": 90}},
},
}})

evaluate_flag(store, "new-checkout", {"country": "US"})
# {"flag_key": "new-checkout", "variant": "on", "value": True,
# "reason": "TARGETING_MATCH"}

if is_enabled(store, "new-checkout", {"targeting_key": "user-123"}):
...

評估順序對齊 OpenFeature/Unleash/LaunchDarkly:停用的旗標提供 ``off_variant``(原因
``DISABLED``);未知旗標回傳呼叫端預設(原因 ``ERROR``);目標規則依序嘗試(``TARGETING_MATCH``);
否則套用 fallthrough(``DEFAULT`` / ``SPLIT``)。目標運算子包含 ``eq``/``ne``/``lt``/``gt``/
``in``/``not_in``/``contains`` 與 ``semver_*``。百分比推出是
``sha256("{key}.{salt}.{context_key}")`` 的一致雜湊分桶,因此主體具**黏性** —— 永遠得到相同變體。
``percentage_bucket`` 與 ``assign_variant`` 亦可直接使用。

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

``AC_evaluate_flag`` 接受 ``flags``(store 對映或 JSON 字串)、``key`` 與選用的 ``context``,回傳
``{value, variant, reason}``。``AC_flag_enabled`` 回傳 ``{enabled}``。兩者皆以 MCP 工具
(``ac_evaluate_flag`` / ``ac_flag_enabled``)以及 Script Builder 中 **Flow** 分類下的命令提供。
1 change: 1 addition & 0 deletions docs/source/Zh/zh_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ AutoControl 所有功能的完整使用指南。
doc/new_features/v65_features_doc
doc/new_features/v66_features_doc
doc/new_features/v67_features_doc
doc/new_features/v68_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
7 changes: 7 additions & 0 deletions je_auto_control/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,11 @@
from je_auto_control.utils.text_diff import (
MergeResult, PatchApplyError, apply_unified, three_way_merge, unified_diff,
)
# Feature flags with targeting rules + deterministic rollout
from je_auto_control.utils.feature_flags import (
Flag, FlagStore, assign_variant, evaluate_flag, is_enabled,
percentage_bucket,
)
# Background popup/interrupt watchdog (unattended automation)
from je_auto_control.utils.watchdog import (
PopupWatchdog, WatchdogRule, default_popup_watchdog,
Expand Down Expand Up @@ -834,6 +839,8 @@ def start_autocontrol_gui(*args, **kwargs):
"Recurrence", "next_occurrence", "occurrences", "parse_rrule",
"MergeResult", "PatchApplyError", "apply_unified", "three_way_merge",
"unified_diff",
"Flag", "FlagStore", "assign_variant", "evaluate_flag", "is_enabled",
"percentage_bucket",
# MCP server
"AuditLogger", "HttpMCPServer", "MCPContent", "MCPPrompt",
"MCPPromptArgument", "MCPResource", "MCPServer", "MCPTool",
Expand Down
20 changes: 20 additions & 0 deletions je_auto_control/gui/script_builder/command_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -1155,6 +1155,26 @@ def _add_misc_specs(specs: List[CommandSpec]) -> None:
),
description="Validate JSON against a JSON Schema; returns {ok, errors}.",
))
specs.append(CommandSpec(
"AC_evaluate_flag", "Flow", "Feature Flag: Evaluate",
fields=(
FieldSpec("flags", FieldType.STRING,
placeholder='{"flags": {"f": {"variants": {...}}}}'),
FieldSpec("key", FieldType.STRING, placeholder="new-checkout"),
FieldSpec("context", FieldType.STRING, optional=True,
placeholder='{"targeting_key": "user1", "country": "US"}'),
),
description="Evaluate a feature flag; returns {value, variant, reason}.",
))
specs.append(CommandSpec(
"AC_flag_enabled", "Flow", "Feature Flag: Enabled?",
fields=(
FieldSpec("flags", FieldType.STRING),
FieldSpec("key", FieldType.STRING, placeholder="new-checkout"),
FieldSpec("context", FieldType.STRING, optional=True),
),
description="Boolean feature-flag check; returns {enabled}.",
))
specs.append(CommandSpec(
"AC_unified_diff", "Data", "Text: Unified Diff",
fields=(
Expand Down
26 changes: 26 additions & 0 deletions je_auto_control/utils/executor/action_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2928,6 +2928,30 @@ def _rate_limit(name: str, rate: float = 1.0, capacity: float = 1.0,
"wait": round(bucket.time_until_available(float(n)), 4)}


def _evaluate_flag(flags: Any, key: str, context: Any = None) -> Dict[str, Any]:
"""Adapter: evaluate a feature flag (flags/context dict or JSON string)."""
import json
from je_auto_control.utils.feature_flags import FlagStore, evaluate_flag
if isinstance(flags, str):
flags = json.loads(flags)
if isinstance(context, str):
context = json.loads(context)
return evaluate_flag(FlagStore.from_dict(flags), key, context or {})


def _flag_enabled(flags: Any, key: str, context: Any = None,
default: bool = False) -> Dict[str, Any]:
"""Adapter: boolean feature-flag check."""
import json
from je_auto_control.utils.feature_flags import FlagStore, is_enabled
if isinstance(flags, str):
flags = json.loads(flags)
if isinstance(context, str):
context = json.loads(context)
store = FlagStore.from_dict(flags)
return {"enabled": is_enabled(store, key, context or {}, bool(default))}


def _unified_diff(a: str, b: str) -> Dict[str, Any]:
"""Adapter: unified diff transforming text a into b."""
from je_auto_control.utils.text_diff import unified_diff
Expand Down Expand Up @@ -3854,6 +3878,8 @@ def __init__(self):
"AC_ab_significance": _ab_significance,
"AC_rrule_occurrences": _rrule_occurrences,
"AC_rrule_next": _rrule_next,
"AC_evaluate_flag": _evaluate_flag,
"AC_flag_enabled": _flag_enabled,
"AC_unified_diff": _unified_diff,
"AC_apply_unified": _apply_unified,
"AC_three_way_merge": _three_way_merge,
Expand Down
10 changes: 10 additions & 0 deletions je_auto_control/utils/feature_flags/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""Feature-flag evaluation with targeting rules and deterministic rollout."""
from je_auto_control.utils.feature_flags.feature_flags import (
Flag, FlagStore, assign_variant, evaluate_flag, is_enabled,
percentage_bucket,
)

__all__ = [
"Flag", "FlagStore", "assign_variant", "evaluate_flag", "is_enabled",
"percentage_bucket",
]
Loading
Loading