kaloko 0.18.0 → 0.19.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kaloko",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "Your AI agents build; Kaloko walks every screen, puts it on one canvas and records who approved what.",
5
5
  "keywords": [
6
6
  "qa",
@@ -33,10 +33,28 @@
33
33
  "minProperties": 1,
34
34
  "additionalProperties": { "$ref": "#/$defs/environment" }
35
35
  },
36
- "evaluators": {
36
+ "evaluate": {
37
37
  "type": "object",
38
38
  "additionalProperties": false,
39
+ "description": "Settings of every evaluation (kaloko evaluate and the end of kaloko walk).",
40
+ "properties": {
41
+ "budget": {
42
+ "type": "object",
43
+ "additionalProperties": false,
44
+ "description": "Upper bound of one evaluation (#158). Steps go first, then the semantic sample of each stack, chosen by priority (pages failing a deterministic acceptance check, then those checked longest ago) unless the step says semantic_order: walk; what is past the budget stays not_evaluated with a note.",
45
+ "properties": {
46
+ "calls": { "type": "integer", "minimum": 0, "description": "Uncached requests to JEV (your key or through Kaloko) per evaluation, escalations included; requests to your own openai_compatible evaluator are not counted. With a budget, screens without semantic criteria get no quick signals unless already cached. kaloko evaluate --budget N overrides it." }
47
+ }
48
+ }
49
+ }
50
+ },
51
+ "evaluators": {
52
+ "type": "object",
53
+ "description": "Text evaluators (jev, and your own of kind openai_compatible under any other name), the vision evaluator and triage.",
54
+ "additionalProperties": { "$ref": "#/$defs/openaiEvaluator" },
39
55
  "properties": {
56
+ "text": { "type": "string", "default": "jev", "description": "The evaluator that answers the semantic questions first: jev, or the name of an openai_compatible evaluator here (a local model in Ollama or LM Studio). Its verdicts are marked via: own-model on the canvas, in the PR comment and in acceptance records." },
57
+ "escalate_to": { "const": "jev", "description": "Answers of your own evaluator (evaluators.text) that are not clear enough for a verdict (between the fail and pass thresholds, or missing) are asked again of JEV (your key or through Kaloko, counted in evaluate.budget). Only your model escalates to JEV, never the other way. Quick signals are not escalated." },
40
58
  "jev": {
41
59
  "type": "object",
42
60
  "additionalProperties": false,
@@ -201,6 +219,23 @@
201
219
  "images": { "type": "object", "additionalProperties": false, "properties": { "viewport": { "type": "string" }, "locale": { "type": "string", "description": "reader (the reader's language) or a locale." } } }
202
220
  }
203
221
  },
222
+ "openaiEvaluator": {
223
+ "type": "object",
224
+ "additionalProperties": false,
225
+ "required": ["kind", "base_url", "model"],
226
+ "description": "Your own text evaluator behind an OpenAI-compatible chat completions API (#158): Ollama (http://localhost:11434/v1), LM Studio (http://localhost:1234/v1), vLLM or a gateway. Used only when evaluators.text or evaluators.escalate_to names it; the page state goes only to base_url.",
227
+ "properties": {
228
+ "kind": { "const": "openai_compatible" },
229
+ "base_url": { "type": "string", "description": "The API root with /v1; requests go to {base_url}/chat/completions." },
230
+ "model": { "type": "string", "description": "The model as the server names it (qwen3:8b, gemma3:12b)." },
231
+ "api_key_env": { "type": "string", "description": "Variable with a key sent as a bearer token; a local server needs none." },
232
+ "concurrency": { "type": "integer", "minimum": 1, "maximum": 16, "default": 2, "description": "Requests in flight at once; a local model usually answers one at a time." },
233
+ "max_state_chars": { "type": "integer", "minimum": 1000, "default": 24000, "description": "The page text is cut to this length (the top of the page): small models have small context windows." },
234
+ "timeout_ms": { "type": "integer", "minimum": 1000, "default": 120000 },
235
+ "temperature": { "type": "number", "minimum": 0, "maximum": 2, "default": 0 },
236
+ "response_format": { "enum": ["json_schema", "json_object", "none"], "default": "json_schema", "description": "How the answer is held to JSON; json_object or none for servers without structured output." }
237
+ }
238
+ },
204
239
  "record": {
205
240
  "description": "Recordings of walked steps (opt-in): true or step records each step from the previous screen to its capture; flow records the whole walk per locale with a chapter per step; failed keeps only steps that fail; false or off turns it off. Uploaded on Business and Enterprise.",
206
241
  "oneOf": [
@@ -441,6 +441,40 @@
441
441
  "null"
442
442
  ],
443
443
  "description": "Version of the application under test (git tag on HEAD or --app-version); documentation captures carry it."
444
+ },
445
+ "site": {
446
+ "type": "object",
447
+ "description": "Site checks (#152, scenario health.site), once per site and run: robots.txt, sitemaps, llms.txt, http→https and www redirects. Advisory: health shows a finding per failed check (pack site, step site:<host>); they never decide a verdict.",
448
+ "properties": {
449
+ "origins": {
450
+ "type": "array",
451
+ "items": {
452
+ "type": "object",
453
+ "required": ["origin", "checks"],
454
+ "properties": {
455
+ "origin": { "type": "string" },
456
+ "checked_at": { "type": "string" },
457
+ "checks": {
458
+ "type": "array",
459
+ "items": {
460
+ "type": "object",
461
+ "required": ["id", "verdict"],
462
+ "properties": {
463
+ "id": { "type": "string", "description": "HC1101 … HC1107." },
464
+ "text": { "type": "string" },
465
+ "verdict": { "enum": ["pass", "fail", "not_evaluated"] },
466
+ "level": { "enum": ["error", "warning"] },
467
+ "evidence": { "type": ["string", "null"] },
468
+ "na": { "type": "boolean", "description": "Nothing to check here (no robots.txt to read, no other host name)." },
469
+ "details": { "type": "array", "items": { "type": "string" } },
470
+ "help": { "type": "object" }
471
+ }
472
+ }
473
+ }
474
+ }
475
+ }
476
+ }
477
+ }
444
478
  }
445
479
  },
446
480
  "$defs": {
@@ -665,7 +699,9 @@
665
699
  "counts": { "type": "object", "description": "Stack steps: pages per verdict." },
666
700
  "evidence": { "type": ["string", "null"] },
667
701
  "check": { "type": "object" },
668
- "help": { "type": "object" }
702
+ "help": { "type": "object" },
703
+ "cross_page": { "type": "boolean", "description": "Stack steps (#151): a check across the stack's pages (HC121–HC126: duplicates, canonicals, hreflang pairs, sitemap pages). counts says how many pages are affected; it cannot be adopted as a criterion." },
704
+ "urls": { "type": "array", "items": { "type": "string" }, "description": "Cross-page checks: the first affected addresses (at most 20); the stack's items.json has every group." }
669
705
  }
670
706
  }
