Skip to content

Commit 96364bd

Browse files
authored
Merge pull request #278 from Integration-Automation/feat/json-contract
Add JSON contract and snapshot matching
2 parents 3184798 + 771f088 commit 96364bd

15 files changed

Lines changed: 449 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) — JSON Contract & Snapshot Matching](#whats-new-2026-06-21--json-contract--snapshot-matching)
1617
- [What's new (2026-06-21) — SLSA Build Provenance](#whats-new-2026-06-21--slsa-build-provenance)
1718
- [What's new (2026-06-21) — Feature Flags](#whats-new-2026-06-21--feature-flags)
1819
- [What's new (2026-06-21) — Text Diff, Patch & Three-Way Merge](#whats-new-2026-06-21--text-diff-patch--three-way-merge)
@@ -122,6 +123,12 @@
122123

123124
---
124125

126+
## What's new (2026-06-21) — JSON Contract & Snapshot Matching
127+
128+
Match, diff and snapshot JSON payloads. Full reference: [`docs/source/Eng/doc/new_features/v70_features_doc.rst`](docs/source/Eng/doc/new_features/v70_features_doc.rst).
129+
130+
- **`match_json` / `diff_json` / `normalize_json` / `snapshot_json`** (`AC_match_json`, `AC_diff_json`): `json_schema` validates against an authored schema and `jsonpath` extracts, but nothing matched two payloads with relaxed rules or diffed them path-by-path. This adds contract/snapshot matching — `partial` (subset), `match_type` (Pact-style `like`), `ignore` volatile paths — returning `{path, kind}` mismatches (`missing`/`extra`/`changed`), plus golden-master `snapshot_json`. Composes with `json_schema` + `json_patch`; pure-stdlib.
131+
125132
## What's new (2026-06-21) — SLSA Build Provenance
126133

127134
Attest what was built. Full reference: [`docs/source/Eng/doc/new_features/v69_features_doc.rst`](docs/source/Eng/doc/new_features/v69_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) — JSON 合约与快照比对](#本次更新-2026-06-21--json-合约与快照比对)
1516
- [本次更新 (2026-06-21) — SLSA 构建来源证明](#本次更新-2026-06-21--slsa-构建来源证明)
1617
- [本次更新 (2026-06-21) — 功能旗标](#本次更新-2026-06-21--功能旗标)
1718
- [本次更新 (2026-06-21) — 文本 Diff、应用与三方合并](#本次更新-2026-06-21--文本-diff应用与三方合并)
@@ -121,6 +122,12 @@
121122

122123
---
123124

125+
## 本次更新 (2026-06-21) — JSON 合约与快照比对
126+
127+
比对、取差异与快照 JSON 内容。完整参考:[`docs/source/Zh/doc/new_features/v70_features_doc.rst`](../docs/source/Zh/doc/new_features/v70_features_doc.rst)
128+
129+
- **`match_json` / `diff_json` / `normalize_json` / `snapshot_json`**(`AC_match_json``AC_diff_json`):`json_schema` 以撰写的 schema 验证、`jsonpath` 提取,但没有任何东西能以宽松规则比对两份内容或逐路径取差异。本功能补上合约/快照比对 —— `partial`(子集)、`match_type`(Pact 风格 `like`)、`ignore` 易变路径 —— 返回 `{path, kind}` 不符(`missing`/`extra`/`changed`),外加 golden-master `snapshot_json`。与 `json_schema` + `json_patch` 互补;纯标准库。
130+
124131
## 本次更新 (2026-06-21) — SLSA 构建来源证明
125132

126133
证明构建产生了什么。完整参考:[`docs/source/Zh/doc/new_features/v69_features_doc.rst`](../docs/source/Zh/doc/new_features/v69_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) — JSON 合約與快照比對](#本次更新-2026-06-21--json-合約與快照比對)
1516
- [本次更新 (2026-06-21) — SLSA 建置來源證明](#本次更新-2026-06-21--slsa-建置來源證明)
1617
- [本次更新 (2026-06-21) — 功能旗標](#本次更新-2026-06-21--功能旗標)
1718
- [本次更新 (2026-06-21) — 文字 Diff、套用與三方合併](#本次更新-2026-06-21--文字-diff套用與三方合併)
@@ -121,6 +122,12 @@
121122

122123
---
123124

125+
## 本次更新 (2026-06-21) — JSON 合約與快照比對
126+
127+
比對、取差異與快照 JSON 內容。完整參考:[`docs/source/Zh/doc/new_features/v70_features_doc.rst`](../docs/source/Zh/doc/new_features/v70_features_doc.rst)
128+
129+
- **`match_json` / `diff_json` / `normalize_json` / `snapshot_json`**(`AC_match_json``AC_diff_json`):`json_schema` 以撰寫的 schema 驗證、`jsonpath` 擷取,但沒有任何東西能以寬鬆規則比對兩份內容或逐路徑取差異。本功能補上合約/快照比對 —— `partial`(子集)、`match_type`(Pact 風格 `like`)、`ignore` 易變路徑 —— 回傳 `{path, kind}` 不符(`missing`/`extra`/`changed`),外加 golden-master `snapshot_json`。與 `json_schema` + `json_patch` 互補;純標準函式庫。
130+
124131
## 本次更新 (2026-06-21) — SLSA 建置來源證明
125132

126133
證明建置產生了什麼。完整參考:[`docs/source/Zh/doc/new_features/v69_features_doc.rst`](../docs/source/Zh/doc/new_features/v69_features_doc.rst)
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
JSON Contract & Snapshot Matching
2+
=================================
3+
4+
``json_schema`` validates a value against an authored schema and ``jsonpath``
5+
extracts values, but nothing matched two JSON *payloads* with relaxed rules
6+
(type-only, partial, ignore volatile paths) or diffed them path-by-path for
7+
contract / snapshot tests. This adds that layer; it composes with
8+
``json_schema`` (shape) and ``json_patch`` (structured edits).
9+
10+
Pure standard library (``json``); deterministic; imports no ``PySide6``.
11+
12+
Headless API
13+
------------
14+
15+
.. code-block:: python
16+
17+
from je_auto_control import match_json, diff_json, snapshot_json
18+
19+
report = match_json(actual, {"id": 1, "name": "Ada"})
20+
if not report.ok:
21+
for m in report.mismatches:
22+
print(m["path"], m["kind"]) # e.g. "$.name" "changed"
23+
24+
# Pact-style "like": values may differ, types must match
25+
match_json(response, template, match_type=True)
26+
# subset match: extra keys in `actual` are allowed
27+
match_json(response, template, partial=True)
28+
# ignore volatile fields
29+
match_json(response, template, ignore=["$.created_at", "$.id"])
30+
31+
diff_json(actual, expected) # [{path, kind, ...}]
32+
snapshot_json(actual, "golden/checkout.json") # write-if-absent, else compare
33+
34+
``match_json`` returns a ``MatchReport(ok, mismatches)`` where each mismatch is
35+
``{path, kind}`` with ``kind`` one of ``missing`` (in expected, absent from
36+
actual), ``extra`` (in actual, absent from expected), or ``changed``. Options:
37+
``partial`` drops ``extra`` mismatches (subset match), ``match_type`` accepts a
38+
``changed`` leaf whose types match (Pact ``like``), and ``ignore`` skips listed
39+
paths. ``diff_json`` is the raw path-tagged diff; ``normalize_json`` returns a
40+
canonical copy (sorted keys, ``drop`` keys removed) for stable comparison;
41+
``snapshot_json`` is golden-master testing (writes the file on first run, then
42+
matches against it). ``true`` stays distinct from ``1``.
43+
44+
Executor commands
45+
-----------------
46+
47+
``AC_match_json`` takes ``actual`` / ``expected`` (objects or JSON strings) plus
48+
optional ``partial`` / ``match_type`` and returns ``{ok, mismatches}``.
49+
``AC_diff_json`` returns ``{diffs}``. Both are exposed as MCP tools
50+
(``ac_match_json`` / ``ac_diff_json``) and as Script Builder commands under
51+
**Data**.

docs/source/Eng/eng_index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ Comprehensive guides for all AutoControl features.
9292
doc/new_features/v67_features_doc
9393
doc/new_features/v68_features_doc
9494
doc/new_features/v69_features_doc
95+
doc/new_features/v70_features_doc
9596
doc/ocr_backends/ocr_backends_doc
9697
doc/observability/observability_doc
9798
doc/operations_layer/operations_layer_doc
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
JSON 合約與快照比對
2+
===================
3+
4+
``json_schema`` 以撰寫的 schema 驗證一個值,``jsonpath`` 擷取值,但沒有任何東西能以寬鬆規則
5+
(僅型別、部分、忽略易變路徑)比對兩份 JSON *內容*,或逐路徑對它們取差異以做合約 / 快照測試。
6+
本功能補上這個層;它與 ``json_schema``(形狀)及 ``json_patch``(結構化編輯)互補。
7+
8+
純標準函式庫(``json``);具決定性;不匯入 ``PySide6``。
9+
10+
無頭 API
11+
--------
12+
13+
.. code-block:: python
14+
15+
from je_auto_control import match_json, diff_json, snapshot_json
16+
17+
report = match_json(actual, {"id": 1, "name": "Ada"})
18+
if not report.ok:
19+
for m in report.mismatches:
20+
print(m["path"], m["kind"]) # 例如 "$.name" "changed"
21+
22+
# Pact 風格的 "like":值可不同,但型別必須相符
23+
match_json(response, template, match_type=True)
24+
# 子集比對:允許 actual 有額外的鍵
25+
match_json(response, template, partial=True)
26+
# 忽略易變欄位
27+
match_json(response, template, ignore=["$.created_at", "$.id"])
28+
29+
diff_json(actual, expected) # [{path, kind, ...}]
30+
snapshot_json(actual, "golden/checkout.json") # 不存在則寫入,否則比對
31+
32+
``match_json`` 回傳 ``MatchReport(ok, mismatches)``,每個不符為 ``{path, kind}``,``kind`` 為
33+
``missing``(在 expected、actual 沒有)、``extra``(在 actual、expected 沒有)或 ``changed`` 之一。
34+
選項:``partial`` 捨棄 ``extra`` 不符(子集比對),``match_type`` 接受型別相符的 ``changed`` 葉
35+
(Pact ``like``),``ignore`` 略過列出的路徑。``diff_json`` 是原始的路徑標記差異;``normalize_json``
36+
回傳正規化副本(鍵排序、移除 ``drop`` 鍵)以利穩定比對;``snapshot_json`` 是 golden-master 測試
37+
(首次執行寫檔,之後比對)。``true`` 與 ``1`` 保持相異。
38+
39+
執行器命令
40+
----------
41+
42+
``AC_match_json`` 接受 ``actual`` / ``expected``(物件或 JSON 字串)及選用的 ``partial`` /
43+
``match_type``,回傳 ``{ok, mismatches}``。``AC_diff_json`` 回傳 ``{diffs}``。兩者皆以 MCP 工具
44+
(``ac_match_json`` / ``ac_diff_json``)以及 Script Builder 中 **Data** 分類下的命令提供。

docs/source/Zh/zh_index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ AutoControl 所有功能的完整使用指南。
9292
doc/new_features/v67_features_doc
9393
doc/new_features/v68_features_doc
9494
doc/new_features/v69_features_doc
95+
doc/new_features/v70_features_doc
9596
doc/ocr_backends/ocr_backends_doc
9697
doc/observability/observability_doc
9798
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
@@ -356,6 +356,10 @@
356356
build_provenance, subject_for, subject_for_bytes, verify_provenance,
357357
write_provenance,
358358
)
359+
# JSON contract / snapshot matching
360+
from je_auto_control.utils.json_contract import (
361+
MatchReport, diff_json, match_json, normalize_json, snapshot_json,
362+
)
359363
# Background popup/interrupt watchdog (unattended automation)
360364
from je_auto_control.utils.watchdog import (
361365
PopupWatchdog, WatchdogRule, default_popup_watchdog,
@@ -848,6 +852,7 @@ def start_autocontrol_gui(*args, **kwargs):
848852
"percentage_bucket",
849853
"build_provenance", "subject_for", "subject_for_bytes",
850854
"verify_provenance", "write_provenance",
855+
"MatchReport", "diff_json", "match_json", "normalize_json", "snapshot_json",
851856
# MCP server
852857
"AuditLogger", "HttpMCPServer", "MCPContent", "MCPPrompt",
853858
"MCPPromptArgument", "MCPResource", "MCPServer", "MCPTool",

je_auto_control/gui/script_builder/command_schema.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1155,6 +1155,27 @@ def _add_misc_specs(specs: List[CommandSpec]) -> None:
11551155
),
11561156
description="Validate JSON against a JSON Schema; returns {ok, errors}.",
11571157
))
1158+
specs.append(CommandSpec(
1159+
"AC_match_json", "Data", "JSON Contract: Match",
1160+
fields=(
1161+
FieldSpec("actual", FieldType.STRING,
1162+
placeholder='{"id": 1, "name": "Ada"}'),
1163+
FieldSpec("expected", FieldType.STRING,
1164+
placeholder='{"id": 1, "name": "Ada"}'),
1165+
FieldSpec("partial", FieldType.BOOL, optional=True, default=False),
1166+
FieldSpec("match_type", FieldType.BOOL, optional=True,
1167+
default=False),
1168+
),
1169+
description="Match JSON against expected (partial/type); {ok, mismatches}.",
1170+
))
1171+
specs.append(CommandSpec(
1172+
"AC_diff_json", "Data", "JSON Contract: Diff",
1173+
fields=(
1174+
FieldSpec("actual", FieldType.STRING, placeholder='[1, 2, 3]'),
1175+
FieldSpec("expected", FieldType.STRING, placeholder='[1, 2]'),
1176+
),
1177+
description="Path-tagged diff between two JSON payloads; {diffs}.",
1178+
))
11581179
specs.append(CommandSpec(
11591180
"AC_evaluate_flag", "Flow", "Feature Flag: Evaluate",
11601181
fields=(

je_auto_control/utils/executor/action_executor.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2928,6 +2928,30 @@ def _rate_limit(name: str, rate: float = 1.0, capacity: float = 1.0,
29282928
"wait": round(bucket.time_until_available(float(n)), 4)}
29292929

29302930

2931+
def _match_json(actual: Any, expected: Any, partial: bool = False,
2932+
match_type: bool = False) -> Dict[str, Any]:
2933+
"""Adapter: match a JSON payload against an expected one (relaxed rules)."""
2934+
import json
2935+
from je_auto_control.utils.json_contract import match_json
2936+
if isinstance(actual, str):
2937+
actual = json.loads(actual)
2938+
if isinstance(expected, str):
2939+
expected = json.loads(expected)
2940+
return match_json(actual, expected, partial=bool(partial),
2941+
match_type=bool(match_type)).to_dict()
2942+
2943+
2944+
def _diff_json(actual: Any, expected: Any) -> Dict[str, Any]:
2945+
"""Adapter: path-tagged diff between two JSON payloads."""
2946+
import json
2947+
from je_auto_control.utils.json_contract import diff_json
2948+
if isinstance(actual, str):
2949+
actual = json.loads(actual)
2950+
if isinstance(expected, str):
2951+
expected = json.loads(expected)
2952+
return {"diffs": diff_json(actual, expected)}
2953+
2954+
29312955
def _build_provenance(paths: Any, builder_id: str = "je_auto_control",
29322956
build_type: str = "https://je-auto-control/buildtype/v1"
29332957
) -> Dict[str, Any]:
@@ -3907,6 +3931,8 @@ def __init__(self):
39073931
"AC_flag_enabled": _flag_enabled,
39083932
"AC_build_provenance": _build_provenance,
39093933
"AC_verify_provenance": _verify_provenance,
3934+
"AC_match_json": _match_json,
3935+
"AC_diff_json": _diff_json,
39103936
"AC_unified_diff": _unified_diff,
39113937
"AC_apply_unified": _apply_unified,
39123938
"AC_three_way_merge": _three_way_merge,

0 commit comments

Comments
 (0)