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
8 changes: 8 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-19) — Checkpoint & Resume](#whats-new-2026-06-19--checkpoint--resume)
- [What's new (2026-06-19) — i18n / l10n Testing](#whats-new-2026-06-19--i18n--l10n-testing)
- [What's new (2026-06-19) — Data Quality](#whats-new-2026-06-19--data-quality)
- [What's new (2026-06-19) — SBOM & Suite Sharding](#whats-new-2026-06-19--sbom--suite-sharding)
Expand Down Expand Up @@ -73,6 +74,13 @@

---

## What's new (2026-06-19) — Checkpoint & Resume

Durable execution for long flows + a `py.typed` marker, full stack. Full reference: [`docs/source/Eng/doc/new_features/v21_features_doc.rst`](docs/source/Eng/doc/new_features/v21_features_doc.rst).

- **Flow checkpoint & resume** — `run_resumable(actions, run_id=..., store=...)` / `CheckpointStore` (`AC_run_resumable` / `AC_checkpoint_status` / `AC_checkpoint_clear`, `ac_*`): persist step-index + variables after each step; on re-run with the same `run_id`, fast-forward past completed steps and rehydrate variables — a flow that crashes at step 400 resumes at 400, not 0. Pluggable (SQLite default), cleared on completion.
- **`py.typed` marker** — ships the PEP 561 marker so Mypy/Pyright/Pylance honor AutoControl's inline type hints in downstream code (the repo's typed API was previously invisible to type checkers).

## What's new (2026-06-19) — i18n / l10n Testing

Three pure-stdlib internationalization/localization testing helpers that compound, full stack. Full reference: [`docs/source/Eng/doc/new_features/v20_features_doc.rst`](docs/source/Eng/doc/new_features/v20_features_doc.rst).
Expand Down
8 changes: 8 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-19) — 检查点与续跑](#本次更新-2026-06-19--检查点与续跑)
- [本次更新 (2026-06-19) — i18n / l10n 测试](#本次更新-2026-06-19--i18n--l10n-测试)
- [本次更新 (2026-06-19) — 数据质量](#本次更新-2026-06-19--数据质量)
- [本次更新 (2026-06-19) — SBOM 与测试分片](#本次更新-2026-06-19--sbom-与测试分片)
Expand Down Expand Up @@ -72,6 +73,13 @@

---

## 本次更新 (2026-06-19) — 检查点与续跑

长流程的耐久执行 + `py.typed` 标记,走完整五层。完整参考:[`docs/source/Zh/doc/new_features/v21_features_doc.rst`](../docs/source/Zh/doc/new_features/v21_features_doc.rst)。

- **流程检查点与续跑** — `run_resumable(actions, run_id=..., store=...)` / `CheckpointStore`(`AC_run_resumable` / `AC_checkpoint_status` / `AC_checkpoint_clear`、`ac_*`):每步后持久化 step-index + 变量;以相同 `run_id` 再执行时快进略过已完成步骤并还原变量——在第 400 步崩溃的流程会从 400 续跑,而非从 0。可抽换(默认 SQLite),完成后清除。
- **`py.typed` 标记** — 附带 PEP 561 标记,让 Mypy/Pyright/Pylance 在下游代码采用 AutoControl 的内嵌类型注解(此前类型化 API 对类型检查器是隐形的)。

## 本次更新 (2026-06-19) — i18n / l10n 测试

三项可互相搭配的纯标准库国际化/本地化测试辅助工具,走完整五层。完整参考:[`docs/source/Zh/doc/new_features/v20_features_doc.rst`](../docs/source/Zh/doc/new_features/v20_features_doc.rst)。
Expand Down
8 changes: 8 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-19) — 檢查點與續跑](#本次更新-2026-06-19--檢查點與續跑)
- [本次更新 (2026-06-19) — i18n / l10n 測試](#本次更新-2026-06-19--i18n--l10n-測試)
- [本次更新 (2026-06-19) — 資料品質](#本次更新-2026-06-19--資料品質)
- [本次更新 (2026-06-19) — SBOM 與測試分片](#本次更新-2026-06-19--sbom-與測試分片)
Expand Down Expand Up @@ -72,6 +73,13 @@

---

## 本次更新 (2026-06-19) — 檢查點與續跑

長流程的耐久執行 + `py.typed` 標記,走完整五層。完整參考:[`docs/source/Zh/doc/new_features/v21_features_doc.rst`](../docs/source/Zh/doc/new_features/v21_features_doc.rst)。

- **流程檢查點與續跑** — `run_resumable(actions, run_id=..., store=...)` / `CheckpointStore`(`AC_run_resumable` / `AC_checkpoint_status` / `AC_checkpoint_clear`、`ac_*`):每步後持久化 step-index + 變數;以相同 `run_id` 再執行時快轉略過已完成步驟並還原變數——在第 400 步當掉的流程會從 400 續跑,而非從 0。可抽換(預設 SQLite),完成後清除。
- **`py.typed` 標記** — 附帶 PEP 561 標記,讓 Mypy/Pyright/Pylance 在下游程式碼採用 AutoControl 的內嵌型別註記(此前型別化 API 對型別檢查器是隱形的)。

## 本次更新 (2026-06-19) — i18n / l10n 測試

三項可互相搭配的純標準庫國際化/在地化測試輔助工具,走完整五層。完整參考:[`docs/source/Zh/doc/new_features/v20_features_doc.rst`](../docs/source/Zh/doc/new_features/v20_features_doc.rst)。
Expand Down
51 changes: 51 additions & 0 deletions docs/source/Eng/doc/new_features/v21_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
==================================================
New Features (2026-06-19) — Checkpoint & Resume
==================================================

Durable execution for long action lists, plus a ``py.typed`` marker so the
package's inline type hints are honored by type checkers. Pure standard
library; wired through the full stack (facade, ``AC_*`` executor commands,
MCP tools, Script Builder).

.. contents::
:local:
:depth: 2


Flow checkpoint & resume
=======================

A multi-hour unattended flow that dies at step 400 should not restart from
zero. :func:`run_resumable` persists ``{run_id, step_index, variables}``
after each executed step to a pluggable store; on a later run with the same
``run_id`` it fast-forwards past completed steps and rehydrates the script
variables::

from je_auto_control import run_resumable, CheckpointStore

store = CheckpointStore("runs.db")
result = run_resumable(actions, run_id="nightly-invoices", store=store)
result["resumed_from"] # 0 on a fresh run, N when resuming after a crash

On normal completion the checkpoint is cleared. The store is injectable, so
resume is unit-tested deterministically without a real crash:
``CheckpointStore.save`` / ``load`` / ``clear``.

Executor / MCP commands:

* ``AC_run_resumable`` — run ``actions`` with checkpoint/resume keyed by
``run_id`` (persisted to ``db``).
* ``AC_checkpoint_status`` — the saved checkpoint for a run (or null).
* ``AC_checkpoint_clear`` — delete a run's checkpoint.

(and the matching ``ac_run_resumable`` / ``ac_checkpoint_status`` /
``ac_checkpoint_clear`` MCP tools).


``py.typed`` marker
==================

The package now ships a PEP 561 ``py.typed`` marker, so Mypy / Pyright /
Pylance honor AutoControl's inline type annotations in downstream code —
completing the value of the typed public API. No code change for callers;
just better editor autocompletion and type checking out of the box.
1 change: 1 addition & 0 deletions docs/source/Eng/eng_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Comprehensive guides for all AutoControl features.
doc/new_features/v18_features_doc
doc/new_features/v19_features_doc
doc/new_features/v20_features_doc
doc/new_features/v21_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
47 changes: 47 additions & 0 deletions docs/source/Zh/doc/new_features/v21_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
==========================================
新功能 (2026-06-19) — 檢查點與續跑
==========================================

長動作清單的耐久執行(durable execution),並新增 ``py.typed`` 標記讓
型別檢查器採用套件的內嵌型別註記。純標準庫;走完整五層(facade、
``AC_*`` 執行器指令、MCP 工具、Script Builder)。

.. contents::
:local:
:depth: 2


流程檢查點與續跑
================

跑了數小時、卻在第 400 步當掉的無人值守流程,不該從頭重來。
:func:`run_resumable` 在每執行完一步後,把 ``{run_id, step_index,
variables}`` 存入可抽換的儲存後端;之後以相同 ``run_id`` 再執行時,會
快轉略過已完成的步驟並還原腳本變數::

from je_auto_control import run_resumable, CheckpointStore

store = CheckpointStore("runs.db")
result = run_resumable(actions, run_id="nightly-invoices", store=store)
result["resumed_from"] # 全新執行為 0;當機後續跑則為 N

正常完成後檢查點會被清除。儲存後端可注入,因此續跑邏輯可在不真的當機的
情況下做決定性單元測試:``CheckpointStore.save`` / ``load`` / ``clear``。

執行器 / MCP 指令:

* ``AC_run_resumable`` — 以 ``run_id`` 為鍵,帶檢查點/續跑執行 ``actions``
(存到 ``db``)。
* ``AC_checkpoint_status`` — 某次執行已存的檢查點(或 null)。
* ``AC_checkpoint_clear`` — 刪除某次執行的檢查點。

(以及對應的 ``ac_run_resumable`` / ``ac_checkpoint_status`` /
``ac_checkpoint_clear`` MCP 工具)。


``py.typed`` 標記
=================

套件現在附帶 PEP 561 的 ``py.typed`` 標記,讓 Mypy / Pyright / Pylance
在下游程式碼中採用 AutoControl 的內嵌型別註記——讓型別化的公開 API 真正
發揮價值。呼叫端無需改動;開箱即享更好的編輯器自動完成與型別檢查。
1 change: 1 addition & 0 deletions docs/source/Zh/zh_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ AutoControl 所有功能的完整使用指南。
doc/new_features/v18_features_doc
doc/new_features/v19_features_doc
doc/new_features/v20_features_doc
doc/new_features/v21_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 @@ -161,6 +161,10 @@
from je_auto_control.utils.i18n_test import (
check_catalog, check_overflow, pseudo_localize, pseudo_localize_catalog,
)
# Flow checkpoint & resume (durable execution for long action lists)
from je_auto_control.utils.checkpoint import (
Checkpoint, CheckpointStore, run_resumable,
)
# Background popup/interrupt watchdog (unattended automation)
from je_auto_control.utils.watchdog import (
PopupWatchdog, WatchdogRule, default_popup_watchdog,
Expand Down Expand Up @@ -583,6 +587,7 @@ def start_autocontrol_gui(*args, **kwargs):
"extract_fields", "mask_rows", "validate_rows",
"check_catalog", "check_overflow", "pseudo_localize",
"pseudo_localize_catalog",
"Checkpoint", "CheckpointStore", "run_resumable",
# MCP server
"AuditLogger", "HttpMCPServer", "MCPContent", "MCPPrompt",
"MCPPromptArgument", "MCPResource", "MCPServer", "MCPTool",
Expand Down
22 changes: 22 additions & 0 deletions je_auto_control/gui/script_builder/command_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,28 @@ def _add_misc_specs(specs: List[CommandSpec]) -> None:
_add_memory_specs(specs)
_add_data_quality_specs(specs)
_add_i18n_specs(specs)
_add_checkpoint_specs(specs)


def _add_checkpoint_specs(specs: List[CommandSpec]) -> None:
run_id = FieldSpec("run_id", FieldType.STRING)
db = FieldSpec("db", FieldType.FILE_PATH)
specs.append(CommandSpec(
"AC_run_resumable", "Flow", "Run Resumable (checkpoint)",
fields=(run_id, db),
description="Run 'actions' (JSON view) with checkpoint/resume keyed "
"by run_id; resumes past completed steps after a crash.",
))
specs.append(CommandSpec(
"AC_checkpoint_status", "Flow", "Checkpoint: Status",
fields=(run_id, db),
description="Return the saved checkpoint for a run (step + variables).",
))
specs.append(CommandSpec(
"AC_checkpoint_clear", "Flow", "Checkpoint: Clear",
fields=(run_id, db),
description="Delete a run's checkpoint.",
))
specs.append(CommandSpec(
"AC_wcag_audit", "Accessibility", "WCAG 2.2 Conformance Audit",
fields=(
Expand Down
1 change: 1 addition & 0 deletions je_auto_control/py.typed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# PEP 561 marker: this package ships inline type information.
6 changes: 6 additions & 0 deletions je_auto_control/utils/checkpoint/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Flow checkpoint & resume — durable execution for long action lists."""
from je_auto_control.utils.checkpoint.checkpoint import (
Checkpoint, CheckpointStore, run_resumable,
)

__all__ = ["Checkpoint", "CheckpointStore", "run_resumable"]
109 changes: 109 additions & 0 deletions je_auto_control/utils/checkpoint/checkpoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""Flow checkpoint & resume — durable execution for long action lists.

A multi-hour unattended flow that dies at step 400 should not restart from
zero. After each executed step this persists ``{run_id, step_index,
variables}`` to a pluggable store (SQLite by default); on a later run with
the same ``run_id`` it fast-forwards past completed steps and rehydrates the
script variables, so execution resumes where it stopped.

Pure standard library (``sqlite3`` / ``json``); imports no ``PySide6``. The
store is injectable, so resume logic is unit-tested deterministically
without a real crash.
"""
import json
import sqlite3
import time
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional


@dataclass
class Checkpoint:
"""A persisted run position: next step to execute + variable snapshot."""
run_id: str
step_index: int
variables: Dict[str, Any] = field(default_factory=dict)
updated: float = 0.0


class CheckpointStore:
"""SQLite-backed store of one checkpoint per ``run_id``."""

def __init__(self, db_path: str) -> None:
self._db_path = db_path
self._ensure_schema()

def _connect(self) -> sqlite3.Connection:
conn = sqlite3.connect(self._db_path, timeout=30.0,
isolation_level=None)
conn.row_factory = sqlite3.Row
return conn

def _ensure_schema(self) -> None:
with self._connect() as conn:
conn.execute(
"CREATE TABLE IF NOT EXISTS checkpoints ("
"run_id TEXT PRIMARY KEY, step_index INTEGER NOT NULL, "
"variables TEXT NOT NULL, updated REAL NOT NULL)")

def save(self, run_id: str, step_index: int,
variables: Dict[str, Any]) -> None:
"""Persist (or overwrite) the checkpoint for ``run_id``."""
with self._connect() as conn:
conn.execute(
"INSERT INTO checkpoints (run_id, step_index, variables, "
"updated) VALUES (?, ?, ?, ?) ON CONFLICT(run_id) DO UPDATE "
"SET step_index=excluded.step_index, "
"variables=excluded.variables, updated=excluded.updated",
(str(run_id), int(step_index), json.dumps(variables),
time.time()))

def load(self, run_id: str) -> Optional[Checkpoint]:
"""Return the checkpoint for ``run_id`` or ``None``."""
with self._connect() as conn:
row = conn.execute(
"SELECT * FROM checkpoints WHERE run_id=?",
(str(run_id),)).fetchone()
if row is None:
return None
return Checkpoint(run_id=row["run_id"], step_index=row["step_index"],
variables=json.loads(row["variables"]),
updated=row["updated"])

def clear(self, run_id: str) -> bool:
"""Delete the checkpoint for ``run_id``; return whether it existed."""
with self._connect() as conn:
cur = conn.execute("DELETE FROM checkpoints WHERE run_id=?",
(str(run_id),))
return cur.rowcount > 0


def _new_executor() -> Any:
from je_auto_control.utils.executor.action_executor import Executor
return Executor()


def run_resumable(actions: List[Any], *, run_id: str, store: CheckpointStore,
executor: Any = None,
variables: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""Run ``actions``, checkpointing after each step; resume if interrupted.

On entry, any saved checkpoint for ``run_id`` fast-forwards past
completed steps and rehydrates variables. On normal completion the
checkpoint is cleared. Returns ``{completed, total, resumed_from,
record}``.
"""
runner = executor or _new_executor()
existing = store.load(run_id)
start = existing.step_index if existing else 0
if existing:
runner.variables.update_many(existing.variables)
elif variables:
runner.variables.update_many(variables)
record: Dict[str, Any] = {}
for index in range(start, len(actions)):
record.update(runner.execute_action([actions[index]]))
store.save(run_id, index + 1, runner.variables.as_dict())
store.clear(run_id)
return {"completed": True, "total": len(actions),
"resumed_from": start, "record": record}
29 changes: 29 additions & 0 deletions je_auto_control/utils/executor/action_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2717,6 +2717,32 @@ def _check_catalog(base: Dict[str, Any],
return check_catalog(base, target)


def _run_resumable(actions: List[Any], run_id: str, db: str,
variables: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""Adapter: run actions with checkpoint/resume keyed by run_id."""
from je_auto_control.utils.checkpoint import CheckpointStore, run_resumable
return run_resumable(actions, run_id=run_id,
store=CheckpointStore(db), variables=variables)


def _checkpoint_status(run_id: str, db: str) -> Dict[str, Any]:
"""Adapter: return the saved checkpoint for a run (or null)."""
from je_auto_control.utils.checkpoint import CheckpointStore
checkpoint = CheckpointStore(db).load(run_id)
if checkpoint is None:
return {"checkpoint": None}
return {"checkpoint": {"run_id": checkpoint.run_id,
"step_index": checkpoint.step_index,
"variables": checkpoint.variables}}


def _checkpoint_clear(run_id: str, db: str) -> Dict[str, Any]:
"""Adapter: delete a run's checkpoint."""
from je_auto_control.utils.checkpoint import CheckpointStore
return {"cleared": CheckpointStore(db).clear(run_id)}


class Executor:
"""
Executor
Expand Down Expand Up @@ -2924,6 +2950,9 @@ def __init__(self):
"AC_pseudo_localize": _pseudo_localize,
"AC_check_overflow": _check_overflow,
"AC_check_catalog": _check_catalog,
"AC_run_resumable": _run_resumable,
"AC_checkpoint_status": _checkpoint_status,
"AC_checkpoint_clear": _checkpoint_clear,
"AC_a11y_record_start": _a11y_record_start,
"AC_a11y_record_stop": _a11y_record_stop,
"AC_a11y_record_events": _a11y_record_events,
Expand Down
Loading
Loading