671
707
  },
@@ -724,6 +760,16 @@
724
760
  "string",
725
761
  "null"
726
762
  ]
763
+ },
764
+ "rotation": {
765
+ "type": "object",
766
+ "description": "Rotating sample (items.rotate), per locale: mode (week or run), week or run, offset, limit, total, rounds (runs to cover every page), covered, never (pages not checked yet) and oldest_checked.",
767
+ "additionalProperties": { "type": "object" }
768
+ },
769
+ "cross_page": {
770
+ "type": "array",
771
+ "description": "Cross-page checks of a stack with the seo pack (#151): id, verdict, pages compared and pages affected. items.json has them in full under cross_page (groups with the affected addresses) and marks each affected page with cross_fails.",
772
+ "items": { "type": "object" }
727
773
  }
728
774
  }
729
775
  }
@@ -94,7 +94,23 @@
94
94
  "additionalProperties": false,
95
95
  "description": "Health (CS Kondice): the checks of packs are advisory, shown apart from acceptance and never part of a verdict. acceptance lists the packs whose checks still count for acceptance in this scenario, as they did before health was split off (the design pack always does). To make one check count, adopt it: kaloko health adopt <step>:<check> (e.g. home:HC103) adds the criterion AC103 with from: HC103.",
96
96
  "properties": {
97
- "acceptance": { "type": "array", "uniqueItems": true, "items": { "enum": ["seo", "usability", "a11y", "perf", "console", "errors", "app", "design"] } }
97
+ "acceptance": { "type": "array", "uniqueItems": true, "items": { "enum": ["seo", "usability", "a11y", "perf", "console", "errors", "app", "design", "aeo", "locale"] } },
98
+ "site": {
99
+ "description": "Site checks (#152), once per site and run: robots.txt, the sitemaps it lists, llms.txt, and http→https and www redirects in one hop. Advisory (health, pack site, HC11xx); off unless set, because they make requests of their own. true for the defaults, or an object.",
100
+ "oneOf": [
101
+ { "type": "boolean" },
102
+ {
103
+ "type": "object",
104
+ "additionalProperties": false,
105
+ "properties": {
106
+ "paths": { "type": "array", "maxItems": 50, "items": { "type": "string", "pattern": "^/" }, "description": "Paths robots.txt must not block for search engines (default: / and the start path of each walked language)." },
107
+ "ai_crawlers": { "type": "boolean", "description": "false skips HC1103 (AI crawlers blocked in robots.txt) when blocking them is the site's decision. Default true." },
108
+ "sitemaps": { "type": "integer", "minimum": 1, "maximum": 50, "description": "Most sitemap files to fetch per site, index children included (default and most 50)." },
109
+ "environments": { "type": "array", "minItems": 1, "items": { "type": "string" }, "description": "Run the site checks only on these environments (e.g. [production]). Default: every environment; off production HC1102 is a warning and HC1103 is not evaluated." }
110
+ }
111
+ }
112
+ ]
113
+ }
98
114
  }
99
115
  },
100
116
  "tags": {
@@ -344,6 +360,10 @@
344
360
  "items": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9_-]{0,39}$", "not": { "enum": ["index"] } },
345
361
  "description": "States this step is captured in besides the default one, e.g. [empty, error, loading]; any name works. Each state is one more capture per viewport (keys <viewport>.state-<name>) and a value of the State switcher on the canvas. Design steps render <step>/<state>.html (or <step>/<locale>/<state>.html), else <step>/index.html?state=<name>; walks of web and app steps run the step script once more per state with the argument state. `default` may be listed; it is always captured."
346
362
  },
