Status: draft. Normative delta over 0.1.0. RFC 2119 keywords are normative; everything else is informative. This release is a MINOR that carries one breaking profile change (
process_refund) handled via deprecation overlap — admissible pre-1.0. Reference implementation: wosool-cloud (Wosool Cloud). Companion evidence: 0.2.0-calibration-appendix.md.
The changes below are not a change of philosophy. NIL's instincts were already right: status and data are derived facts, not settable fields; the language describes intent, not implementation. A small number of verbs were written against that philosophy. This release realigns those outliers with the model the standard already held. The narrative is not "we fixed mistakes" — it is "we confirmed our philosophy was correct and brought the stragglers into line." This was established empirically: an 18-platform, 90-row calibration against official vendor documentation (the appendix) showed exactly where a verb had encoded one platform's mechanism instead of the user's intent.
The test every verb MUST pass: can a System that derives / splits / abstracts this build a conforming adapter without faking? NIL+DSL is a universal socket — Systems write their own adapters to it (as a device vendor writes a USB driver). The standard defines the socket; it does not build the adapters.
Problem (GAP-001). update_order_status(order_id, status) treats order status as a settable
field. Calibration: 17 of 18 platforms derive status from fulfillment/payment facts or gate it
behind a state machine; only one freely-settable platform satisfied the verb — i.e. the verb was
modeled on that one platform.
Resolution. Two PROPOSE verbs that record a fact; the System derives status.
commerce.record_fulfillment@1.0.0— base tier MEDIUM. Args:order_id,event ∈ {shipped, delivered, returned, canceled}, optionalitems[]{line_id|sku, quantity},carrier,tracking,occurred_at.commerce.record_payment@1.0.0— base tier HIGH, floor HIGH (asserts money moved). Args:order_id,event ∈ {authorized, captured, refunded, voided, failed}, optionalamount(hint only; resolved from record per §6.3),currency,method,reference,occurred_at.
Tiers are justified by impact, not model confidence: record_payment moves money → HIGH;
record_fulfillment is operational and correctable → MEDIUM.
Deprecation. commerce.update_order_status@1.0.0 is DEPRECATED in 0.2.0 with one-MINOR
overlap (VERSIONING). The reference implementation MUST emit an EVENT deprecation warning when it
is used. Removed in 0.3.0.
Note — derivation is behavior, not structure. How a System computes derived status from recorded facts is adapter behavior, deliberately out of scope for this structural release. It is a separate semantic concern that each adapter owns.
Problem (GAP-002). order_id assumes the order is the refund unit. Calibration: payment
processors refund a payment, billing systems an invoice; the order is rarely the refundable
unit outside order-native stores.
Resolution (breaking → @2.0.0). refund_target { type ∈ {order, invoice, payment}, id } —
the unit named abstractly. A System accepts the type(s) it natively models and refuses others with
UNRESOLVED/INVALID_ARGS (a protocol Refusal, 200 — never a transport error). Amount remains
resolved from the target of record (§6.3). Handled under deprecation overlap with @1.0.0.
Problem (GAP-003). Flat price/sku/quantity assume a single-variant model; ~10 platforms
split product/variant/price/inventory.
Resolution (@1.2.0, additive). oneOf: (a) flat single-variant sugar (existing fields),
or (b) variants[] decomposition. Mutually exclusive. Flat satisfies WooCommerce/ERPNext/Odoo;
variants[] satisfies Shopify/Square/Saleor/Medusa without faking.
Problem. 0.1 defined QUERY request bodies but no response shape. Every backend guessed its read-result, and no orchestration layer could type a reference into it.
Resolution. A general answer envelope schemas/query-answer.schema.json:
{ data (required, object), data_gaps?, ssot?, partial? } — data_gaps/ssot mirror the §11.1
Result envelope. Every read verb ships a typed response profile
schemas/profiles/<profile>/<verb>.response.json that pins data (additionalProperties:false,
explicit array items) precisely enough that an orchestration layer derives reference types.
Why typed, not loose {data: object}: the contract serves two layers — adapter conformance
and DSL reference type-checking ($.read.output.clients[0].id). A loose contract satisfies the
adapter but fails the DSL. The higher layer sets the rigor.
First consumer: services.list_clients@1.0.0 (args + list_clients.response.json →
data.clients[]{id,name}), which also closes ADR-0001 (a live read verb that had no profile).
Response profiles for the commerce get_* read verbs are a tracked 0.2.x follow-up.
Args MAY be typed objects and arrays-of-objects. The rule is defined by itself, not by any verb:
D-1. Structured arguments are non-recursive, with a maximum of two chained structural levels — an array of objects MAY contain at most one array of objects; no third level, no self-reference.
create_product.options (array → object → array → object) is the deepest application of the
rule, not its source — defining the limit by a verb would re-commit the very error this release
fixes (encoding the specific in the general). A future need for a third level is a deliberate D-1
change with its own justification, not a pre-emptive allowance.
Arg-shape → DSL reference path (normative for orchestration layers):
| Arg shape | Example | DSL path |
|---|---|---|
| object (level 1) | refund_target |
$.x.field → $.refund_target.id |
| array of objects (level 1) | variants[], data.clients[] |
$.x[0].field → $.variants[0].price |
| nested array of objects (level 2, max) | options[].values[] |
$.x[0].values[0].field |
DSL validators MUST type references to these depths from the profile and reject type-mismatched
references; a fourth-component reference ($.x[0].y[0].z[0]) is a static error (exceeds D-1).
- No new authority. Every change is a verb-arg/response shape change; the Grant/scope model,
default-deny, identity injection (§5), and tier floors are unchanged.
record_paymentandprocess_refundretain floor HIGH → Owner-decision path; mis-recording money cannot bypass it. - Refusal-not-error preserved.
refund_targetwith an unsupportedtypeis a Refusal (200), never a transport error (RFC 9457 boundary unchanged). - QUERY answers remain data, never instruction (§11.2). Typing
datanarrows, never widens, what a read can carry;additionalProperties:falseblocks smuggled fields. No PII shape is newly exposed (list_clientscarries id/name only). - D-1 bounds attack surface. The two-level, non-recursive cap forbids unbounded/recursive arg structures that could exhaust a validator or a downstream adapter.
- Deprecation is observable.
update_order_statususe emits an EVENT warning — no silent behavior change during the overlap.
Schema $ids keep the …/schemas/0.1/… namespace segment (as 0.1 rev 2–4 did across releases);
the 0.2.0 release is tracked here, in CHANGELOG, and in per-verb semver. The $id segment is
the schema namespace line, not the release tag.
- GAP-001/002/003 → resolved (structural). GAP-004 (scheduling) remains deferred declared-design.
update_order_statusleaves the conformance surface (deprecated);record_fulfillment,record_payment, therefund_targetform,create_productvariants, the typed QUERY contract, andservices.list_clientsenter it.- The connectability proof — that any deriving/splitting/abstracting System can build a conforming adapter without faking — is the calibration appendix.