kaloko 0.4.8 → 0.4.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -1
- package/package.json +1 -1
- package/schemas/config.v1.json +26 -0
- package/schemas/scenario.v1.json +32 -0
- package/schemas/spec.v1.json +60 -0
- package/skills/kaloko/SKILL.md +12 -2
- package/skills/kaloko/reference/acceptance.md +34 -2
- package/skills/kaloko/reference/design.md +35 -2
- package/skills/kaloko/reference/evaluation.md +46 -3
- package/skills/kaloko/reference/execution.md +18 -0
- package/skills/kaloko/reference/mcp.md +3 -1
- package/src/cli.mjs +44 -4
- package/src/lib/build.mjs +30 -5
- package/src/lib/capture.mjs +43 -1
- package/src/lib/design-cli.mjs +7 -3
- package/src/lib/devspec.mjs +821 -0
- package/src/lib/doctor.mjs +7 -0
- package/src/lib/drift.mjs +16 -8
- package/src/lib/evaluate.mjs +94 -12
- package/src/lib/keyboard.mjs +261 -0
- package/src/lib/manifest-core.mjs +16 -5
- package/src/lib/pack-help.mjs +7 -0
- package/src/lib/packs.mjs +9 -2
- package/src/lib/png.mjs +61 -0
- package/src/lib/probe.mjs +57 -0
- package/src/lib/recheck.mjs +3 -1
- package/src/lib/spec-cli.mjs +90 -0
- package/src/lib/stability-core.mjs +114 -0
- package/src/lib/stability.mjs +249 -0
- package/src/lib/vision-core.mjs +132 -0
- package/src/lib/vision.mjs +127 -0
- package/templates/kaloko.config.yml +6 -0
- package/viewer/index.html +500 -44
package/README.md
CHANGED
|
@@ -81,6 +81,15 @@ what still waits for a person, so it can pass the link on. Agents never
|
|
|
81
81
|
approve for people: their decisions are marked and do not count as a
|
|
82
82
|
person's.
|
|
83
83
|
|
|
84
|
+
Every AI verdict comes with one sentence saying why. Criteria marked
|
|
85
|
+
`vision: true`, and answers the text evaluator is unsure about, get a second
|
|
86
|
+
look at the screenshot from a vision model (`ANTHROPIC_API_KEY`, or through
|
|
87
|
+
Kaloko); `kaloko build` lists what to act on with the score and the reason.
|
|
88
|
+
The `a11y` pack walks the page with Tab (order, visible focus, traps, skip
|
|
89
|
+
link) and reads names from the accessibility tree. Pixel diffs skip the
|
|
90
|
+
regions a step ignores; `kaloko walk --stability 3` and `kaloko stability`
|
|
91
|
+
find the clocks, ads and carousels worth ignoring.
|
|
92
|
+
|
|
84
93
|
## Design (Kaloko Design)
|
|
85
94
|
|
|
86
95
|
Design the screens of a flow as live HTML with your own agent and skills, in the same scenario that development
|
|
@@ -98,6 +107,17 @@ npx kaloko share --name "Checkout v2" # a version of the whole flow
|
|
|
98
107
|
npx kaloko reference --design --version 2 # after the team accepted it
|
|
99
108
|
```
|
|
100
109
|
|
|
110
|
+
Developers and their agents build from what was approved. *Dev mode* (`D` in the step detail) shows each
|
|
111
|
+
element's box, sizes and distances, its colors, type and spacing with the token every value comes from (or a
|
|
112
|
+
warning when it is not a token), contrast against WCAG AA, the `:hover`, `:focus`, `:active` and disabled states,
|
|
113
|
+
and its images. The same data is the step spec an agent reads, and `kaloko drift` checks the implementation
|
|
114
|
+
against it:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
npx kaloko spec payment # elements, styles → tokens, states (also MCP get_step_spec)
|
|
118
|
+
npx kaloko walk && npx kaloko drift # expected token vs actual value per element; exit 1 while it differs
|
|
119
|
+
```
|
|
120
|
+
|
|
101
121
|
On the canvas, *Prototype* (or `P`) presents the steps full screen as live pages: click through links between
|
|
102
122
|
steps, switch the device, walk with the arrow keys, and share a link that opens straight into a step. The rules
|
|
103
123
|
for pages that work everywhere are in `skills/kaloko/reference/portable-html.md`.
|
|
@@ -141,7 +161,9 @@ captured HTML, page text and console output.
|
|
|
141
161
|
- **Service and evaluator**: `KALOKO_TOKEN` for `share`, `feedback`, `review`, `inbox` and clean-up (`runs`, `trash`, `prune`, `keep`, `archive`)
|
|
142
162
|
(`npx kaloko login` approves one in the browser and saves it to `~/.config/kaloko/.env`, or create it in your
|
|
143
163
|
organization's settings); `TYPESAFE_API_KEY` for the
|
|
144
|
-
semantic evaluator
|
|
164
|
+
semantic evaluator and `ANTHROPIC_API_KEY` for the vision evaluator (both
|
|
165
|
+
optional; without them Kaloko evaluates on your plan's allowance). The names
|
|
166
|
+
can be changed in the config.
|
|
145
167
|
|
|
146
168
|
`npx kaloko doctor` checks that every variable the config names is set and that
|
|
147
169
|
each environment lets you in.
|
package/package.json
CHANGED
package/schemas/config.v1.json
CHANGED
|
@@ -45,6 +45,32 @@
|
|
|
45
45
|
"signals": { "type": "boolean", "default": true, "description": "Quick signals on every screen (purpose met, expected language, looks broken); never change verdicts." },
|
|
46
46
|
"concurrency": { "type": "integer", "minimum": 1, "maximum": 16, "default": 6, "description": "Evaluator requests in flight at once." }
|
|
47
47
|
}
|
|
48
|
+
},
|
|
49
|
+
"vision": {
|
|
50
|
+
"description": "Second, vision-capable evaluator (#17): sees the screenshot (a crop for scoped criteria) and answers with a probability and a one-sentence reason. Asked only for criteria with vision: true and for text answers in the uncertain band; false turns it off.",
|
|
51
|
+
"oneOf": [
|
|
52
|
+
{ "const": false },
|
|
53
|
+
{
|
|
54
|
+
"type": "object",
|
|
55
|
+
"additionalProperties": false,
|
|
56
|
+
"properties": {
|
|
57
|
+
"provider": { "enum": ["anthropic"], "default": "anthropic" },
|
|
58
|
+
"model": { "type": "string", "default": "claude-opus-5-5", "description": "Model id of the provider (Anthropic Messages API with image input)." },
|
|
59
|
+
"api_key_env": { "type": "string", "default": "ANTHROPIC_API_KEY" },
|
|
60
|
+
"base_url": { "type": "string", "default": "https://api.anthropic.com" },
|
|
61
|
+
"when": { "enum": ["uncertain", "flagged", "always", "off"], "default": "uncertain", "description": "flagged: only criteria with vision: true; uncertain: those plus text answers in the uncertain band; always: every semantic criterion (most expensive)." },
|
|
62
|
+
"uncertain": { "type": "array", "items": { "type": "number", "minimum": 0, "maximum": 1 }, "minItems": 2, "maxItems": 2, "default": [0.15, 0.85], "description": "Text scores strictly between these go to the vision evaluator." },
|
|
63
|
+
"max_calls": { "type": "integer", "minimum": 0, "default": 40, "description": "Vision requests per evaluation (uncached); the rest stay with the text answer." },
|
|
64
|
+
"max_width": { "type": "integer", "minimum": 320, "maximum": 1568, "default": 1000, "description": "Images are scaled down to this width (px)." },
|
|
65
|
+
"tile_height": { "type": "integer", "minimum": 400, "maximum": 1568, "default": 1400, "description": "A full page is cut into tiles of this height." },
|
|
66
|
+
"max_tiles": { "type": "integer", "minimum": 1, "maximum": 4, "default": 2, "description": "Tiles of a full page sent at most (from the top); scoped criteria get a crop instead." },
|
|
67
|
+
"effort": { "enum": ["low", "medium", "high"], "default": "low" },
|
|
68
|
+
"reason_language": { "type": "string", "default": "en", "description": "Language of the one-sentence reasons (en, cs, …)." },
|
|
69
|
+
"hosted": { "type": "boolean", "default": true, "description": "Without a local key, ask through the organization's service (counts against the monthly allowance; one vision question counts as 5)." },
|
|
70
|
+
"concurrency": { "type": "integer", "minimum": 1, "maximum": 8, "default": 3 }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
]
|
|
48
74
|
}
|
|
49
75
|
}
|
|
50
76
|
},
|
package/schemas/scenario.v1.json
CHANGED
|
@@ -262,6 +262,11 @@
|
|
|
262
262
|
"capture": {
|
|
263
263
|
"$ref": "#/$defs/capture"
|
|
264
264
|
},
|
|
265
|
+
"ignore": {
|
|
266
|
+
"type": "array",
|
|
267
|
+
"description": "Regions left out of pixel comparisons (clocks, ads, carousels): a selector, or a rectangle in page pixels of one viewport. kaloko stability suggests them.",
|
|
268
|
+
"items": { "$ref": "#/$defs/ignoreRegion" }
|
|
269
|
+
},
|
|
265
270
|
"items": {
|
|
266
271
|
"type": "object",
|
|
267
272
|
"additionalProperties": false,
|
|
@@ -500,6 +505,13 @@
|
|
|
500
505
|
"a11y_contrast",
|
|
501
506
|
"a11y_main",
|
|
502
507
|
"a11y_unique_ids",
|
|
508
|
+
"a11y_focus_order",
|
|
509
|
+
"a11y_focus_visible",
|
|
510
|
+
"a11y_keyboard_trap",
|
|
511
|
+
"a11y_skip_link",
|
|
512
|
+
"a11y_ax_names",
|
|
513
|
+
"a11y_landmarks",
|
|
514
|
+
"a11y_aria",
|
|
503
515
|
"perf_lcp",
|
|
504
516
|
"perf_cls",
|
|
505
517
|
"perf_ttfb",
|
|
@@ -588,6 +600,10 @@
|
|
|
588
600
|
"console"
|
|
589
601
|
],
|
|
590
602
|
"description": "console: the evaluator reads the captured console errors with the step purpose (triage) instead of the page"
|
|
603
|
+
},
|
|
604
|
+
"vision": {
|
|
605
|
+
"type": "boolean",
|
|
606
|
+
"description": "Also show the evaluator the screenshot (a crop of scope when set): layout, imagery, brand. Needs a vision evaluator (evaluators.vision)."
|
|
591
607
|
}
|
|
592
608
|
}
|
|
593
609
|
},
|
|
@@ -632,6 +648,22 @@
|
|
|
632
648
|
}
|
|
633
649
|
}
|
|
634
650
|
},
|
|
651
|
+
"ignoreRegion": {
|
|
652
|
+
"type": "object",
|
|
653
|
+
"additionalProperties": false,
|
|
654
|
+
"oneOf": [
|
|
655
|
+
{ "required": ["selector"] },
|
|
656
|
+
{ "required": ["rect", "viewport"] }
|
|
657
|
+
],
|
|
658
|
+
"properties": {
|
|
659
|
+
"selector": { "type": "string", "minLength": 1, "description": "CSS selector; every visible match is ignored." },
|
|
660
|
+
"rect": { "type": "array", "items": { "type": "number", "minimum": 0 }, "minItems": 4, "maxItems": 4, "description": "[x, y, width, height] in CSS pixels of the page." },
|
|
661
|
+
"viewport": { "type": "string", "description": "The viewport a rect belongs to." },
|
|
662
|
+
"viewports": { "type": "array", "items": { "type": "string" }, "description": "Only in these viewports (selector)." },
|
|
663
|
+
"locales": { "type": "array", "items": { "type": "string" }, "description": "Only in these locales." },
|
|
664
|
+
"note": { "type": "string" }
|
|
665
|
+
}
|
|
666
|
+
},
|
|
635
667
|
"capture": {
|
|
636
668
|
"type": "object",
|
|
637
669
|
"additionalProperties": false,
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://qawalk.com/schemas/spec.v1.json",
|
|
4
|
+
"title": "qawalk.spec.v1",
|
|
5
|
+
"description": "The step spec (#42 Dev mode): what to build for one step as it was approved. Written by the in-page engine src/lib/devspec.mjs at capture (steps/<step>/<locale>/<viewport>.spec.json, and the capture record of a design revision); the canvas Dev mode runs the same engine on the live page. Served by GET …/scenarios/<id>/steps/<step>/spec, MCP get_step_spec and kaloko spec. Elements are listed in page order; a style lists only what differs from what the element inherits (page, then the parent element).",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["schema", "elements"],
|
|
8
|
+
"$defs": {
|
|
9
|
+
"value": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"description": "A computed value (colors as #rrggbb[aa], lengths in px) and the token it maps to. token: the DTCG path (several, space-separated, for a shorthand such as padding; 0 for a zero side); null when the set has tokens of that kind but none fits and none is near. nearest: the closest token when the value is not one. mode: the mode whose value it is (light, dark, …). hard: the stylesheet typed the raw value although the token exists. var: the custom property the author used when it names no token.",
|
|
12
|
+
"required": ["value"],
|
|
13
|
+
"properties": {
|
|
14
|
+
"value": { "type": "string" },
|
|
15
|
+
"token": { "type": ["string", "null"] },
|
|
16
|
+
"nearest": { "type": "string" },
|
|
17
|
+
"mode": { "type": "string" },
|
|
18
|
+
"hard": { "type": "boolean" },
|
|
19
|
+
"var": { "type": "string" }
|
|
20
|
+
},
|
|
21
|
+
"additionalProperties": false
|
|
22
|
+
},
|
|
23
|
+
"styles": {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"description": "CSS property → value: color, background-color, border-color, font-family, font-size, font-weight, line-height, letter-spacing, padding, gap, row-gap, column-gap, border-width, border-radius, box-shadow, opacity, display and flex or grid properties.",
|
|
26
|
+
"additionalProperties": { "$ref": "#/$defs/value" }
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"properties": {
|
|
30
|
+
"schema": { "const": "qawalk.spec.v1" },
|
|
31
|
+
"title": { "type": "string" },
|
|
32
|
+
"lang": { "type": ["string", "null"] },
|
|
33
|
+
"viewport": { "type": "array", "items": { "type": "number" }, "minItems": 2, "maxItems": 2 },
|
|
34
|
+
"size": { "type": "array", "items": { "type": "number" }, "minItems": 2, "maxItems": 2, "description": "Document width and height." },
|
|
35
|
+
"tokens": { "type": ["object", "null"], "description": "The token set the values were mapped with (the step's tokens.json); null without one.", "properties": { "count": { "type": "integer" }, "modes": { "type": "array", "items": { "type": "string" } } } },
|
|
36
|
+
"page": { "$ref": "#/$defs/styles", "description": "Typography and text color of the page (body), inherited by every element that does not say otherwise." },
|
|
37
|
+
"elements": {
|
|
38
|
+
"type": "array",
|
|
39
|
+
"items": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"required": ["i", "parent", "tag", "sel", "styles"],
|
|
42
|
+
"properties": {
|
|
43
|
+
"i": { "type": "integer" },
|
|
44
|
+
"parent": { "type": "integer", "description": "Index of the nearest listed ancestor, -1 at the top." },
|
|
45
|
+
"tag": { "type": "string" },
|
|
46
|
+
"role": { "type": ["string", "null"], "description": "ARIA role, explicit or implicit (button, link, heading, main, textbox, …)." },
|
|
47
|
+
"name": { "type": ["string", "null"], "description": "Accessible name or own text (at most 80 characters)." },
|
|
48
|
+
"sel": { "type": "string", "description": "A CSS selector of the element in the design page (ids, data-testid and classes first)." },
|
|
49
|
+
"box": { "type": "array", "items": { "type": "number" }, "minItems": 4, "maxItems": 4, "description": "x, y, width, height in CSS px of the page." },
|
|
50
|
+
"styles": { "$ref": "#/$defs/styles" },
|
|
51
|
+
"contrast": { "type": "object", "properties": { "ratio": { "type": "number" }, "aa": { "type": "boolean" } } },
|
|
52
|
+
"states": { "type": "object", "description": "hover, focus, active, disabled → the styles that change in that state (the page's rules forced on the element).", "additionalProperties": { "$ref": "#/$defs/styles" } },
|
|
53
|
+
"asset": { "type": "object", "properties": { "kind": { "enum": ["img", "svg"] }, "src": { "type": "string" }, "size": { "type": "array" }, "alt": { "type": ["string", "null"] } } }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"used": { "type": "object", "description": "Token path → number of values that use it.", "additionalProperties": { "type": "integer" } },
|
|
58
|
+
"off": { "type": "array", "description": "Values outside the token set, most frequent first.", "items": { "type": "object", "properties": { "prop": { "type": "string" }, "value": { "type": "string" }, "count": { "type": "integer" }, "nearest": { "type": ["string", "null"] }, "sel": { "type": "string" } } } }
|
|
59
|
+
}
|
|
60
|
+
}
|
package/skills/kaloko/SKILL.md
CHANGED
|
@@ -11,7 +11,9 @@ description: >
|
|
|
11
11
|
reviewer rejected on a canvas, what is waiting in the Kaloko inbox, or how
|
|
12
12
|
to report a run in an issue, and when designing screens of a flow in a
|
|
13
13
|
design environment (live HTML steps, drafts, versions, the design
|
|
14
|
-
reference, checking implementation against it)
|
|
14
|
+
reference, checking implementation against it), and when implementing a
|
|
15
|
+
screen that has an approved design (read the step spec, build with its
|
|
16
|
+
tokens, check drift).
|
|
15
17
|
---
|
|
16
18
|
|
|
17
19
|
# Kaloko
|
|
@@ -32,6 +34,10 @@ the canvas, storage and collaboration are the service.
|
|
|
32
34
|
for them (`kaloko share` prints it; `kaloko review` repeats it), and you
|
|
33
35
|
read `kaloko feedback` and `kaloko inbox` before continuing on that task.
|
|
34
36
|
- Changes to intent are written back to `ACCEPTANCE.md` (change log row).
|
|
37
|
+
- When you implemented a step from an approved design: you read its spec
|
|
38
|
+
(`kaloko spec <step>`), walked the implementation with `packs: [design]`,
|
|
39
|
+
and `kaloko drift` is clean or the PR names each difference left for a
|
|
40
|
+
person to decide.
|
|
35
41
|
- On a design environment: the folder follows the bundle rules and
|
|
36
42
|
`kaloko design lint` shows no error, `kaloko walk` and `kaloko sync` ran,
|
|
37
43
|
`kaloko status` shows no conflict, and you read the draft's feedback; a
|
|
@@ -77,7 +83,8 @@ they can do; never write secrets into the config, a scenario or a script.
|
|
|
77
83
|
[reference/stacks.md](reference/stacks.md)
|
|
78
84
|
- Choosing playwright, agent or manual per step, and driving the shared
|
|
79
85
|
browser with your capabilities: [reference/execution.md](reference/execution.md)
|
|
80
|
-
- Evaluation,
|
|
86
|
+
- Evaluation, thresholds, the vision evaluator and its reasons, reading
|
|
87
|
+
results: [reference/evaluation.md](reference/evaluation.md)
|
|
81
88
|
- Environments, access (gates, accounts, sign-in), e-mails, readonly and
|
|
82
89
|
production rules, and what is yours to arrange for the project's stack:
|
|
83
90
|
[reference/environments.md](reference/environments.md)
|
|
@@ -94,6 +101,9 @@ they can do; never write secrets into the config, a scenario or a script.
|
|
|
94
101
|
references, handoff, tokens, the design pack, canvas edits, branches, HTML
|
|
95
102
|
view, presenting the prototype, exports, bringing your own design skill):
|
|
96
103
|
[reference/design.md](reference/design.md)
|
|
104
|
+
- Implementing a screen from the approved design: read the step spec, build
|
|
105
|
+
with its tokens, walk, check drift, fix:
|
|
106
|
+
[reference/design.md#implement-from-the-approved-design](reference/design.md#implement-from-the-approved-design)
|
|
97
107
|
- Writing design pages that work on the live step, from disk and in other
|
|
98
108
|
tools (relative URLs, no CDNs, state in the URL, deterministic content,
|
|
99
109
|
links between steps; what `kaloko design lint` checks):
|
|
@@ -40,8 +40,21 @@ for public pages and landings; task flows keep their own criteria.
|
|
|
40
40
|
|
|
41
41
|
- `a11y` (`AC3xx`): images with alt, links and buttons with a name, labelled
|
|
42
42
|
fields, heading levels not skipped, WCAG AA text contrast, a `main`
|
|
43
|
-
landmark, unique ids
|
|
44
|
-
|
|
43
|
+
landmark, unique ids (`AC301`–`AC307`, read from the page). After the
|
|
44
|
+
screenshot the walk also presses Tab through the page: `AC308` tab order
|
|
45
|
+
(hidden focus, big jumps back, positive `tabindex`, controls left out or
|
|
46
|
+
mouse-only), `AC309` visible focus with at least 3 : 1 contrast (each stop
|
|
47
|
+
screenshotted focused and unfocused), `AC310` no keyboard trap (a modal
|
|
48
|
+
dialog may keep focus), `AC311` a skip link or main content within three
|
|
49
|
+
stops, `AC312` names from Chrome's accessibility tree (what a screen
|
|
50
|
+
reader announces). `AC313` landmarks (one `main`, labelled repeats) and
|
|
51
|
+
`AC314` ARIA (unknown roles and attributes, references to missing ids,
|
|
52
|
+
`aria-hidden` over focusable content) are read from the page. The first
|
|
53
|
+
five focus stops are kept as screenshots and the canvas can draw the tab
|
|
54
|
+
order on the capture. The walk only moves focus: nothing is clicked or
|
|
55
|
+
typed, and the page is left unfocused at the top. Stack steps walk the
|
|
56
|
+
keyboard on their first five pages. Still a subset of WCAG 2.2 AA; it
|
|
57
|
+
does not replace an audit with a screen reader.
|
|
45
58
|
- `perf` (`AC4xx`): LCP ≤ 2.5 s, CLS ≤ 0.1, TTFB ≤ 800 ms, page weight ≤ 3 MB,
|
|
46
59
|
measured in this walk's browser since the page load (lab values: the
|
|
47
60
|
machine and network of the walk matter; compare runs, not absolutes).
|
|
@@ -227,3 +240,22 @@ skips a session's nodes there. The canvas shows a chip with the person on
|
|
|
227
240
|
every step, groups the step list by person and draws hand-offs between
|
|
228
241
|
people as dashed arrows. Sessions run one after another, never in parallel,
|
|
229
242
|
and never share cookies.
|
|
243
|
+
|
|
244
|
+
## Ignore regions
|
|
245
|
+
|
|
246
|
+
Clocks, ads, carousels and "3 minutes ago" change on every capture and make
|
|
247
|
+
pixel diffs noisy. List them on the step and both the canvas diff and the
|
|
248
|
+
design pixel check (`AC801`) leave them out:
|
|
249
|
+
|
|
250
|
+
```yaml
|
|
251
|
+
- id: home
|
|
252
|
+
ignore:
|
|
253
|
+
- { selector: "#clock" } # every visible match
|
|
254
|
+
- { selector: ".ad-slot", viewports: [desktop] }
|
|
255
|
+
- { rect: [0, 620, 1280, 240], viewport: desktop } # page pixels of one viewport
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Do not guess them: `kaloko walk --stability 3` notes what moves while the
|
|
259
|
+
page sits still, and `kaloko stability` compares the step with earlier runs
|
|
260
|
+
and writes suggestions (see [execution.md](execution.md)). Ignore only noise;
|
|
261
|
+
a region that matters to a criterion stays in.
|
|
@@ -39,8 +39,10 @@ design/
|
|
|
39
39
|
the bundle.
|
|
40
40
|
- Links between steps are plain relative links to the page: `<a href="../payment/index.html">`. The canvas
|
|
41
41
|
follows them; `data-kaloko-go="payment"` (or `data-qawalk-go`) moves to that step whatever the `href` says.
|
|
42
|
-
- Reference token values as CSS custom properties
|
|
43
|
-
check see
|
|
42
|
+
- Reference token values as CSS custom properties named after the token path (`color.accent` →
|
|
43
|
+
`var(--color-accent)`), so Dev mode, the step spec and the token check see which token a value comes from.
|
|
44
|
+
Define interaction states in CSS (`:hover`, `:focus-visible`, `:active`, `:disabled`): Dev mode and the spec
|
|
45
|
+
read them from the stylesheet.
|
|
44
46
|
- Write pages by the portable HTML rules ([portable-html.md](portable-html.md)): relative URLs only, bundled
|
|
45
47
|
fonts, no CDNs, state in the URL instead of storage, deterministic content, viewport meta and `lang`.
|
|
46
48
|
`kaloko design lint` checks the folder; `kaloko sync` runs it first and stops on errors (things that break on
|
|
@@ -94,8 +96,39 @@ Development builds from the design reference:
|
|
|
94
96
|
- `kaloko export --live [--version n] --out design/` writes the live files into a folder.
|
|
95
97
|
- MCP `get_step` (scenario, step, `ref: design`) returns the file list, a signed entry URL, the token set, the
|
|
96
98
|
page structure and open comments; `list_step_changes` names the steps that changed since the last reference.
|
|
99
|
+
- MCP `get_step_spec` (or `kaloko spec <step>`) returns what to build, element by element, with tokens; see
|
|
100
|
+
[Implement from the approved design](#implement-from-the-approved-design).
|
|
97
101
|
- Translate to the project's stack with the project's own skills; keep the step ids and the token names.
|
|
98
102
|
|
|
103
|
+
## Implement from the approved design
|
|
104
|
+
|
|
105
|
+
Use this when you build or change a screen that has an approved design step (a design reference, or a version
|
|
106
|
+
the person names). The step spec says what was approved in the terms you write code in: elements in page order
|
|
107
|
+
with role, accessible name and selector, and their colors, typography, spacing, radius, shadow and layout, each
|
|
108
|
+
value with the DTCG token it comes from. People see the same data in the canvas: *Dev mode* (`D`) in the step
|
|
109
|
+
detail.
|
|
110
|
+
|
|
111
|
+
1. **Read the spec** of each step you build: `kaloko spec <step>` (default `--ref design`; `--ref v4`, `draft` or a
|
|
112
|
+
run id), or MCP `get_step_spec`. `--json` gives `qawalk.spec.v1` (`schemas/spec.v1.json`).
|
|
113
|
+
2. **Implement with the tokens.** A line such as `button "Pay" 120×44 bg #2e6bff=color.brand.primary · pad 12px
|
|
114
|
+
20px=space.3 space.5` means `background: var(--color-brand-primary); padding: var(--space-3) var(--space-5)`
|
|
115
|
+
(the custom property is the token path with dots as dashes) or the project's own token mechanism. `not a token;
|
|
116
|
+
nearest x` means the design itself used a raw value: ask the designer before you copy it, and say so in the PR.
|
|
117
|
+
`:hover`, `:focus`, `:active` and `:disabled` lines are states to build; `(light)` names the mode of a value.
|
|
118
|
+
Keep the roles and names: they are how the implementation is matched with the design.
|
|
119
|
+
3. **Walk it** with `packs: [design]` on the implementation steps; the capture then stores the implementation's
|
|
120
|
+
spec next to the screenshot.
|
|
121
|
+
4. **Check drift**: `kaloko drift [step]` compares the walked run with the design reference of the linked design
|
|
122
|
+
branch and exits 1 while something differs. A shared run: MCP `get_step_drift` (`run`: id or `latest`).
|
|
123
|
+
Findings name the element, the expected token and value, and what the page has; `typed raw` means the value
|
|
124
|
+
is right but should come from the token; `not found` means the design's element (by role and name) is missing.
|
|
125
|
+
5. **Fix and repeat** until drift is clean or what is left is a decision for a person (a missing element on
|
|
126
|
+
purpose, a raw value the designer accepted); report those in the PR instead of hiding them.
|
|
127
|
+
|
|
128
|
+
Specs exist for captures made with the current CLI: walk the design and sync again if `kaloko spec` says there is
|
|
129
|
+
none. The pixel, structure and token checks of the design pack (`AC801`–`AC803`) still run in `kaloko evaluate`;
|
|
130
|
+
drift is the per-element view an agent can act on.
|
|
131
|
+
|
|
99
132
|
## Your data is yours
|
|
100
133
|
|
|
101
134
|
Everything exports in open formats, readable without Kaloko:
|
|
@@ -10,9 +10,11 @@ answer, so running it again is cheap.
|
|
|
10
10
|
- deterministic results computed in the live page at capture time (with
|
|
11
11
|
evidence: selector snippet, measured value);
|
|
12
12
|
- the semantic evaluator for semantic criteria. It is a replaceable provider;
|
|
13
|
-
the default is JEV by TypeSafe (`TYPESAFE_API_KEY`), which
|
|
14
|
-
|
|
15
|
-
`
|
|
13
|
+
the default is JEV by TypeSafe (`TYPESAFE_API_KEY`), which reads the
|
|
14
|
+
reduced page state (no images) and returns only a probability per question
|
|
15
|
+
(`score`, 0–1). Verdicts: `pass` ≥ 0.85, `fail` ≤ 0.15, otherwise
|
|
16
|
+
`needs_review` (thresholds in config);
|
|
17
|
+
- a vision evaluator for a second look with the screenshot (below);
|
|
16
18
|
- `manual` criteria stay `not_evaluated` until a reviewer decides.
|
|
17
19
|
|
|
18
20
|
Where the evaluator runs: with `TYPESAFE_API_KEY` set locally, your own key; without it, the organization's
|
|
@@ -25,6 +27,47 @@ report with the reason `kaloko evaluate` gives.
|
|
|
25
27
|
The default evaluator is text-only and most accurate in English. It is weak
|
|
26
28
|
at numbers, which is why those are checked deterministically.
|
|
27
29
|
|
|
30
|
+
## Vision and reasons
|
|
31
|
+
|
|
32
|
+
Every semantic verdict carries one sentence, `reason`, shown as "Why" next to
|
|
33
|
+
the score on the canvas, in `kaloko build`, in the walk summary and in
|
|
34
|
+
`get_run_summary`. With only the text evaluator the sentence says what it
|
|
35
|
+
read and how sure it was (JEV gives no explanation). The vision evaluator
|
|
36
|
+
gives a real one: it sees the screenshot, or a crop of the criterion's
|
|
37
|
+
`scope` (or `highlight`), and answers with a probability and what in the
|
|
38
|
+
image decided it. It is asked for:
|
|
39
|
+
|
|
40
|
+
- criteria with `vision: true` on the check (layout, imagery, "looks like the
|
|
41
|
+
brand", anything the text cannot show);
|
|
42
|
+
- text answers in the uncertain band (0.15–0.85 by default), which would
|
|
43
|
+
otherwise go to a person.
|
|
44
|
+
|
|
45
|
+
Its answer replaces the score (the text score stays as `text_score` in the
|
|
46
|
+
evaluation). Cost stays bounded: only those criteria, one request per
|
|
47
|
+
capture, images scaled to 1000 px wide and at most two tiles of a full page,
|
|
48
|
+
at most 40 uncached requests per evaluation, and answers cached by content
|
|
49
|
+
like the text ones. The provider is Anthropic's Messages API
|
|
50
|
+
(`ANTHROPIC_API_KEY`, model from `evaluators.vision.model`, default
|
|
51
|
+
`claude-opus-5-5`). Without the key it goes through the organization's
|
|
52
|
+
service, where one vision question counts as five of the monthly allowance;
|
|
53
|
+
when the text evaluator runs with your own `TYPESAFE_API_KEY`, the vision
|
|
54
|
+
evaluator also waits for your own key (`evaluators.vision.hosted: true`
|
|
55
|
+
lets it use the service anyway).
|
|
56
|
+
Configure or switch it off in `kaloko.config.yml`:
|
|
57
|
+
|
|
58
|
+
```yaml
|
|
59
|
+
evaluators:
|
|
60
|
+
vision:
|
|
61
|
+
when: uncertain # flagged (only vision: true) | always | off
|
|
62
|
+
uncertain: [0.15, 0.85]
|
|
63
|
+
max_calls: 40
|
|
64
|
+
reason_language: en # cs for Czech reviewers
|
|
65
|
+
# vision: false # text evaluator only
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Report the reason with each `needs_review` or failing criterion; when the
|
|
69
|
+
evaluator saw the screenshot, say so.
|
|
70
|
+
|
|
28
71
|
When a criterion comes back `needs_review`, read the reduced state the
|
|
29
72
|
evaluator saw (`steps/<node>/<locale>/<viewport>.capture.json`, key `state`)
|
|
30
73
|
and the screenshot. Either the page is genuinely unclear (report it), the
|
|
@@ -73,3 +73,21 @@ The environment policy and gate apply to every context the CLI creates (the
|
|
|
73
73
|
shared browser, `walk` sessions, one-off captures): blocked paths and, when
|
|
74
74
|
readonly, non-GET requests outside `allowed_post_paths` fail and are listed as
|
|
75
75
|
blocked requests in the capture; the gate's credentials go to its origins only.
|
|
76
|
+
|
|
77
|
+
## Noisy regions (`--stability`, `kaloko stability`)
|
|
78
|
+
|
|
79
|
+
When pixel diffs between runs keep flagging the same clock, ad or carousel:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
kaloko walk --stability 3 # 3 screenshots per capture, a moment apart; what moved is noted
|
|
83
|
+
kaloko stability # this run against the 3 previous local runs of the scenario
|
|
84
|
+
kaloko stability --apply # adds the suggested ignore: to each step (block-style YAML only)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`kaloko stability` writes `stability.json` and `stability.yml` into the run
|
|
88
|
+
and prints the suggestions per step: a selector when an element of the stored
|
|
89
|
+
page covers the region, otherwise a rectangle for one viewport. Look at the
|
|
90
|
+
screenshots before applying; a region that changed because the page really
|
|
91
|
+
changed is not noise. `--same-commit` compares only runs of the same commit,
|
|
92
|
+
`--runs a,b` names them, `--last N` sets how many runs are compared. Capture
|
|
93
|
+
the steps again after applying: the capture records the ignored regions.
|
|
@@ -23,7 +23,7 @@ file.
|
|
|
23
23
|
| Tool | Use it for |
|
|
24
24
|
|---|---|
|
|
25
25
|
| `list_runs` | published runs, newest first; filters `scenario`, `project`, `verdict`, `author`, `environment`, `branch`, `limit` |
|
|
26
|
-
| `get_run_summary` | one run: statuses per step and locale, criteria that need attention with evidence, screenshot URLs, verdict, baseline, canvas URL, open feedback |
|
|
26
|
+
| `get_run_summary` | one run: statuses per step and locale, criteria that need attention with evidence (semantic ones with score and the evaluator's one-sentence `reason` per locale and viewport), regions ignored in pixel diffs and regions seen moving (`ignored_regions`, `unstable_regions`), screenshot URLs, verdict, baseline, canvas URL, open feedback |
|
|
27
27
|
| `get_feedback` | run verdict with note, approved steps, unresolved rejections and comments, failed manual criteria (`run_id`, or `scenario` for its latest run) |
|
|
28
28
|
| `list_notifications` | the token owner's inbox (`unread_only`, `limit`) |
|
|
29
29
|
| `list_projects` | projects of the organization with counts; the slug is the `project` filter of `list_runs` and the value for `project:` in a scenario |
|
|
@@ -65,6 +65,8 @@ The same data and permissions as the REST API; each tool is its REST route.
|
|
|
65
65
|
|---|---|
|
|
66
66
|
| `get_draft` | which revision each step shows, step status, who changed it (person or agent) |
|
|
67
67
|
| `get_step` | the handoff: live files, signed entry URL, token set, page structure, open comments (`ref`: draft, design, vN, run id) |
|
|
68
|
+
| `get_step_spec` | what to build for a step as approved: elements with role, name and selector, styles mapped to tokens, states, contrast (`ref`: design by default, draft, vN, run id; `format` text or json) |
|
|
69
|
+
| `get_step_drift` | how an implementation run (`run`: id or `latest`) differs from the design reference, per element: expected token against the actual value |
|
|
68
70
|
| `get_step_history` | versions, and revisions of a step with authors |
|
|
69
71
|
| `list_step_changes` | steps that differ between two references (default design reference → draft) |
|
|
70
72
|
| `get_references`, `get_tokens` | design reference and baseline; the project's token set |
|
package/src/cli.mjs
CHANGED
|
@@ -17,7 +17,8 @@ import { newContext, accountFor, gateHeaders, gateOrigins, originMatcher, gateFo
|
|
|
17
17
|
import { configureGate } from './lib/linkcheck.mjs'
|
|
18
18
|
import { rememberEnvironment } from './lib/secrets.mjs'
|
|
19
19
|
import { evaluateRun, semanticPrefetch } from './lib/evaluate.mjs'
|
|
20
|
-
import { buildManifest, writePreview } from './lib/build.mjs'
|
|
20
|
+
import { buildManifest, writePreview, attentionLines } from './lib/build.mjs'
|
|
21
|
+
import { stabilityCmd } from './lib/stability.mjs'
|
|
21
22
|
import { localized, readJson, UserError, globToRegExp, failureStreak, expiryNote } from './lib/util.mjs'
|
|
22
23
|
import { packageRoot } from './lib/schema.mjs'
|
|
23
24
|
import { compareRuns, formatComparison } from './lib/compare.mjs'
|
|
@@ -35,6 +36,7 @@ import { htmlViewAllowed } from './lib/htmlview.mjs'
|
|
|
35
36
|
import { service, serviceUrl } from './lib/service.mjs'
|
|
36
37
|
import { login } from './lib/login.mjs'
|
|
37
38
|
import { initNextSteps, loginNextSteps } from './lib/onboarding.mjs'
|
|
39
|
+
import { specCmd, driftCmd } from './lib/spec-cli.mjs'
|
|
38
40
|
import { walkDesign, syncDesign, publishVersion, historyCmd, referenceCmd, tokensCmd, exportLive, localRevisions, branchCmd, uploadBlobs } from './lib/design-cli.mjs'
|
|
39
41
|
import { designDir, readBase, headKey } from './lib/design.mjs'
|
|
40
42
|
import { lintDesign, formatLint } from './lib/design-lint.mjs'
|
|
@@ -101,6 +103,8 @@ Run
|
|
|
101
103
|
verdicts and what failed (--no-evaluate skips that; kaloko evaluate redoes it)
|
|
102
104
|
[--items a,b] [--limit N] Stack steps (kind: stack, many pages of one template): only pages whose id or
|
|
103
105
|
path contains a, b; at most N pages (default items.limit, 500)
|
|
106
|
+
[--stability N] Take N screenshots of each capture a moment apart and note regions that move
|
|
107
|
+
(clocks, carousels); kaloko stability turns them into ignore suggestions
|
|
104
108
|
status Show what the current run has captured
|
|
105
109
|
Scenarios may declare "sessions" (people in the flow); walk keeps one browser context per session and locale
|
|
106
110
|
|
|
@@ -143,6 +147,10 @@ Results
|
|
|
143
147
|
Newest capture of every step as steps/<step>/<locale>/<viewport>.<ext> plus
|
|
144
148
|
index.json (documentation layer, Business plan); unchanged files are skipped
|
|
145
149
|
public [--run id] [--off] Make a shared run readable without sign-in (admin; testers may refresh an already public scenario)
|
|
150
|
+
stability [--last 4 | --runs a,b] [--same-commit] [--apply] [--json]
|
|
151
|
+
Regions that change on every run or move while the page is idle (clocks, ads,
|
|
152
|
+
carousels): ignore suggestions per step in <run>/stability.yml; --apply adds
|
|
153
|
+
them to the scenario (ignore:), and pixel diffs then skip them
|
|
146
154
|
compare [--run id] [--baseline id | --design] [--json]
|
|
147
155
|
Text diff of two runs: step statuses, criterion verdicts, scores and evidence
|
|
148
156
|
(default: current run against the scenario baseline, else the previous run)
|
|
@@ -160,6 +168,12 @@ Design (environments with kind: design; live HTML steps in a folder)
|
|
|
160
168
|
share [--name <text>] [--branch b] Publish the draft as a version of the flow (a run): "v4 · 3 changed, 17 unchanged"
|
|
161
169
|
history [--scenario id] [--step id] [--locale l] [--json]
|
|
162
170
|
Versions of the flow; revisions of a step with who (person or agent) and when
|
|
171
|
+
spec [step] [--ref design|draft|vN|<run id>] [--locale l] [--viewport v] [--local] [--json]
|
|
172
|
+
What to build for a step as approved: elements with role, name and selector, styles
|
|
173
|
+
mapped to DTCG tokens (or the nearest one), :hover/:focus/:active/:disabled, contrast
|
|
174
|
+
drift [step] [--ref design|vN] [--remote <run id|latest>] [--json]
|
|
175
|
+
How this implementation run differs from the approved design (expected token vs actual
|
|
176
|
+
value, raw values, missing elements and states); exit code 1 when something differs
|
|
163
177
|
reference [--scenario id] (--design|--baseline) (--run id|--version n|--off) [--branch b]
|
|
164
178
|
Show or set the design reference (accepted version) and the baseline
|
|
165
179
|
tokens pull|push|check|validate [--project p] [--file tokens.json|tokens.resolver.json] [--mode m] [--json]
|
|
@@ -704,7 +718,7 @@ const commands = {
|
|
|
704
718
|
return
|
|
705
719
|
}
|
|
706
720
|
const since = (await readCaptures(run.dir)).filter((c) => c.node === node.id).map((c) => c.at).pop()
|
|
707
|
-
const results = await captureStep({ page, node, locale, options: captureOptions(scenario, node), viewports: scopeViewports(run, list(args.viewports)), run, consoleFile: conn ? consoleLog(root, config.output, env.name) : null, maxChars: config.evaluators?.jev?.max_state_chars, since, htmlView: htmlViewAllowed(env, scenario) ? { scenarioId: scenario.id } : null })
|
|
721
|
+
const results = await captureStep({ page, node, locale, options: captureOptions(scenario, node), viewports: scopeViewports(run, list(args.viewports)), run, consoleFile: conn ? consoleLog(root, config.output, env.name) : null, maxChars: config.evaluators?.jev?.max_state_chars, since, htmlView: htmlViewAllowed(env, scenario) ? { scenarioId: scenario.id } : null, stability: Number(args.stability) || 0 })
|
|
708
722
|
for (const r of results) {
|
|
709
723
|
const failed = Object.entries(r.checks).filter(([, v]) => v.ok === false).map(([k]) => k)
|
|
710
724
|
say(`✓ ${node.id} ${locale} ${r.viewport} ${r.size.width}×${r.size.height}px ${failed.length ? `failing checks: ${failed.join(', ')}` : 'checks ok'}${r.blocked_requests.length ? ` blocked: ${r.blocked_requests.length}` : ''}`)
|
|
@@ -828,7 +842,7 @@ const commands = {
|
|
|
828
842
|
// the automatic capture runs unless this script invocation captured the step itself: a crop is an
|
|
829
843
|
// attachment (#49), and a capture from an earlier walk must not stand in for this one (#50)
|
|
830
844
|
let captured = false
|
|
831
|
-
const capture = (opts = {}) => { captured = true; return captureStep({ page: sPage, node, locale, options: captureOptions(scenario, node), viewports: scopeViewports(run, opts.viewports), run, htmlView: htmlViewAllowed(env, scenario) ? { scenarioId: scenario.id } : null, consoleFile: conn && !sess ? consoleLog(root, config.output, env.name) : null, maxChars: config.evaluators?.jev?.max_state_chars, extra: node.session ? { session: node.session } : {} }).then((r) => { prefetch?.add(r, run.dir); return r }) }
|
|
845
|
+
const capture = (opts = {}) => { captured = true; return captureStep({ page: sPage, node, locale, options: captureOptions(scenario, node), viewports: scopeViewports(run, opts.viewports), run, htmlView: htmlViewAllowed(env, scenario) ? { scenarioId: scenario.id } : null, consoleFile: conn && !sess ? consoleLog(root, config.output, env.name) : null, maxChars: config.evaluators?.jev?.max_state_chars, extra: node.session ? { session: node.session } : {}, stability: Number(args.stability) || 0 }).then((r) => { prefetch?.add(r, run.dir); return r }) }
|
|
832
846
|
await mod.default({ page: sPage, context: sCtx, locale, node: { id: node.id, title: localized(node.title, locale), kind: node.kind }, session: sessionInfo(node.session), url: (id = node.id) => urlFor(env, stepOf(scenario, id), locale), env, run: run.meta, capture, crop: (selector, title, o = {}) => captureCrop({ page: sPage, node, locale, selector, title, run, padding: Number(o.padding) || 0 }), acceptDialogs: () => acceptDialogs(sPage) })
|
|
833
847
|
if (!captured) await capture()
|
|
834
848
|
say(`✓ ${node.id} ${locale}`)
|
|
@@ -949,6 +963,7 @@ const commands = {
|
|
|
949
963
|
const report = await evaluateRun({ run, scenario, config, root })
|
|
950
964
|
const via = report.via.qawalk ? ` via Kaloko${report.hosted?.limit != null ? ` (${report.hosted.questions} of ${report.hosted.limit} questions this month)` : ''}` : report.via['own-key'] ? ' with your key' : ''
|
|
951
965
|
say(`Evaluated ${report.evaluated} capture(s); semantic evaluator ${report.model}${via}: ${report.semantic_calls} call(s), ${report.semantic_cached} cached.`)
|
|
966
|
+
if (report.vision?.asked) say(`Vision evaluator ${report.vision.model}: ${report.vision.asked} capture(s) with flagged or uncertain criteria, ${report.vision.calls} call(s), ${report.vision.cached} cached${report.vision.skipped.length ? `; not asked: ${[...new Set(report.vision.skipped.map((x) => x.split(': ').slice(1).join(': ')))].slice(0, 2).join('; ')}` : ''}.`)
|
|
952
967
|
if (report.skipped.length) {
|
|
953
968
|
// one line per reason: "12 captures: TYPESAFE_API_KEY is not set …"
|
|
954
969
|
const byReason = new Map()
|
|
@@ -964,9 +979,15 @@ const commands = {
|
|
|
964
979
|
const file = await writePreview({ run, manifest })
|
|
965
980
|
const s = manifest.summary
|
|
966
981
|
say(`${manifest.id}: ${s.pass} pass · ${s.partial} partial · ${s.fail} fail · ${s.skip} not checked\nPreview: ${file}`)
|
|
982
|
+
const why = attentionLines(manifest); if (why.length) say(`AI verdicts to act on (score and why):\n${why.join('\n')}`)
|
|
967
983
|
return file
|
|
968
984
|
},
|
|
969
985
|
|
|
986
|
+
async stability (args) {
|
|
987
|
+
const { config, root, run, scenario } = await runContext(args)
|
|
988
|
+
await stabilityCmd({ root, config, run, scenario, args, say })
|
|
989
|
+
},
|
|
990
|
+
|
|
970
991
|
async preview (args) {
|
|
971
992
|
const file = await commands.build(args)
|
|
972
993
|
say(pathToFileURL(file).href)
|
|
@@ -989,7 +1010,7 @@ const commands = {
|
|
|
989
1010
|
await writePreview({ run, manifest })
|
|
990
1011
|
const files = new Set()
|
|
991
1012
|
for (const n of manifest.nodes) {
|
|
992
|
-
for (const byVp of Object.values(n.captures || {})) for (const c of Object.values(byVp)) [c.image || c.png, c.thumb, c.html, c.eval, c.tree].filter(Boolean).forEach((p) => files.add(p))
|
|
1013
|
+
for (const byVp of Object.values(n.captures || {})) for (const c of Object.values(byVp)) [c.image || c.png, c.thumb, c.html, c.eval, c.tree, c.spec].filter(Boolean).forEach((p) => files.add(p))
|
|
993
1014
|
for (const a of n.attachments || []) files.add(a.file)
|
|
994
1015
|
// stack steps: the items index and every page's files
|
|
995
1016
|
if (n.stack?.items) {
|
|
@@ -1097,6 +1118,25 @@ const commands = {
|
|
|
1097
1118
|
await historyCmd({ config, scenarioId, client: service(config), args, say })
|
|
1098
1119
|
},
|
|
1099
1120
|
|
|
1121
|
+
async spec (args) {
|
|
1122
|
+
const { config, root } = await context(args)
|
|
1123
|
+
let run = null
|
|
1124
|
+
try { run = await resolveRun({ root, output: config.output, run: args.run }) } catch {}
|
|
1125
|
+
const scenario = run ? await loadScenario(resolve(root, run.meta.scenario_file)).catch(() => null) : typeof args.scenario === 'string' && /\.ya?ml$/.test(args.scenario) ? await loadScenario(resolve(args.scenario)) : null
|
|
1126
|
+
const scenarioId = await scenarioIdOf(args, config, root)
|
|
1127
|
+
let client = null
|
|
1128
|
+
if (!args.local) { try { client = service(config) } catch (e) { if (!run) throw e } }
|
|
1129
|
+
await specCmd({ config, client, run, scenario, scenarioId, args, say })
|
|
1130
|
+
},
|
|
1131
|
+
|
|
1132
|
+
async drift (args) {
|
|
1133
|
+
const { config, root } = await context(args)
|
|
1134
|
+
let run = null
|
|
1135
|
+
try { run = await resolveRun({ root, output: config.output, run: args.run }) } catch {}
|
|
1136
|
+
const scenarioId = await scenarioIdOf(args, config, root)
|
|
1137
|
+
await driftCmd({ config, client: service(config), run, scenarioId, args, say })
|
|
1138
|
+
},
|
|
1139
|
+
|
|
1100
1140
|
async reference (args) {
|
|
1101
1141
|
const { config, root } = await context(args)
|
|
1102
1142
|
const scenarioId = await scenarioIdOf(args, config, root)
|