363
+ "expect_status": {
364
+ "oneOf": [{ "type": "integer", "minimum": 100, "maximum": 599 }, { "type": "array", "minItems": 1, "items": { "type": "integer", "minimum": 100, "maximum": 599 } }],
365
+ "description": "The HTTP status this step is expected to answer with, e.g. 404 for a missing page. A status of 400 or more marks an error page: the errors pack checks HC601 and HC605 only on error pages (this, or a 4xx/5xx answer)."
366
+ },
347
367
  "page": {
348
368
  "enum": ["landing", "work", "list", "settings", "form", "doc"],
349
369
  "description": "What kind of page this step is (see the scenario's page); the usability pack asks per type, e.g. one primary call to action only on landing pages."
@@ -359,10 +379,12 @@
359
379
  "console",
360
380
  "errors",
361
381
  "app",
362
- "design"
382
+ "design",
383
+ "aeo",
384
+ "locale"
363
385
  ]
364
386
  },
365
- "description": "Check packs whose checks are added to this node. Health checks (advisory): HC1xx seo, HC2xx usability, HC3xx a11y, HC4xx perf, HC5xx console, HC6xx errors, HC7xx app. AC8xx design: checks against the design reference, always acceptance. A pack listed in health.acceptance adds its checks as acceptance criteria with the same number (HC302 → AC302)."
387
+ "description": "Check packs whose checks are added to this node. Health checks (advisory): HC1xx seo, HC2xx usability, HC3xx a11y, HC4xx perf, HC5xx console, HC6xx errors, HC7xx app, HC9xx aeo, HC10xx locale. AC8xx design: checks against the design reference, always acceptance. A pack listed in health.acceptance adds its checks as acceptance criteria with the same number (HC302 → AC302)."
366
388
  },
