Define the contract before transforming
In a fictional APS case, a form supplies parameters for payment-batch preparation. feature_enabled decides whether a task may execute; retries specifies a permitted integer; each service has an authorized port. Start by writing required types, bounds and associations. The lab supplies feature_enabled=false and retries=3 as key=value pairs: both arrive as strings. With a JSON object containing unquoted false and 3, assertions recognize boolean and integer types. Appearance in the form is not evidence of the type received by the play. Use type tests for contracts and type_debug for diagnosis when needed. Conversion should be deliberate and follow an accepted-input policy. In the experiment, converting text 3x with int returned zero. If zero means no retries, an attempt to correct a type can hide an important operational change.
Separate absence, empty, zero and null
The experiment compares four inputs. A missing variable receives 30 through default(30). An empty string stays empty with the same filter. Zero stays zero, but default(30,true) replaces it with 30. A defined null passes through mandatory and stays null. These results do not determine service policy: you must define it. For pause_seconds, zero may be a valid value that must not disappear. For service_name, null and empty may be errors to reject before any change. In the lab, the nonempty-string assertion fails and the following APPLY_SENTINEL task does not execute. Looking up a missing environment variable returns empty, which an ordinary default also leaves unchanged. Use an empty-value fallback only when that matches requirements. Document these distinctions for RUN so an override does not silently change the meaning of zero.
Compose objects with an explicit list policy
The base has limits={timeout:30,retries:2}; the patch has limits={timeout:45}. Simple composition replaces the limits object and loses retries. Observed recursive composition preserves retries=2 and applies timeout=45. However, recursion does not define list policy. With ports=[8080,8080] in base and [9090] in patch, recursion alone produces [9090]. Using append_rp produces [8080,8080,9090]: duplicates exclusive to base remain. If the consumer requires unique ports, validate that rule independently. Also decide whether duplication is an error or may be normalized; automatic removal can hide competing definitions. Always compare the effective object with the consumer contract, including sibling keys unchanged by the request. Intending to change only timeout is insufficient evidence that retries survived.
Preserve identity and cardinality
Three names and two ports produce only two pairs with zip. The lab adds a length assertion and rejects data before transformation. Even with equal lengths, check associations: sorting names and ports independently does not prove each service received its approved port. A list of records holding name and port together makes the relationship explicit. Another experiment converts two entries sharing key batch into a dictionary; only the last value survives. Check uniqueness before this information loss when the contract prohibits duplicates. Finally, selecting port equal to integer 8080 includes batch_a and excludes batch_b when the second port is string "8080". Normalize types before selection according to input policy, retaining rejection evidence. A filter can execute without error and still produce an incomplete set.
Control names and structure at input
The record object contains an items key. In the experiment, record.items refers to a method, while record["items"] returns stored names. Prefer explicit item lookup when a name may collide with dictionary attributes. Loading variables can also create collisions: timeout=30 for a probe is replaced by 75 when include_vars loads a file at top level. With name:application, timeout retains 30 and application.timeout contains 75. This lets you select each consumer’s data without relying on loading order to distinguish concepts. For external results, separate syntax and structure checks. Text {not-json} fails parsing; JSON text 17 is valid but produces a scalar. A mapping assertion rejects that result before the consumer tries to read service and port. Then add validation for keys, types, bounds and relationships required by the service.
Rehearse conditional migration
The same exercise ran twice on core 2.16.14 and twice on core 2.19.0, versions pinned for behavioral comparison without claiming they are the newest patch. With feature_enabled set to string "false", when:feature_enabled executes the task on 2.16.14 and fails with a boolean-result requirement on 2.19.0. With when: feature_enabled | bool, this recognized input yields false and skips the task on both versions. That does not authorize arbitrary text: define permitted inputs or require a boolean at entry. The porting guide identifies other review areas; this experiment does not establish compatibility for an entire project. Before updating the controller, identify relevant conditions and test negative examples too. Do not enable older compatibility merely to turn a visible failure into execution that violates requirements.
Guided exercise and handover criteria
Predict results before opening commands.json. For each experiment, write input, expected type, transformation and acceptance criterion. Run the runner in a fresh directory and compare predictions: each run makes 14 invocations; versions 2.16.14 and 2.19.0 pass 31 and 32 checks respectively. The newer version’s extra check confirms the message requiring a boolean. Tasks execute on the controller using synthetic data, without remote modules, credentials or RHEL services. To practise a realistic handover, define contracts for three services and prepare invalid inputs: an empty name, an unexpected textual port and a duplicate key. Explain where each input should be rejected and which task should no longer execute. Summarize evidence for RUN and connect it to inventories, role contracts and template validation. Functional service acceptance still requires another rehearsal.
SYNTHETIC INPUTS
false in key=value -> string
false in JSON -> boolean
0 | default(30) -> 0
0 | default(30,true) -> 30
null | mandatory -> null
3 names zip 2 ports -> 2 pairs
when: feature_enabled | bool -> false for recognized string "false"A batch receives feature_enabled="false", name and port lists of different lengths and configuration with a duplicate key. Identify gates that should prevent execution.
Common pitfalls
Treating zero as missing; accepting null through mandatory; losing entries through zip or items2dict; assuming append_rp uniqueness; using strings as implicit authorization.
Related topics: Inventory and configuration · Role contracts · Template validation
Error-free data transformation does not prove contract compliance: validate types, content and relationships before changing systems.
Reference: Assert input contracts · EX294 current objectives inspected 2026-09-30; RHCE in Ansible framework effective 2026-05-11; booking product version not publicly pinned