From 20231433d8e2dda7ecf4dbcfd4c9b60d834069ea Mon Sep 17 00:00:00 2001 From: AI Bot Date: Fri, 26 Jun 2026 22:13:56 +0300 Subject: [PATCH] =?UTF-8?q?feat(intent):=20op=3Dmethod=20contract=20?= =?UTF-8?q?=E2=80=94=20nil=5Fintent=20can=20express=20workflow=20actions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend the intent write contract so the single nil_intent payload covers workflow methods (post an invoice, validate a picking, confirm an order), not just CRUD: - Change gains `method` + `params` (for op="method"). - OP_TO_RESOURCE["method"] = "resource.method" — maps to the adapter's generic, allow-list-governed (default-deny) method spine, so no per-action verb is needed. Contract readiness only: the write-execution provider that consumes `change` (CRUD and method) over propose→commit is the intent-unification phase. Reads are unchanged. Backward-compatible (new fields default-empty). Adapter side (odoo resource.method + per-(model,method) allow-list) already shipped. --- src/nilscript/dataplane/intent.py | 12 +++++++++--- tests/test_intent_reads.py | 12 ++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/nilscript/dataplane/intent.py b/src/nilscript/dataplane/intent.py index 41a02f7..3a8b956 100644 --- a/src/nilscript/dataplane/intent.py +++ b/src/nilscript/dataplane/intent.py @@ -36,12 +36,14 @@ class Binding: value: Any -# op → the universal generic-CRUD verb every adapter supports (no adapter-specific verb map, no -# keywords): a change intent executes through resource.* — the deterministic write spine. +# op → the universal generic verb every adapter supports (no adapter-specific verb map, no keywords): +# a change intent executes through resource.* — the deterministic write spine. `method` covers workflow +# actions that are NOT CRUD (post an invoice, validate a picking) via the adapter's generic op=method. OP_TO_RESOURCE: dict[str, str] = { "create": "resource.create", "update": "resource.update", "remove": "resource.delete", + "method": "resource.method", } @@ -49,8 +51,12 @@ class Binding: class Change: """The write shape of an intent: what to make true. Resolved → propose→commit→tier (governed).""" - op: str # create | update | remove + op: str # create | update | remove | method set: dict[str, Any] = field(default_factory=dict) + # for op="method": the workflow method to invoke (action_post / button_validate / …) and its kwargs. + # The adapter's allow-list (default-deny) still governs which (model, method) pairs are committable. + method: str | None = None + params: dict[str, Any] = field(default_factory=dict) @dataclass(frozen=True) diff --git a/tests/test_intent_reads.py b/tests/test_intent_reads.py index 22362c8..14876d5 100644 --- a/tests/test_intent_reads.py +++ b/tests/test_intent_reads.py @@ -112,3 +112,15 @@ def test_seek_summary_groups_via_aggregate() -> None: def test_seek_summary_without_a_dimension_is_a_refusal() -> None: out = _resolver().resolve(Intent(about="res.partner", where=(), seek="summary")) assert out.kind == "refusal" and out.code == "MISSING_DIMENSION" + + +# ── op=method contract readiness (Odoo full-coverage gap 3) ─────────────────────────────────────── +def test_change_contract_supports_method_op() -> None: + """The intent write contract carries `method`/`params` and maps op=method → the adapter's generic + resource.method spine, so workflow actions (post/validate/confirm) are expressible through the one + nil_intent payload. The execution provider that consumes this is the intent-unification phase.""" + from nilscript.dataplane import OP_TO_RESOURCE, Change + + assert OP_TO_RESOURCE["method"] == "resource.method" + change = Change(op="method", method="button_validate", params={"foo": "bar"}) + assert change.op == "method" and change.method == "button_validate" and change.params == {"foo": "bar"}