367
389
  "capture": {
368
390
  "$ref": "#/$defs/capture"
@@ -394,7 +416,7 @@
394
416
  "items": {
395
417
  "type": "object",
396
418
  "additionalProperties": false,
397
- "description": "Stack steps: which pages of one template the step covers. One source: urls, file, sitemap (with match/exclude), crawl or script.",
419
+ "description": "Stack steps: which pages of one template the step covers. One source: urls, file, sitemap or robots (with match/exclude), crawl or script.",
398
420
  "properties": {
399
421
  "urls": {
400
422
  "type": "array",
@@ -405,19 +427,23 @@
405
427
  },
406
428
  "file": {
407
429
  "type": "string",
408
- "description": "Text (one path or URL per line), CSV (first column) or JSON array of paths/URLs or {path,title}; relative to the scenario"
430
+ "description": "Text (one path or URL per line), CSV (first column) or JSON array of paths/URLs or {path, title, key, purpose, edit_url, content_ref}; relative to the scenario"
409
431
  },
410
432
  "sitemap": {
411
433
  "type": "string",
412
- "description": "Sitemap path or URL (sitemap indexes are followed one level)"
434
+ "description": "Sitemap path or URL, gzipped (.gz) or not; sitemap indexes are followed two levels. `robots` reads the Sitemap: lines of /robots.txt (see robots)."
435
+ },
436
+ "robots": {
437
+ "type": "boolean",
438
+ "description": "The sitemaps listed in /robots.txt (Sitemap: lines); when their URLs name the walked locale (/sitemaps/de/…), only those of that locale"
413
439
  },
414
440
  "match": {
415
- "type": "string",
416
- "description": "Regex on the path: keep matching pages"
441
+ "oneOf": [{ "type": "string" }, { "type": "object", "minProperties": 1, "additionalProperties": { "type": "string" } }],
442
+ "description": "Regex on the path: keep matching pages. A map by locale ({ default: '^/eshop/', fi: '^/fi/tuotteet/' }) gives each locale its own regex (the locale, its language, then default). Applies to every source before rotate and limit."
417
443
  },
418
444
  "exclude": {
419
- "type": "string",
420
- "description": "Regex on the path: drop matching pages"
445
+ "oneOf": [{ "type": "string" }, { "type": "object", "minProperties": 1, "additionalProperties": { "type": "string" } }],
446
+ "description": "Regex on the path: drop matching pages. A string or a map by locale like match."
421
447
  },
422
448
  "crawl": {
423
449
  "type": "object",
@@ -449,7 +475,15 @@
449
475
  },
450
476
  "script": {
451
477
  "type": "string",
452
- "description": "Module relative to the scenario: default export async ({ page, env, locale, url }) => [{ path | url, title?, key? }]"
478
+ "description": "Module relative to the scenario: default export async ({ page, env, locale, url, node, params }) => [{ path | url, title?, key?, purpose?, edit_url?, content_ref? }]. node: { id, title, lane, lane_title, session }; params: items.params. edit_url (the page in the CMS) and content_ref (its record id) show in the stack index and in kaloko export findings."
479
+ },
480
+ "resolve": {
481
+ "type": "string",
482
+ "description": "Module relative to the scenario that links each walked page to its source: default export async (item, { env, locale, node, params, url }) => ({ edit_url?, content_ref? }). Called for the walked pages only (after match, rotate, limit and walk --items), 15 s per page; a page it fails on is walked without the link."
483
+ },
484
+ "params": {
485
+ "type": "object",
486
+ "description": "Values passed to items.script as params, so one script serves several stacks (for example { family: magazine })."
453
487
  },
454
488
  "limit": {
455
489
  "type": "integer",
@@ -457,11 +491,19 @@
457
491
  "maximum": 10000,
458
492
  "description": "At most this many pages (default 500)"
459
493
  },
494
+ "rotate": {
495
+ "enum": ["week", "run"],
496
+ "description": "Rotating sample: each run takes limit pages starting at a stable offset, the ISO week (week) or the number of runs so far in this output folder (run), so the whole list is covered over several runs."
497
+ },
460
498
  "semantic_sample": {
461
499
  "type": "integer",
462
500
  "minimum": 0,
463
501
  "maximum": 10000,
464
- "description": "Semantic criteria are evaluated on the first N pages only (default 20); deterministic ones on every page"
502
+ "description": "Semantic criteria are evaluated on N pages only (default 20), in the order of semantic_order; deterministic ones on every page"
503
+ },
504
+ "semantic_order": {
505
+ "enum": ["walk", "priority"],
506
+ "description": "Which pages make the semantic sample. walk (default without an evaluator budget): the first N in walk order. priority (default with evaluate.budget): pages failing a deterministic acceptance criterion first, then those never or least recently checked (last_checked of a rotating sample), then walk order."
465
507
  },
466
508
  "viewports": {
467
509
  "type": "array",
@@ -492,7 +534,7 @@
492
534
  },
493
535
  "from": {
494
536
  "type": "string",
495
- "description": "title | h1 | description | canonical | lang | words | http | url | path | a CSS selector (text), selector@attribute, or count:selector"
537
+ "description": "title | h1 | description | canonical | lang | words | http | url | path | jsonld_types (the @type values of the JSON-LD blocks) | og:title, twitter:card or meta:<name> (a meta tag's content) | a CSS selector (text), selector@attribute, or count:selector"
496
538
  }
497
539
  }
498
540
  }
@@ -649,6 +691,16 @@
649
691
  "link_home",
650
692
  "links_ok",
651
693
  "hreflang_pairs",
694
+ "llms_txt",
695
+ "nojs_parity",
696
+ "jsonld_present",
697
+ "jsonld_valid",
698
+ "aeo_faq_schema",
699
+ "aeo_answer_first",
700
+ "locale_lang",
701
+ "locale_hreflang",
702
+ "locale_text",
703
+ "locale_prices",
652
704
  "app_touch_targets",
653
705
  "app_no_truncation",
654
706
  "app_no_crash",
@@ -687,6 +739,11 @@
687
739
  "attr": {
688
740
  "type": "string"
689
741
  },
742
+ "types": {
743
+ "type": "array",
744
+ "items": { "type": "string", "minLength": 1 },
745
+ "description": "jsonld_present: schema.org types the page's structured data must contain (Product, BreadcrumbList, FAQPage…)."
746
+ },
690
747
  "property": {
691
748
  "type": "string",
692
749
  "minLength": 1,
@@ -759,7 +816,13 @@
759
816
  "ratio": {
760
817
  "type": "number",
761
818
  "minimum": 0,
762
- "description": "design_pixels: maximum share of differing pixels (default 0.03)."
819
+ "description": "design_pixels: maximum share of differing pixels (default 0.03); nojs_parity: the least share of the rendered body text the raw HTML must hold (default 0.3)."
820
+ },
821
+ "user_agent": {
822
+ "type": "string",
823
+ "minLength": 1,
824
+ "maxLength": 300,
825
+ "description": "nojs_parity: fetch the raw HTML as this crawler, without cookies, and respect its robots.txt rules: gptbot, oai-searchbot, claudebot, perplexitybot, googlebot, bingbot, or a whole user agent string. Without it, the walker's own browser user agent and session, and robots.txt is not read."
763
826
  },
764
827
  "follow": {
765
828
  "type": "boolean",
@@ -918,6 +981,10 @@
918
981
  "type": "boolean",
919
982
  "description": "Screenshot of the whole page (default true; false for stack pages: the first screen only)"
920
983
  },
984
+ "screenshot": {
985
+ "enum": ["full", "thumb"],
986
+ "description": "thumb keeps only the small card thumbnail (640 px wide) as the step's image: for stacks of hundreds of pages where the checks read the page, not the picture (SEO crawls). Default full."
987
+ },
921
988
  "record": { "$ref": "#/$defs/record" },
922
989
  "trace": { "type": "boolean", "default": true, "description": "Keep a trace per walked step (actions, network requests, console) shown under Technical; false or walk --no-trace turns it off." }
923
990
  }
@@ -43,8 +43,15 @@ health checks (ids `HC1xx` for `seo`, `HC2xx` for `usability`; acceptance
43
43
  criteria are `AC`, health checks `HC`, and the number is the check's: the
44
44
  same check kept in acceptance or adopted is `AC103` for `HC103`) at load time:
45
45
  `seo` checks one H1, title and description length, canonical, hreflang for
46
- every locale, Open Graph, indexing, viewport, `html lang`, overflow and
47
- console; `usability` checks unlabeled inputs and link texts and asks the
46
+ every locale, Open Graph, indexing, viewport, `html lang`, overflow,
47
+ console and structured data (HC112 JSON-LD present, HC113 every block valid);
48
+ `aeo` (HC9xx) adds answer-engine signals: FAQPage/QAPage where the page asks
49
+ and answers, an answer-first paragraph under the H1, `/llms.txt`, a meta
50
+ description, the raw HTML as GPTBot gets it without JavaScript (HC905,
51
+ `nojs_parity`; `user_agent: claudebot|googlebot|…` and `ratio` on a criterion
52
+ of your own); `locale` (HC10xx, scenarios with `locales`) checks html lang,
53
+ the own hreflang, the text's language and price decimals against the locale;
54
+ `usability` checks unlabeled inputs and link texts and asks the
48
55
  evaluator questions that fit the step's page type. Pack checks are health,
49
56
  not acceptance: they are evaluated on every walk but never decide a step's
50
57
  verdict (see [evaluation.md#health](evaluation.md#health)). The `design` pack
@@ -122,13 +129,20 @@ English words; one plain word such as a brand name is left alone).
122
129
  - a plain-language message with a way forward (semantic);
123
130
  - no console errors. The document's own error status is not counted.
124
131
 
132
+ HC601 and HC605 apply only to error pages: a step with `expect_status`
133
+ (400 or more, or an own `http_status` criterion of 400 or more) or a page
134
+ that answered 4xx/5xx. On other pages they are not applicable, so keep the
135
+ pack off normal pages and stacks (`kaloko validate` warns on a stack).
136
+
125
137
  ## Error pages
126
138
 
127
139
  Every site has them and they are easy to forget. Give each one its own
128
140
  step with `packs: [errors]`:
129
141
 
130
- - **404:** use `path: /no-such-page-{random}`. `{random}` becomes a new
131
- token on every walk, so no cache or an accidentally created page makes it
142
+ - **404:** use `path: /no-such-page-{random}` and `expect_status: 404`, so a
143
+ soft 404 (HTTP 200) fails HC601 instead of being not applicable.
144
+ `{random}` becomes a new token on every walk, so no cache or an
145
+ accidentally created page makes it
132
146
  pass. `kaloko draft` adds this step on its own.
133
147
  - **Exact status:** override HC601 with your own AC601, for example
134
148
  `{ id: AC601, text: Answers 410, check: { type: deterministic, assert: http_status, value: 410 } }`.
@@ -27,6 +27,48 @@ report with the reason `kaloko evaluate` gives.
27
27
  The default evaluator is text-only and most accurate in English. It is weak
28
28
  at numbers, which is why those are checked deterministically.
29
29
 
30
+ ## Budget and your own model
31
+
32
+ On large sites the semantic questions are what costs money. Three settings keep it in hand:
33
+
34
+ - **Budget.** `evaluate.budget.calls` in `kaloko.config.yml` (or `kaloko evaluate --budget N`, `kaloko walk
35
+ --budget N`) caps the uncached JEV calls of one evaluation, the walk's own questions included. Ordinary steps
36
+ go first, then each stack's sample. What does not fit stays `not_evaluated` with the note "the evaluator
37
+ budget of N call(s) is used up", and `kaloko evaluate` prints how many captures it left out. Cached answers
38
+ are free, so the next evaluation continues where this one stopped. With a budget, screens without semantic
39
+ criteria get no new quick signals. Report the skipped count; never call those criteria passed.
40
+ - **Sample order.** A stack asks the semantic questions on `semantic_sample` pages (default 20). With a budget,
41
+ or `items.semantic_order: priority` on the step, they are chosen by priority: pages failing a deterministic
42
+ acceptance criterion first (health checks do not count), then pages never or least recently checked
43
+ (`last_checked` of a rotating sample), then the rest in walk order. Otherwise, and with `semantic_order: walk`,
44
+ the first pages in walk order. The same captures always give the same sample and spend the budget on the same
45
+ pages.
46
+ - **Your own model.** An evaluator of `kind: openai_compatible` (Ollama, LM Studio, vLLM) answers the same
47
+ questions from the same page text, for free and without the page leaving the machine. It is opt-in:
48
+ `evaluators.text: <name>` makes it answer first. With `evaluators.escalate_to: jev` its answers that are not
49
+ clear enough for a verdict (between the thresholds, a missing answer, or anything but a number from 0 to 1)
50
+ are asked again of JEV, counted in the budget; the quick signals are not escalated. Escalation goes only that
51
+ way, never from JEV to your model. Small models are rougher judges, so treat their verdicts as a first pass and
52
+ keep JEV for escalation. After three failed connections in a row an evaluation stops asking your model.
53
+
54
+ ```yaml
55
+ evaluate:
56
+ budget: { calls: 300 }
57
+ evaluators:
58
+ ollama:
59
+ kind: openai_compatible
60
+ base_url: http://localhost:11434/v1 # LM Studio: http://localhost:1234/v1
61
+ model: qwen3:8b
62
+ text: ollama
63
+ escalate_to: jev
64
+ ```
65
+
66
+ Every text verdict then names who decided it, with `via`, `model` and `prompt_version`: `openai-compatible`
67
+ via `own-model` for your model, `typesafe-jev` for an escalated answer with the local score kept as
68
+ `first_score`. The canvas, acceptance records and the PR comment show your model's verdicts as such; say so in the
69
+ report too, and never present them as JEV's. Pages are not batched into one
70
+ request: each page state is one request, cached by its content, which is what makes re-evaluation free.
71
+
30
72
  ## Vision and reasons
31
73
 
32
74
  Every semantic verdict carries one sentence, `reason`, shown as "Why" next to
@@ -71,7 +113,8 @@ evaluator saw the screenshot, say so.
71
113
  ## Health
72
114
 
73
115
  The checks of packs (`seo`, `a11y`, `perf`, `usability`, `console`,
74
- `errors`, `app`) are the run's **health** (CS *kondice*): advisory, stored in
116
+ `errors`, `app`, `aeo`, `locale`), the cross-page checks of stacks with
117
+ `seo` and the site checks of `health.site` are the run's **health** (CS *kondice*): advisory, stored in
75
118
  `node.health` and `summary.health` of `run.json`, never in `criteria`. They
76
119
  do not change a step's status, the run verdict, the review queue, carried
77
120
  approvals, required approvers, the acceptance record or exit codes. The
@@ -77,6 +77,12 @@ kaloko schedule hook <id> # URL + secret that start the run after a deploy
77
77
  and the `health.worse` webhook on a new error-level finding. Each schedule
78
78
  sets it to `error` (default), `warning` or `off` on kaloko.app or with
79
79
  `kaloko schedule add --health-alert`; changing it is the person's call.
80
+ - Health for dashboards: every shared run with health checks gets counts per
81
+ locale, step, check and pack (`GET …/runs/<id>/health/aggregate`,
82
+ `…/scenarios/<id>/latest/health/aggregate`) and a diff against the previous
83
+ run per page (`…/health/diff`: new, fixed, still open, first seen). A
84
+ `read:health` token reads these and nothing else; the `health.changed`
85
+ webhook says when something is new or fixed. help/reference/health-api.md.
80
86
  - Does not work for: native apps, local or private addresses (localhost, IPs,
81
87
  `.local`), header credentials, base URLs per locale.
82
88
  - For those, or for the full walk on a schedule, write a GitHub Actions
@@ -52,6 +52,8 @@ is null. Projects and products come complete, without pages.
52
52
  | `add_sample_run` | adds the sample run (or returns the one there is); needs an upload or full token and the Developer / Tester role |
53
53
  | `search_screens` | the screen library: newest screenshot of every step across the organization's flows (`q` words matched against titles, ids, paths, URLs, page title and H1; `project`, `product`, `module`, `scenario`, `locale`, `viewport`, `device`, `status`, `environment`; `source: design` or `all` for design versions; `group: none` for every locale and viewport; `cursor` from `next`), each with a signed image and the canvas link |
54
54
  | `get_health` | the advisory health of a run (`run_id`), the newest run of a scenario (`scenario`) or the trend of a project (`project`): findings by severity, pack and step with evidence and a fix, and what is new or fixed since the previous run; never a verdict, see [evaluation.md#health](evaluation.md#health) |
55
+ | `get_health_aggregate` | health counted for a dashboard (`run_id`, or `scenario` and `environment` for its newest run; `by` locale, step, check, pack; filters; `top` pages per group): pass / fail / unsure / not evaluated per group, the most affected pages, and new / fixed / still open since the previous run; group ids are stable across runs |
56
+ | `get_health_diff` | per page, locale and check what is new, fixed or still open since the previous run (`state` picks one list), each with `first_seen` (run and date); ids `step\|locale\|check\|page` are stable across runs |
55
57
  | `adopt_health_check` | turns a health check into an acceptance criterion of the scenario (`run_id`, `step`, `check` such as `HC103`, `text` to reword it; it becomes `AC103` with `from: HC103`); Developer / Tester role. Only when the person asks; `kaloko health pull` then writes it into the scenario file |
56
58
  | `get_affected_steps` | which steps of a scenario changed files reach, with the reason per step (`scenario`, `changed_files`, `run_id`, `upstream`); route files are matched against the step paths and URLs of the newest run. Imports and `affects:` globs need the repository: `kaloko affected` follows them |
57
59
 
@@ -17,6 +17,15 @@ sorting, thumbnails and CSV export, and each page opens in the lightbox.
17
17
  per page, and export the failing ones for the content team.
18
18
  - A redesign or migration: walk the old URL list against the new site.
19
19
 
20
+ For a weekly SEO and answer-engine monitor of a public site, start with
21
+ `npx kaloko init --seo <site>`: it reads robots.txt and the sitemaps, finds
22
+ the languages and the biggest content families and writes
23
+ `qa/flows/seo-<site>.yml` with one rotating stack per family (`match` per
24
+ language, packs seo and aeo). Read the comment at the top of the file:
25
+ families it paired across languages by size are guesses to check. Then
26
+ `kaloko validate`, `kaloko walk --limit 20`, and
27
+ `kaloko schedule export github`. See help/how-to/seo-monitor.md.
28
+
20
29
  Not for a flow of different screens: those stay separate steps. A stack
21
30
  step does not click through anything. It opens each page by URL. Use a
22
31
  step script only to prepare each page, for example to dismiss a dialog
@@ -31,10 +40,13 @@ step script only to prepare each page, for example to dismiss a dialog
31
40
  purpose: Every article reads well and is findable.
32
41
  packs: [seo] # health checks of every page (advisory)
33
42
  items:
34
- sitemap: /sitemap.xml # or urls / file / crawl / script (exactly one)
43
+ sitemap: /sitemap.xml # or robots: true / urls / file / crawl / script (exactly one)
35
44
  match: '^/clanky/' # regex on the path with its query; exclude: '…' drops pages
45
+ # or a map by locale: { default: '^/blog/', fi: '^/fi/blogi/' }
36
46
  limit: 800 # default 500, at most 10000
37
- semantic_sample: 20 # semantic criteria on the first 20 pages only
47
+ # rotate: week # each ISO week the next `limit` pages (run: each run)
48
+ semantic_sample: 20 # semantic criteria on 20 pages only
49
+ # semantic_order: priority # failing and least recently checked pages first
38
50
  # viewports: [desktop] # default: the first viewport of the scenario
39
51
  fields: # index columns (max 12); default title, h1, words, http
40
52
  - { id: title, label: Title, from: title }
@@ -55,12 +67,43 @@ Item sources:
55
67
  - `file: qa/data/products.csv` is a list next to the scenario. Accepted
56
68
  formats are text with one path per line, CSV (first column), or JSON:
57
69
  `["/a"]` or `[{ path, title, key, purpose }]`, where `key` gives a stable id.
58
- - `sitemap: /sitemap.xml` also follows a sitemap index (up to 50 sitemaps).
70
+ - `sitemap: /sitemap.xml` also follows a sitemap index, two levels deep
71
+ (up to 50 sitemaps each). Gzipped sitemaps work: a `.gz` URL or gzip
72
+ bytes behind any URL are unpacked.
73
+ - `sitemap: robots` (or `robots: true`) reads the `Sitemap:` lines of
74
+ `/robots.txt` on the environment's origin. When their URLs name the
75
+ walked locale as a segment (`/sitemaps/de/sitemap.xml.gz`,
76
+ `sitemap-de.xml`; `en_US` finds `en-us`, `en` does not), only that
77
+ locale's sitemaps are read; otherwise all of them. Use it when a site
78
+ lists one sitemap per language, so the scenario does not hard-code paths.
59
79
  - `crawl: { from: /clanky, links: "a.card", next: "a.next", pages: 20 }`
60
80
  collects links from a listing and follows its pagination.
61
81
  - `script: qa/steps/items.mjs` uses the default export
62
- `({ page, env, locale, url }) => [paths | { path, title, key, purpose }]` for
63
- anything else, such as an API or a search.
82
+ `({ page, env, locale, url, node, params }) => [paths | { path, title, key, purpose }]`
83
+ for anything else, such as an API or a search. `node` is
84
+ `{ id, title, lane, lane_title, session }` of the step and `params` is
85
+ `items.params`, so one script can serve several stacks.
86
+
87
+ Fix handoff: a script or a JSON list may give each page `edit_url` (the
88
+ page in the CMS, http(s) only) and `content_ref` (its record id for a
89
+ content API). `resolve: steps/resolve.mjs` (default export
90
+ `async (item, { env, locale, node, params, url }) => ({ edit_url, content_ref })`)
91
+ looks them up for the walked pages only, 15 s per page; a page it fails on is walked
92
+ without them and the walk says how many. They show in the page detail
93
+ ("Edit in CMS"), the stack CSV, `get_stack_items` and
94
+ `kaloko export findings`.
95
+
96
+ `match` and `exclude` apply to every source before `limit` (and before
97
+ `rotate`). Either is a regex or a map by locale; the walked locale, then its
98
+ language (`en` for `en_US`), then `default` is used. A `match` map without
99
+ the locale and without `default` is an error.
100
+
101
+ `rotate: week` (or `rotate: run`) walks a big list in turns: each ISO week
102
+ (or each run) takes the next `limit` pages, wrapping around, so the list is
103
+ covered in `pages ÷ limit` rounds. `run` keeps its count and each page's last
104
+ check in `.cache/stack/` of the output folder, so use `week` on CI with a
105
+ fresh checkout. The stack index gets `rotation` per locale (window, covered,
106
+ never checked) and each page `last_checked` (its previous check, or null).
64
107
 
65
108
  An item's `purpose` (for example its title and meta description) is what the
66
109
  semantic evaluator reads as `screen_purpose` for that page instead of the
@@ -73,11 +116,44 @@ in the wrong language.
73
116
  Pages are deduplicated. External URLs are allowed (e.g. an old domain).
74
117
  `{locale}` in any source string is replaced by the walked locale. This is
75
118
  how one sitemap with every language is split:
76
- `match: '^/guides/.*lang={locale}'`, or `sitemap: /{locale}/sitemap.xml`.
119
+ `match: '^/guides/.*lang={locale}'`, or `sitemap: /{locale}/sitemap.xml`,
120
+ or `sitemap: /sitemaps/{locale}/sitemap.xml.gz`. One content family per
121
+ stack: the same per-locale sitemap with `match: '/eshop/'` in one step and
122
+ `match: '/(magazin|magazine|revista)/'` in another.
77
123
 
78
124
  Field sources: `title`, `h1`, `description`, `canonical`, `lang`, `url`,
79
125
  `path`, `words` (words in main/article/body), `http` (the navigation
80
- status), `count:<selector>`, `<selector>` (text) and `<selector>@<attr>`.
126
+ status), `jsonld_types` (the schema.org types of the page's JSON-LD, for
127
+ example `Product, Offer, BreadcrumbList`; `invalid×1` when a block does not
128
+ parse), a meta tag by name (`og:title`, `og:image`, `twitter:card`,
129
+ `meta:robots`), `count:<selector>`, `<selector>` (text) and
130
+ `<selector>@<attr>`.
131
+
132
+ Structured data is also summarised without a field: every capture records
133
+ `structured_data` (blocks, invalid blocks, types), and `items.json` carries
134
+ `structured_data` per page and, at the top, pages per type, pages without
135
+ structured data and pages with invalid JSON-LD. The `seo` pack checks it
136
+ (HC112 present, HC113 valid); `packs: [aeo]` adds the answer-engine signals
137
+ (HC901–HC905; HC905 `nojs_parity` fetches each page's raw HTML as GPTBot,
138
+ respecting robots.txt, and fails when the title, canonical, H1, JSON-LD or
139
+ most of the text appear only after JavaScript). See
140
+ help/reference/checks-and-packs.md.
141
+
142
+ With the `seo` pack a stack also compares its pages with each other after the
143
+ walk (HC121–HC126, advisory): shared titles, descriptions and H1s within a
144
+ language, canonicals pointing elsewhere or to error/noindex pages, hreflang
145
+ that the other walked version does not answer, and (sitemap or robots
146
+ sources) listed pages that answer non-200, redirect or say noindex. They are
147
+ health entries of the stack node (`cross_page: true`, `counts` in pages,
148
+ `urls`); `items.json` has them under `cross_page` with the groups, and each
149
+ affected page lists the ids in `cross_fails`. They cannot be adopted.
150
+ `packs: [locale]` (scenarios with `locales`) checks html lang, the own
151
+ hreflang, the text's language and price decimals against the walked locale
152
+ (HC1001–HC1004). `health: { site: true }` on the scenario adds site checks
153
+ once per run (robots.txt, sitemaps, llms.txt, http→https and www redirects;
154
+ HC1101–HC1107, `site.json` in the run, `site` in the manifest). Off the
155
+ `production` environment HC1102 is a warning and HC1103 is skipped;
156
+ `health.site.environments: [production]` runs them there only.
81
157
 
82
158
  A step `execution.script` still works. It is called with `item` in the
83
159
  context, opens `item.url` itself and may call `capture()`. Without a script,
@@ -86,19 +162,28 @@ the walk opens each page and captures it.
86
162
  ## Cost and size: choose deliberately
87
163
 
88
164
  - Deterministic criteria and packs run on every page. They are cheap.
89
- - Semantic criteria go to the evaluator. On a stack they run only on the
90
- first `semantic_sample` pages (default 20). The other pages show the
91
- criterion as not evaluated. Raise the sample only when the question
165
+ - Semantic criteria go to the evaluator. On a stack they run only on
166
+ `semantic_sample` pages (default 20), the first in walk order. With
167
+ `semantic_order: priority`, or an evaluator budget, pages failing a
168
+ deterministic acceptance criterion come first, then those never or least
169
+ recently checked. The other pages show the criterion as not evaluated. `evaluate.budget.calls`
170
+ caps the calls of a whole evaluation (evaluation.md). Raise the sample only when the question
92
171
  really differs between pages, and tell the person what it costs
93
172
  (one call per page and viewport).
94
173
  - Stacks capture the first screen of each page (fast, small). Set
95
174
  `capture: { full_page: true }` when the criteria are about the whole page.
96
175
  - Walking takes about 1–2 s per page. For a first look, use
97
176
  `kaloko walk --limit 50`. Re-walk chosen pages with
98
- `--items clanek-7,clanek-12` (matches the id or the path).
99
- - A shared run holds at most 20000 files and 300 MB. Each page stores about
100
- four files, so a stack of a few thousand first-screen pages fits. Split
101
- larger catalogues by section (`match`), one stack per section.
177
+ `--items clanek-7,clanek-12` (matches the id or the path; picked from
178
+ every matching page, also outside this week's rotation, and such a
179
+ partial walk leaves the rotation state alone).
180
+ - A shared run holds at most 20000 files and 300 MB as stored (HTML and JSON
181
+ travel gzip-compressed, about a tenth of their size on disk). Each page
182
+ stores about four files, so a stack of a few thousand first-screen pages
183
+ fits. For SEO crawls where the checks read the page, not the picture, set
184
+ `capture: { screenshot: thumb }`: only the 640 px thumbnail is kept. Split
185
+ larger catalogues by section (`match`) or walk a rotating sample
186
+ (`rotate: week`).
102
187
 
103
188
  ## Reading the results
104
189
 
@@ -118,6 +203,11 @@ the walk opens each page and captures it.
118
203
  `get_stack_items {run_id, step, status?, criterion?, query?, offset, limit}`
119
204
  lists pages with fields, failing criteria, evidence and screenshot URLs.
120
205
  This is the way to hand an agent "fix these 37 articles".
206
+ - `kaloko export findings [--format csv|json] [--locale l] [--step s] [--out file]`
207
+ writes the fix list of the local run: one row per failing criterion or
208
+ health check, page and locale, with URL, evidence, `edit_url`,
209
+ `content_ref` and `recheck` (`kaloko walk --steps s --items <page> --locales l`,
210
+ which re-walks just that page after the fix).
121
211
 
122
212
  ## Report
123
213