7. Processing Model
Permalink to 7. Processing ModelThis section is normative.
7.1 Pipeline
Permalink to 7.1 PipelineA conforming processor MUST implement (or behave as if it implements) the following pipeline:
YAML files → Schema validation → Parse step strings → Apply extension chain
→ Interpolate templates → Use case AST
→ either:
├─ Code generation → Playwright .spec.ts
└─ Direct execution → StepResult[] → Report
Each stage's responsibilities are listed below.
7.2 Schema validation
Permalink to 7.2 Schema validationThe schema in Appendix B MUST be applied to each input document. Documents that fail validation MUST be rejected with an error that identifies the offending field or step.
The reference implementation uses [Zod] for validation.
7.3 Extension resolution
Permalink to 7.3 Extension resolutionExtension cases use extends: <id> to inherit from a parent. Resolution
proceeds as follows:
- Detect cycles. If processing case
Crequires resolving its own ancestor chain and that chain containsC, the processor MUST raise an error. - Resolve the parent first (recursively).
- Merge
data:merged = { ...parent.data, ...child.data }. - Inherit
start_location,expected_result, andpreconditionsfrom the parent if and only if the child does not provide them. - Compute
steps:- If the child supplies
steps_override, take the parent's firstfrom_step − 1steps re-interpolated againstmergeddata, then append the parsed override steps. - Else if the child supplies its own
steps, parse those. - Else inherit the parent's steps re-interpolated against
mergeddata.
- If the child supplies
- Compute
before(§4.6):- If the child supplies its own
before, parse those. The parent's block is replaced, not merged into or appended to — a child that declares setup is stating the setup it needs, not adding to the parent's. - Else inherit the parent's
beforere-interpolated againstmergeddata.
- If the child supplies its own
Re-interpolation is REQUIRED so that data overridden by the child takes effect
even in inherited steps, in the before block as much as in steps.
Ids are unique within a project (§4). Where a processor is asked for one named document and resolves an id its chain names against other documents in the project, however it locates them, and more than one document declares that id, the processor MUST raise an error naming each declaring document rather than choosing one of them: the choice would otherwise depend on the order the processor happened to visit files, and would be invisible to an author who asked about one file. This check precedes step 1 above, so an ambiguous id is reported as such rather than as a cycle.
Two cases sit outside the requirement. A duplicate id the chain does not name is never resolved, and does not arise. And a processor asked for a whole directory is resolving the set it is already parsing rather than answering about one document, so it MAY keep whichever declaration its own order arrives at; an author who names a folder has not asked about a document in it. A processor MAY reject duplicates there too, and one that does SHOULD say so in its documentation.
7.4 Template interpolation
Permalink to 7.4 Template interpolationThe Step Language uses {{ name }} template variables. Names MAY use dot
notation ({{ a.b.c }}) to address nested mapping fields. Whitespace inside the
braces is permitted.
If a referenced variable cannot be resolved against the effective data map, the
processor MUST raise an error (Undefined template variable: {{ … }}). Falling
back to an empty string is non-conforming.
Object values are JSON-stringified; null becomes the empty string; primitives are stringified by their natural string form.
7.5 Step parsing
Permalink to 7.5 Step parsingA modifier whose argument is a regular expression — matches on the attribute
predicate (§5.7.5), on read_image
(§9A.3) and on sr_says (§9A.4) — MUST be
compiled at parse time and MUST be a parse error if it does not compile. The
pattern is otherwise carried to the point of use and constructed there, which is
inside a browser step, a recognition pass, or a comparison against a spoken log
— a long way from the line that is wrong, and reached only after validate has
called the document conforming. The error MUST carry the engine's own reason.
A modifier MUST NOT appear more than once in one step, and a processor MUST
reject a step in which one does. Modifiers are assigned into a single mapping,
so an accepted repeat would overwrite the earlier clause and the step would
apply half of what it reads as saying: enter: field "E" value "a" value "b"
would enter b, and activate: link "R" new_tab true new_tab false would
expect no new tab, with nothing to distinguish either from the check passing.
This applies to a predicate keyword consumed as part of a larger clause too — a
second is after attribute "X" is "Y" would otherwise land as a stray
modifier beside the real one rather than replacing it.
Each step string is parsed against the grammar in Appendix A. A modifier not listed for the step's keyword in the table below, and any input the grammar leaves unconsumed, MUST be rejected as a parse error. The error MUST name the offending token, the running version, and the modifiers the keyword accepts.
Reporting these as warnings is not sufficient: warnings collected into a list no caller reads let a step carrying an unrecognised clause validate cleanly and then run as though the clause had never been written. A step that reads as an assertion and asserts nothing is worse than one that is rejected, because no diagnostic distinguishes it from the check passing.
A scope clause (within / inside) is part of the target production, not
of the modifier list:
target = role-and-name [ SP scope-kw SP target ]
locate-rest = target [ SP "level" SP 1*DIGIT ]
so it MUST follow the target directly and precede every modifier. A scope clause appearing after a modifier is unconsumed input under the rule above and MUST be rejected. A processor MUST NOT skip such a clause and continue: the step then resolves against the whole page while reading as though it were scoped, and — because scanning stops at the clause — any modifier written after it is lost as well. Both are the failure this section exists to prevent, and both are invisible at runtime, since a page-wide match usually succeeds.
A modifier whose grammar takes a boolean — type_slowly, clear_first,
new_tab, force — MUST be given a literal true or false, matched
case-insensitively. A modifier whose grammar takes a fixed keyword — via,
whose only argument is keyboard — MUST be given that keyword, likewise matched
case-insensitively. In both cases any other token, and an absent argument, MUST
be rejected as a parse error naming the modifier, the offending token, and the
step.
Coercing an unrecognised token to false, or discarding it and leaving the
modifier unset, is the same defect in a different place: the step reads as
expressing one behaviour and executes another, and nothing distinguishes that
from the check passing. via is the sharpest case — a processor that ignores
via keybord runs a mouse click for a step whose whole purpose is to prove the
control is operable from the keyboard.
The unconsumed-input half of this requirement is not limited to keywords whose
tail is a modifier list. wait, wait_for, scroll and lang_check take a
closed production (a duration, a target, or the literal page) with nothing
optional left at the end, and a processor MUST reject anything following it
rather than parse the head and discard the remainder. Doing otherwise makes
wait: 2s within region "Sidebar", wait_for: button "Save" within_ms 500,
scroll: main to bottom and lang_check: main lang "fr" valid documents whose
steps do none of what they say — and within_ms, lang and level are real
modifiers on other keywords, so writing one here is a mistake made from memory
rather than from carelessness. The error MUST name the unconsumed text and the
step.
The modifiers below are the tokens an author writes, not the keys the parser
emits — via, not via_keyboard; attribute, not attribute_name. The table
covers every keyword; those whose whole value is a single production (wait,
navigate, screenshot, keyboard, type, note, viewport) are listed
with that production rather than with a modifier list.
within appears here for every keyword that takes a target, because it is a
token an author writes. It is a scope clause bound to the target rather than a
modifier in the Appendix A sense, which is why §5.1 — reading
"modifier" narrowly — answers "no" for keywords whose only entry here is
within.
| Keyword | Recognized modifiers |
|---|---|
locate |
level, within |
focus |
within |
enter |
value, type_slowly, clear_first, within |
select |
option, force, within |
deselect |
within |
activate |
via, new_tab, force, within |
toggle |
attribute, within |
verify |
attribute, level, has_value, is, within |
scroll |
within |
wait |
duration |
wait_for |
within |
navigate |
url |
screenshot |
name |
keyboard |
keys |
type |
text |
note |
text |
audit |
level, within |
contrast |
level, within |
hover |
within |
hover_out |
within |
read_image |
has_text, matches, lang, within |
lang_check |
within |
sr_says |
matches, after, within |
viewport |
preset or <width>x<height> |
anchor |
label |
The reference implementation exports getParseWarnings() as a stable API. It
returns an empty array: every condition it could report is a parse error.
7.6 The Step AST
Permalink to 7.6 The Step ASTThe result of parsing one step is:
interface Step {
keyword: StepKeyword; // §5.1
target?: TargetDescriptor; // §5.4
modifiers: Record<string, string | number | boolean>;
raw: string; // the step's source line, after interpolation (§10.1)
raw_template?: string; // the pre-interpolation form, for extension reuse (§7.3)
line_number: number; // 1-based, for diagnostics
}
interface TargetDescriptor {
role: string; // role token or custom role
name: string; // accessible name
scope?: TargetDescriptor; // recursive
}
The AST is informational; processors MAY use a different internal representation provided that it captures the same information.