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) — 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)
- [What's new (2026-06-21) — Full-Text Search (BM25)](#whats-new-2026-06-21--full-text-search-bm25)
- [What's new (2026-06-21) — JSON Pointer, Patch & Merge Patch](#whats-new-2026-06-21--json-pointer-patch--merge-patch)
Expand Down Expand Up @@ -118,6 +119,12 @@

---

## What's new (2026-06-21) — Calendar Recurrence Rules (RRULE)

Schedule "every 2nd Tuesday". Full reference: [`docs/source/Eng/doc/new_features/v66_features_doc.rst`](docs/source/Eng/doc/new_features/v66_features_doc.rst).

- **`parse_rrule` / `occurrences` / `next_occurrence`** (`AC_rrule_occurrences`, `AC_rrule_next`): the scheduler's cron is 5-field interval-only — it can't express "every 2nd Tuesday", "the last weekday of the month", or "every weekday for 10 occurrences". This adds an RFC 5545 (iCalendar) RRULE parser + occurrence expander supporting `FREQ`/`INTERVAL`/`COUNT`/`UNTIL`/`BYDAY` (with ordinals like `2MO`/`-1FR`)/`BYMONTHDAY`/`BYMONTH`/`BYSETPOS`/`WKST`. Pure-stdlib `datetime`+`calendar`, injectable clock for deterministic `next_occurrence`.

## What's new (2026-06-21) — Statistics & A/B Significance

Decide whether a difference is real. Full reference: [`docs/source/Eng/doc/new_features/v65_features_doc.rst`](docs/source/Eng/doc/new_features/v65_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) — 日历周期规则(RRULE)](#本次更新-2026-06-21--日历周期规则rrule)
- [本次更新 (2026-06-21) — 统计与 A/B 显著性](#本次更新-2026-06-21--统计与-ab-显著性)
- [本次更新 (2026-06-21) — 全文搜索(BM25)](#本次更新-2026-06-21--全文搜索bm25)
- [本次更新 (2026-06-21) — JSON Pointer、Patch 与 Merge Patch](#本次更新-2026-06-21--json-pointerpatch-与-merge-patch)
Expand Down Expand Up @@ -117,6 +118,12 @@

---

## 本次更新 (2026-06-21) — 日历周期规则(RRULE)

排程「每月第 2 个星期二」。完整参考:[`docs/source/Zh/doc/new_features/v66_features_doc.rst`](../docs/source/Zh/doc/new_features/v66_features_doc.rst)。

- **`parse_rrule` / `occurrences` / `next_occurrence`**(`AC_rrule_occurrences`、`AC_rrule_next`):排程器的 cron 只是 5 字段间隔式 —— 无法表达「每月第 2 个星期二」、「每月最后一个工作日」或「连续 10 次的每个工作日」。本功能补上 RFC 5545(iCalendar)RRULE 解析器 + 发生时刻展开器,支持 `FREQ`/`INTERVAL`/`COUNT`/`UNTIL`/`BYDAY`(含序数如 `2MO`/`-1FR`)/`BYMONTHDAY`/`BYMONTH`/`BYSETPOS`/`WKST`。纯标准库 `datetime`+`calendar`,时钟可注入使 `next_occurrence` 确定。

## 本次更新 (2026-06-21) — 统计与 A/B 显著性

判断差异是否为真。完整参考:[`docs/source/Zh/doc/new_features/v65_features_doc.rst`](../docs/source/Zh/doc/new_features/v65_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) — 行事曆週期規則(RRULE)](#本次更新-2026-06-21--行事曆週期規則rrule)
- [本次更新 (2026-06-21) — 統計與 A/B 顯著性](#本次更新-2026-06-21--統計與-ab-顯著性)
- [本次更新 (2026-06-21) — 全文搜尋(BM25)](#本次更新-2026-06-21--全文搜尋bm25)
- [本次更新 (2026-06-21) — JSON Pointer、Patch 與 Merge Patch](#本次更新-2026-06-21--json-pointerpatch-與-merge-patch)
Expand Down Expand Up @@ -117,6 +118,12 @@

---

## 本次更新 (2026-06-21) — 行事曆週期規則(RRULE)

排程「每月第 2 個星期二」。完整參考:[`docs/source/Zh/doc/new_features/v66_features_doc.rst`](../docs/source/Zh/doc/new_features/v66_features_doc.rst)。

- **`parse_rrule` / `occurrences` / `next_occurrence`**(`AC_rrule_occurrences`、`AC_rrule_next`):排程器的 cron 只是 5 欄位間隔式 —— 無法表達「每月第 2 個星期二」、「每月最後一個工作日」或「連續 10 次的每個工作日」。本功能補上 RFC 5545(iCalendar)RRULE 解析器 + 發生時刻展開器,支援 `FREQ`/`INTERVAL`/`COUNT`/`UNTIL`/`BYDAY`(含序數如 `2MO`/`-1FR`)/`BYMONTHDAY`/`BYMONTH`/`BYSETPOS`/`WKST`。純標準函式庫 `datetime`+`calendar`,時鐘可注入使 `next_occurrence` 具決定性。

## 本次更新 (2026-06-21) — 統計與 A/B 顯著性

判斷差異是否為真。完整參考:[`docs/source/Zh/doc/new_features/v65_features_doc.rst`](../docs/source/Zh/doc/new_features/v65_features_doc.rst)。
Expand Down
50 changes: 50 additions & 0 deletions docs/source/Eng/doc/new_features/v66_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
Calendar Recurrence Rules (RRULE)
=================================

The scheduler's cron is interval-style 5-field only — it cannot express "every
2nd Tuesday", "the last weekday of the month", or "every weekday for 10
occurrences". This adds an RFC 5545 (iCalendar) **RRULE** parser and occurrence
expander, the calendar layer above cron.

Supported rule parts: ``FREQ`` (DAILY/WEEKLY/MONTHLY/YEARLY), ``INTERVAL``,
``COUNT``, ``UNTIL``, ``BYDAY`` (incl. ordinals like ``2MO`` / ``-1FR``),
``BYMONTHDAY`` (incl. negatives), ``BYMONTH``, ``BYSETPOS`` and ``WKST``.
Time-level parts and BYWEEKNO/BYYEARDAY are out of scope. Pure standard library
(``datetime`` + ``calendar``); the clock is injectable so ``next_occurrence`` is
deterministic. Imports no ``PySide6``.

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

.. code-block:: python

import datetime
from je_auto_control import parse_rrule, occurrences, next_occurrence

rule = parse_rrule("FREQ=MONTHLY;BYDAY=2TU") # every 2nd Tuesday
start = datetime.datetime(2026, 1, 1, 9, 0)

for moment in occurrences(rule, start, count=3):
print(moment) # 2026-01-13 09:00, 2026-02-10 09:00, ...

# "last weekday of the month"
last = parse_rrule("FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1")

# next fire at or after a given time (inject now for determinism)
nxt = next_occurrence(rule, start, now=datetime.datetime(2026, 3, 15))

``parse_rrule`` accepts the rule with or without the ``RRULE:`` prefix and
returns a frozen ``Recurrence``. ``occurrences`` yields datetimes anchored at
``dtstart`` (its time-of-day and timezone are applied to every occurrence),
bounded by ``COUNT`` / ``UNTIL`` (or the ``count=`` / ``until=`` overrides) and
a safety cap. A date-only ``UNTIL`` bounds the whole day inclusively.
``next_occurrence`` returns the first occurrence at or after ``now``.

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

``AC_rrule_occurrences`` takes ``rule`` and an ISO ``dtstart`` (plus optional
``count``) and returns ``{occurrences}`` as ISO datetimes. ``AC_rrule_next``
takes ``rule`` / ``dtstart`` / optional ``now`` and returns ``{next}``. Both are
exposed as MCP tools (``ac_rrule_occurrences`` / ``ac_rrule_next``) 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 @@ -88,6 +88,7 @@ Comprehensive guides for all AutoControl features.
doc/new_features/v63_features_doc
doc/new_features/v64_features_doc
doc/new_features/v65_features_doc
doc/new_features/v66_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
44 changes: 44 additions & 0 deletions docs/source/Zh/doc/new_features/v66_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
行事曆週期規則(RRULE)
=====================

排程器的 cron 只是間隔式的 5 欄位 —— 無法表達「每月第 2 個星期二」、「每月最後一個工作日」或
「連續 10 次的每個工作日」。本功能補上 RFC 5545(iCalendar)**RRULE** 解析器與發生時刻展開器,
即 cron 之上的行事曆層。

支援的規則部分:``FREQ``(DAILY/WEEKLY/MONTHLY/YEARLY)、``INTERVAL``、``COUNT``、``UNTIL``、
``BYDAY``(含序數如 ``2MO`` / ``-1FR``)、``BYMONTHDAY``(含負數)、``BYMONTH``、``BYSETPOS``
與 ``WKST``。時間層級部分以及 BYWEEKNO/BYYEARDAY 不在範圍內。純標準函式庫(``datetime`` +
``calendar``);時鐘可注入,因此 ``next_occurrence`` 具決定性。不匯入 ``PySide6``。

無頭 API
--------

.. code-block:: python

import datetime
from je_auto_control import parse_rrule, occurrences, next_occurrence

rule = parse_rrule("FREQ=MONTHLY;BYDAY=2TU") # 每月第 2 個星期二
start = datetime.datetime(2026, 1, 1, 9, 0)

for moment in occurrences(rule, start, count=3):
print(moment) # 2026-01-13 09:00、2026-02-10 09:00、...

# 「每月最後一個工作日」
last = parse_rrule("FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1")

# 在指定時間當下或之後的下一次(注入 now 以取得決定性)
nxt = next_occurrence(rule, start, now=datetime.datetime(2026, 3, 15))

``parse_rrule`` 接受帶或不帶 ``RRULE:`` 前綴的規則,回傳凍結的 ``Recurrence``。``occurrences``
產生以 ``dtstart`` 為錨點的 datetime(其時刻與時區會套用到每一次發生),受 ``COUNT`` / ``UNTIL``
(或 ``count=`` / ``until=`` 覆寫)及安全上限約束。僅含日期的 ``UNTIL`` 會包含整天。
``next_occurrence`` 回傳在 ``now`` 當下或之後的第一次發生。

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

``AC_rrule_occurrences`` 接受 ``rule`` 與 ISO ``dtstart``(及選用的 ``count``),回傳
``{occurrences}`` 為 ISO datetime。``AC_rrule_next`` 接受 ``rule`` / ``dtstart`` / 選用的
``now``,回傳 ``{next}``。兩者皆以 MCP 工具(``ac_rrule_occurrences`` / ``ac_rrule_next``)以及
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 @@ -88,6 +88,7 @@ AutoControl 所有功能的完整使用指南。
doc/new_features/v63_features_doc
doc/new_features/v64_features_doc
doc/new_features/v65_features_doc
doc/new_features/v66_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 @@ -338,6 +338,10 @@
chi_square_2x2, cohens_d, describe, normal_cdf, percentile,
two_proportion_z_test, welch_t_test,
)
# RFC 5545 recurrence-rule expansion (calendar scheduling above cron)
from je_auto_control.utils.recurrence import (
Recurrence, next_occurrence, occurrences, parse_rrule,
)
# Background popup/interrupt watchdog (unattended automation)
from je_auto_control.utils.watchdog import (
PopupWatchdog, WatchdogRule, default_popup_watchdog,
Expand Down Expand Up @@ -823,6 +827,7 @@ def start_autocontrol_gui(*args, **kwargs):
"SearchHit", "SearchIndex", "search_documents", "tokenize",
"chi_square_2x2", "cohens_d", "describe", "normal_cdf", "percentile",
"two_proportion_z_test", "welch_t_test",
"Recurrence", "next_occurrence", "occurrences", "parse_rrule",
# MCP server
"AuditLogger", "HttpMCPServer", "MCPContent", "MCPPrompt",
"MCPPromptArgument", "MCPResource", "MCPServer", "MCPTool",
Expand Down
23 changes: 23 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,29 @@ def _add_misc_specs(specs: List[CommandSpec]) -> None:
),
description="Validate JSON against a JSON Schema; returns {ok, errors}.",
))
specs.append(CommandSpec(
"AC_rrule_occurrences", "Flow", "Recurrence: Expand (RRULE)",
fields=(
FieldSpec("rule", FieldType.STRING,
placeholder="FREQ=MONTHLY;BYDAY=2TU"),
FieldSpec("dtstart", FieldType.STRING,
placeholder="2026-01-01T09:00:00"),
FieldSpec("count", FieldType.INT, optional=True, default=10),
),
description="Expand an RFC 5545 RRULE into ISO datetimes.",
))
specs.append(CommandSpec(
"AC_rrule_next", "Flow", "Recurrence: Next Occurrence",
fields=(
FieldSpec("rule", FieldType.STRING,
placeholder="FREQ=WEEKLY;BYDAY=MO,WE,FR"),
FieldSpec("dtstart", FieldType.STRING,
placeholder="2026-01-01T09:00:00"),
FieldSpec("now", FieldType.STRING, optional=True,
placeholder="2026-03-15T00:00:00"),
),
description="Next RRULE occurrence at/after now; returns {next}.",
))
specs.append(CommandSpec(
"AC_describe_stats", "Data", "Describe Statistics",
fields=(
Expand Down
23 changes: 23 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,27 @@ def _rate_limit(name: str, rate: float = 1.0, capacity: float = 1.0,
"wait": round(bucket.time_until_available(float(n)), 4)}


def _rrule_occurrences(rule: str, dtstart: str,
count: int = 10) -> Dict[str, Any]:
"""Adapter: expand an RRULE from an ISO dtstart into ISO datetimes."""
import datetime as _dt
from je_auto_control.utils.recurrence import occurrences, parse_rrule
start = _dt.datetime.fromisoformat(dtstart)
moments = occurrences(parse_rrule(rule), start, count=int(count))
return {"occurrences": [moment.isoformat() for moment in moments]}


def _rrule_next(rule: str, dtstart: str,
now: Optional[str] = None) -> Dict[str, Any]:
"""Adapter: next RRULE occurrence at/after now (ISO in, ISO out)."""
import datetime as _dt
from je_auto_control.utils.recurrence import next_occurrence, parse_rrule
start = _dt.datetime.fromisoformat(dtstart)
when = _dt.datetime.fromisoformat(now) if now else None
moment = next_occurrence(parse_rrule(rule), start, now=when)
return {"next": moment.isoformat() if moment else None}


def _describe_stats(values: Any) -> Dict[str, Any]:
"""Adapter: summary statistics + percentiles of a numeric list (or JSON)."""
import json
Expand Down Expand Up @@ -3811,6 +3832,8 @@ def __init__(self):
"AC_search_documents": _search_documents,
"AC_describe_stats": _describe_stats,
"AC_ab_significance": _ab_significance,
"AC_rrule_occurrences": _rrule_occurrences,
"AC_rrule_next": _rrule_next,
"AC_resolve_pointer": _resolve_pointer,
"AC_apply_json_patch": _apply_json_patch,
"AC_make_json_patch": _make_json_patch,
Expand Down
31 changes: 30 additions & 1 deletion je_auto_control/utils/mcp_server/tools/_factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -3343,6 +3343,35 @@ def rate_limit_tools() -> List[MCPTool]:
]


def recurrence_tools() -> List[MCPTool]:
return [
MCPTool(
name="ac_rrule_occurrences",
description=("Expand an RFC 5545 'rule' (RRULE) from an ISO "
"'dtstart' into the next 'count' ISO datetimes. "
"Returns {occurrences}."),
input_schema=schema(
{"rule": {"type": "string"}, "dtstart": {"type": "string"},
"count": {"type": "integer"}},
["rule", "dtstart"]),
handler=h.rrule_occurrences,
annotations=READ_ONLY,
),
MCPTool(
name="ac_rrule_next",
description=("Next occurrence of an RRULE 'rule' at/after 'now' "
"(ISO; defaults to current time), anchored at ISO "
"'dtstart'. Returns {next}."),
input_schema=schema(
{"rule": {"type": "string"}, "dtstart": {"type": "string"},
"now": {"type": "string"}},
["rule", "dtstart"]),
handler=h.rrule_next,
annotations=READ_ONLY,
),
]


def stats_tools() -> List[MCPTool]:
return [
MCPTool(
Expand Down Expand Up @@ -4627,7 +4656,7 @@ def media_assert_tools() -> List[MCPTool]:
process_mining_tools, asset_tools, events_tools, notify_channel_tools,
jsonpath_tools, json_schema_tools, vuln_scan_tools, vex_tools,
license_policy_tools, jwt_tools, rate_limit_tools, json_patch_tools,
search_index_tools, stats_tools,
search_index_tools, stats_tools, recurrence_tools,
saga_tools, decision_table_tools, locator_repair_tools,
pii_text_tools, sarif_tools,
screen_record_tools,
Expand Down
17 changes: 17 additions & 0 deletions je_auto_control/utils/mcp_server/tools/_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1647,6 +1647,23 @@ def ab_significance(a_conv, a_n, b_conv, b_n):
return two_proportion_z_test(int(a_conv), int(a_n), int(b_conv), int(b_n))


def rrule_occurrences(rule, dtstart, count=10):
import datetime as _dt
from je_auto_control.utils.recurrence import occurrences, parse_rrule
start = _dt.datetime.fromisoformat(dtstart)
moments = occurrences(parse_rrule(rule), start, count=int(count))
return {"occurrences": [moment.isoformat() for moment in moments]}


def rrule_next(rule, dtstart, now=None):
import datetime as _dt
from je_auto_control.utils.recurrence import next_occurrence, parse_rrule
start = _dt.datetime.fromisoformat(dtstart)
when = _dt.datetime.fromisoformat(now) if now else None
moment = next_occurrence(parse_rrule(rule), start, now=when)
return {"next": moment.isoformat() if moment else None}


def run_saga(steps):
from je_auto_control.utils.saga import run_saga as _run
result = _run(steps)
Expand Down
6 changes: 6 additions & 0 deletions je_auto_control/utils/recurrence/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""RFC 5545 recurrence-rule parsing and occurrence expansion."""
from je_auto_control.utils.recurrence.recurrence import (
Recurrence, next_occurrence, occurrences, parse_rrule,
)

__all__ = ["Recurrence", "next_occurrence", "occurrences", "parse_rrule"]
Loading
Loading