kaloko 0.18.0 → 0.20.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.
Files changed (44) hide show
  1. package/package.json +1 -1
  2. package/schemas/config.v1.json +47 -1
  3. package/schemas/run.v1.json +57 -1
  4. package/schemas/scenario.v1.json +100 -14
  5. package/skills/kaloko/reference/acceptance.md +18 -4
  6. package/skills/kaloko/reference/evaluation.md +44 -1
  7. package/skills/kaloko/reference/integrations.md +30 -1
  8. package/skills/kaloko/reference/mcp.md +3 -0
  9. package/skills/kaloko/reference/stacks.md +181 -16
  10. package/src/cli.mjs +191 -37
  11. package/src/lib/build.mjs +37 -7
  12. package/src/lib/capture.mjs +31 -9
  13. package/src/lib/config.mjs +15 -1
  14. package/src/lib/crawler.mjs +242 -0
  15. package/src/lib/draft.mjs +2 -0
  16. package/src/lib/evaluate.mjs +124 -38
  17. package/src/lib/findings.mjs +98 -0
  18. package/src/lib/health-core.mjs +28 -5
  19. package/src/lib/health-findings.mjs +116 -0
  20. package/src/lib/health-index.mjs +202 -0
  21. package/src/lib/health.mjs +4 -0
  22. package/src/lib/integrations-cli.mjs +6 -4
  23. package/src/lib/jev.mjs +34 -14
  24. package/src/lib/linkcheck.mjs +59 -22
  25. package/src/lib/local-evaluator.mjs +270 -0
  26. package/src/lib/manifest-core.mjs +115 -14
  27. package/src/lib/pack-help.mjs +26 -0
  28. package/src/lib/packs.mjs +41 -3
  29. package/src/lib/pii.mjs +2 -0
  30. package/src/lib/pr.mjs +5 -1
  31. package/src/lib/probe.mjs +62 -1
  32. package/src/lib/recheck.mjs +5 -1
  33. package/src/lib/scenario.mjs +16 -3
  34. package/src/lib/seo-core.mjs +400 -0
  35. package/src/lib/seo-init.mjs +272 -0
  36. package/src/lib/service.mjs +31 -2
  37. package/src/lib/site.mjs +362 -0
  38. package/src/lib/stack-core.mjs +244 -0
  39. package/src/lib/stack.mjs +291 -51
  40. package/src/lib/traffic-core.mjs +289 -0
  41. package/src/lib/traffic.mjs +149 -0
  42. package/src/lib/upload.mjs +3 -2
  43. package/src/lib/variants-core.mjs +198 -0
  44. package/viewer/index.html +49 -24
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kaloko",
3
- "version": "0.18.0",
3
+ "version": "0.20.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,
@@ -106,6 +124,17 @@
106
124
  }
107
125
  }
108
126
  },
127
+ "traffic": {
128
+ "type": "object",
129
+ "additionalProperties": false,
130
+ "description": "Traffic data per page (#157), opt-in: Search Console clicks, impressions, CTR and position and GA4 sessions and conversions, joined by URL onto stack pages (items.json traffic) and the rows of kaloko export findings (impact, sorted by it); with the seo pack a stack also gets HC127 (high impressions, low CTR). Nothing is requested unless gsc or ga4 is set. A scenario's traffic block replaces the config's.",
131
+ "properties": {
132
+ "file": { "oneOf": [{ "type": "string" }, { "type": "array", "minItems": 1, "items": { "type": "string" } }], "description": "CSV exports of Search Console (Pages) or GA4 (landing pages), English or Czech headers; a path relative to the file that names it." },
133
+ "gsc": { "type": "object", "additionalProperties": false, "required": ["property"], "properties": { "property": { "type": "string", "minLength": 1, "description": "The Search Console property: sc-domain:example.com or https://www.example.com/." }, "credentials_env": { "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]*$", "description": "Variable with the path of the service account JSON key (or the JSON itself). Default GOOGLE_APPLICATION_CREDENTIALS." }, "days": { "type": "integer", "minimum": 1, "maximum": 480, "description": "Days of data, default 28 (ending two days ago; Search Console data comes in late)." } }, "description": "Search Console API (searchanalytics.query by page) with a service account added to the property as a user. Requested once per run and cached in the run folder .cache." },
134
+ "ga4": { "type": "object", "additionalProperties": false, "required": ["property"], "properties": { "property": { "type": "string", "minLength": 1, "pattern": "^(properties/)?[0-9]+$", "description": "The GA4 property id (123456789 or properties/123456789)." }, "credentials_env": { "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]*$", "description": "Variable with the path of the service account JSON key (or the JSON itself). Default GOOGLE_APPLICATION_CREDENTIALS." }, "days": { "type": "integer", "minimum": 1, "maximum": 480, "description": "Days of data, default 28 (ending yesterday)." } }, "description": "GA4 Data API (runReport: sessions and key events by landing page) with a service account added to the property as a viewer. Requested once per run and cached in the run folder .cache." },
135
+ "low_ctr": { "type": "object", "additionalProperties": false, "description": "HC127 thresholds: a page with at least min_impressions impressions fails when its CTR is below ratio x the site's median CTR at similar positions (1-3, 4-10, 11-20).", "properties": { "min_impressions": { "type": "integer", "minimum": 1, "default": 100 }, "ratio": { "type": "number", "exclusiveMinimum": 0, "maximum": 1, "default": 0.5 } } }
136
+ }
137
+ },
109
138
  "walk": {
110
139
  "type": "object",
111
140
  "additionalProperties": false,
@@ -201,6 +230,23 @@
201
230
  "images": { "type": "object", "additionalProperties": false, "properties": { "viewport": { "type": "string" }, "locale": { "type": "string", "description": "reader (the reader's language) or a locale." } } }
202
231
  }
203
232
  },
233
+ "openaiEvaluator": {
234
+ "type": "object",
235
+ "additionalProperties": false,
236
+ "required": ["kind", "base_url", "model"],
237
+ "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.",
238
+ "properties": {
239
+ "kind": { "const": "openai_compatible" },
240
+ "base_url": { "type": "string", "description": "The API root with /v1; requests go to {base_url}/chat/completions." },
241
+ "model": { "type": "string", "description": "The model as the server names it (qwen3:8b, gemma3:12b)." },
242
+ "api_key_env": { "type": "string", "description": "Variable with a key sent as a bearer token; a local server needs none." },
243
+ "concurrency": { "type": "integer", "minimum": 1, "maximum": 16, "default": 2, "description": "Requests in flight at once; a local model usually answers one at a time." },
244
+ "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." },
245
+ "timeout_ms": { "type": "integer", "minimum": 1000, "default": 120000 },
246
+ "temperature": { "type": "number", "minimum": 0, "maximum": 2, "default": 0 },
247
+ "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." }
248
+ }
249
+ },
204
250
  "record": {
205
251
  "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
252
  "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; HC1005: pages without their own version in a walked locale, with items.variants). 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,26 @@
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
+ "variants": {
770
+ "type": "object",
771
+ "description": "Locale variants (items.variants: hreflang): mode, source_locale, and per other locale the number of source pages without their own version there (missing) and of versions several source pages share (shared). items.json lists them under variants.locales.",
772
+ "properties": {
773
+ "mode": { "type": "string" },
774
+ "source_locale": { "type": ["string", "null"] },
775
+ "missing": { "type": "object", "additionalProperties": { "type": "integer" } },
776
+ "shared": { "type": "object", "additionalProperties": { "type": "integer" } }
777
+ }
778
+ },
779
+ "cross_page": {
780
+ "type": "array",
781
+ "description": "Cross-page checks of a stack with the seo pack (#151), and HC1005 with locale variants: 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.",
782
+ "items": { "type": "object" }
727
783
  }
728
784
  }
729
785
  }
@@ -94,7 +94,34 @@
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
+ }
114
+ }
115
+ },
116
+ "traffic": {
117
+ "type": "object",
118
+ "additionalProperties": false,
119
+ "description": "Traffic data per page (#157), opt-in: Search Console clicks, impressions, CTR and position and GA4 sessions and conversions, joined by URL onto stack pages (items.json traffic) and the rows of kaloko export findings (impact, sorted by it); with the seo pack a stack also gets HC127 (high impressions, low CTR). Nothing is requested unless gsc or ga4 is set. A scenario's traffic block replaces the config's.",
120
+ "properties": {
121
+ "file": { "oneOf": [{ "type": "string" }, { "type": "array", "minItems": 1, "items": { "type": "string" } }], "description": "CSV exports of Search Console (Pages) or GA4 (landing pages), English or Czech headers; a path relative to the file that names it." },
122
+ "gsc": { "type": "object", "additionalProperties": false, "required": ["property"], "properties": { "property": { "type": "string", "minLength": 1, "description": "The Search Console property: sc-domain:example.com or https://www.example.com/." }, "credentials_env": { "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]*$", "description": "Variable with the path of the service account JSON key (or the JSON itself). Default GOOGLE_APPLICATION_CREDENTIALS." }, "days": { "type": "integer", "minimum": 1, "maximum": 480, "description": "Days of data, default 28 (ending two days ago; Search Console data comes in late)." } }, "description": "Search Console API (searchanalytics.query by page) with a service account added to the property as a user. Requested once per run and cached in the run folder .cache." },
123
+ "ga4": { "type": "object", "additionalProperties": false, "required": ["property"], "properties": { "property": { "type": "string", "minLength": 1, "pattern": "^(properties/)?[0-9]+$", "description": "The GA4 property id (123456789 or properties/123456789)." }, "credentials_env": { "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]*$", "description": "Variable with the path of the service account JSON key (or the JSON itself). Default GOOGLE_APPLICATION_CREDENTIALS." }, "days": { "type": "integer", "minimum": 1, "maximum": 480, "description": "Days of data, default 28 (ending yesterday)." } }, "description": "GA4 Data API (runReport: sessions and key events by landing page) with a service account added to the property as a viewer. Requested once per run and cached in the run folder .cache." },
124
+ "low_ctr": { "type": "object", "additionalProperties": false, "description": "HC127 thresholds: a page with at least min_impressions impressions fails when its CTR is below ratio x the site's median CTR at similar positions (1-3, 4-10, 11-20).", "properties": { "min_impressions": { "type": "integer", "minimum": 1, "default": 100 }, "ratio": { "type": "number", "exclusiveMinimum": 0, "maximum": 1, "default": 0.5 } } }
98
125
  }
99
126
  },
100
127
  "tags": {
@@ -344,6 +371,10 @@
344
371
  "items": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9_-]{0,39}$", "not": { "enum": ["index"] } },
345
372
  "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
373
  },
374
+ "expect_status": {
375
+ "oneOf": [{ "type": "integer", "minimum": 100, "maximum": 599 }, { "type": "array", "minItems": 1, "items": { "type": "integer", "minimum": 100, "maximum": 599 } }],
376
+ "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)."
377
+ },
347
378
  "page": {
348
379
  "enum": ["landing", "work", "list", "settings", "form", "doc"],
349
380
  "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 +390,12 @@
359
390
  "console",
360
391
  "errors",
361
392
  "app",
362
- "design"
393
+ "design",
394
+ "aeo",
395
+ "locale"
363
396
  ]
364
397
  },
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)."
398
+ "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
399
  },
367
400
  "capture": {
368
401
  "$ref": "#/$defs/capture"
@@ -394,7 +427,7 @@
394
427
  "items": {
395
428
  "type": "object",
396
429
  "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.",
430
+ "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
431
  "properties": {
399
432
  "urls": {
400
433
  "type": "array",
@@ -405,19 +438,23 @@
405
438
  },
406
439
  "file": {
407
440
  "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"
441
+ "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
442
  },
410
443
  "sitemap": {
411
444
  "type": "string",
412
- "description": "Sitemap path or URL (sitemap indexes are followed one level)"
445
+ "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)."
446
+ },
447
+ "robots": {
448
+ "type": "boolean",
449
+ "description": "The sitemaps listed in /robots.txt (Sitemap: lines); when their URLs name the walked locale (/sitemaps/de/…), only those of that locale"
413
450
  },
414
451
  "match": {
415
- "type": "string",
416
- "description": "Regex on the path: keep matching pages"
452
+ "oneOf": [{ "type": "string" }, { "type": "object", "minProperties": 1, "additionalProperties": { "type": "string" } }],
453
+ "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
454
  },
418
455
  "exclude": {
419
- "type": "string",
420
- "description": "Regex on the path: drop matching pages"
456
+ "oneOf": [{ "type": "string" }, { "type": "object", "minProperties": 1, "additionalProperties": { "type": "string" } }],
457
+ "description": "Regex on the path: drop matching pages. A string or a map by locale like match."
421
458
  },
422
459
  "crawl": {
423
460
  "type": "object",
@@ -449,7 +486,15 @@
449
486
  },
450
487
  "script": {
451
488
  "type": "string",
452
- "description": "Module relative to the scenario: default export async ({ page, env, locale, url }) => [{ path | url, title?, key? }]"
489
+ "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."
490
+ },
491
+ "resolve": {
492
+ "type": "string",
493
+ "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."
494
+ },
495
+ "params": {
496
+ "type": "object",
497
+ "description": "Values passed to items.script as params, so one script serves several stacks (for example { family: magazine })."
453
498
  },
454
499
  "limit": {
455
500
  "type": "integer",
@@ -457,11 +502,27 @@
457
502
  "maximum": 10000,
458
503
  "description": "At most this many pages (default 500)"
459
504
  },
505
+ "variants": {
506
+ "enum": ["hreflang"],
507
+ "description": "Locale variants of one page: the list, limit and rotation come from items.source_locale, and every other locale walks each page's own hreflang alternate for that locale (from the page's HTML head, its Link header or the sitemap's xhtml:link). The item id comes from the source page, so a page and its translations are one item; a page without a version in a locale is reported (HC1005) instead of dropped silently. x-default never stands for a locale."
508
+ },
509
+ "source_locale": {
510
+ "type": "string",
511
+ "description": "With variants: the locale whose list is walked (default: the scenario's first locale). One of the scenario's locales."
512
+ },
513
+ "rotate": {
514
+ "enum": ["week", "run"],
515
+ "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."
516
+ },
460
517
  "semantic_sample": {
461
518
  "type": "integer",
462
519
  "minimum": 0,
463
520
  "maximum": 10000,
464
- "description": "Semantic criteria are evaluated on the first N pages only (default 20); deterministic ones on every page"
521
+ "description": "Semantic criteria are evaluated on N pages only (default 20), in the order of semantic_order; deterministic ones on every page"
522
+ },
523
+ "semantic_order": {
524
+ "enum": ["walk", "priority"],
525
+ "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
526
  },
466
527
  "viewports": {
467
528
  "type": "array",
@@ -492,7 +553,7 @@
492
553
  },
493
554
  "from": {
494
555
  "type": "string",
495
- "description": "title | h1 | description | canonical | lang | words | http | url | path | a CSS selector (text), selector@attribute, or count:selector"
556
+ "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
557
  }
497
558
  }
498
559
  }
@@ -649,6 +710,16 @@
649
710
  "link_home",
650
711
  "links_ok",
651
712
  "hreflang_pairs",
713
+ "llms_txt",
714
+ "nojs_parity",
715
+ "jsonld_present",
716
+ "jsonld_valid",
717
+ "aeo_faq_schema",
718
+ "aeo_answer_first",
719
+ "locale_lang",
720
+ "locale_hreflang",
721
+ "locale_text",
722
+ "locale_prices",
652
723
  "app_touch_targets",
653
724
  "app_no_truncation",
654
725
  "app_no_crash",
@@ -687,6 +758,11 @@
687
758
  "attr": {
688
759
  "type": "string"
689
760
  },
761
+ "types": {
762
+ "type": "array",
763
+ "items": { "type": "string", "minLength": 1 },
764
+ "description": "jsonld_present: schema.org types the page's structured data must contain (Product, BreadcrumbList, FAQPage…)."
765
+ },
690
766
  "property": {
691
767
  "type": "string",
692
768
  "minLength": 1,
@@ -759,7 +835,13 @@
759
835
  "ratio": {
760
836
  "type": "number",
761
837
  "minimum": 0,
762
- "description": "design_pixels: maximum share of differing pixels (default 0.03)."
838
+ "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)."
839
+ },
840
+ "user_agent": {
841
+ "type": "string",
842
+ "minLength": 1,
843
+ "maxLength": 300,
844
+ "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
845
  },
764
846
  "follow": {
765
847
  "type": "boolean",
@@ -918,6 +1000,10 @@
918
1000
  "type": "boolean",
919
1001
  "description": "Screenshot of the whole page (default true; false for stack pages: the first screen only)"
920
1002
  },
1003
+ "screenshot": {
1004
+ "enum": ["full", "thumb"],
1005
+ "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."
1006
+ },
921
1007
  "record": { "$ref": "#/$defs/record" },
922
1008
  "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
1009
  }
@@ -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
@@ -72,13 +72,42 @@ kaloko schedule hook <id> # URL + secret that start the run after a deploy
72
72
  script that only calls `page.goto`). Steps that click or type are left out and
73
73
  named in the answer. Criteria decided: `exists`, `absent`, `count`, `text_*`,
74
74
  `attr_*`, `title_matches`, `meta_matches`; the rest shows "not evaluated".
75
+ - Stack steps: pages from `items.urls`, `items.sitemap` or `items.robots`
76
+ (gzip and indexes too) with `match`/`exclude` (string or per locale), `limit`
77
+ and `rotate: week`, picked like `kaloko walk` picks them. Read without
78
+ screenshots (one `/scrape` per page in the step's first viewport): the
79
+ criteria above, index fields (title, h1, description, words, jsonld_types,
80
+ meta and selector fields; `http` stays empty) and `items.json` on the canvas.
81
+ Read over several 15-minute ticks (150 pages each), at most 1,000 stack pages
82
+ per run and 5 × limit sitemap addresses per locale; each page counts against
83
+ the monthly budget, and a spent budget waits up to a day, then shares with a
84
+ note. Run now, hooks and edits queue behind a run under way. Patterns: ≤ 300
85
+ characters, no backreferences or lookaround. `crawl`, `script` and `file` sources, item scripts and
86
+ `items.resolve` edit links need the full walk.
87
+ - Base URL per locale: an environment with `base_url: { default: …, cs: … }`
88
+ is scheduled as it is; each locale opens its own address, else `default`.
89
+ `kaloko schedule list` shows stack progress and the last run's new and fixed
90
+ health results.
75
91
  - Health alerts: a hosted run (and a run shared from CI) compares its health
76
92
  with the previous run of the scenario and alerts the followers, the channels
77
93
  and the `health.worse` webhook on a new error-level finding. Each schedule
78
94
  sets it to `error` (default), `warning` or `off` on kaloko.app or with
79
95
  `kaloko schedule add --health-alert`; changing it is the person's call.
96
+ - Health for dashboards: every shared run with health checks gets counts per
97
+ locale, step, check and pack (`GET …/runs/<id>/health/aggregate`,
98
+ `…/scenarios/<id>/latest/health/aggregate`) and a diff against the previous
99
+ run per page (`…/health/diff`: new, fixed, still open, first seen). A
100
+ `read:health` token reads these and nothing else; the `health.changed`
101
+ webhook says when something is new or fixed. help/reference/health-api.md.
102
+ - Findings: `GET …/scenarios/<id>/findings` (MCP `list_findings`) groups a
103
+ scenario's open results by check, site, locale and step, with a status
104
+ people set (acknowledged, ignored with a reason) and fixed when no page
105
+ fails. A scenario may open one Jira / Linear issue per new finding
106
+ (`PUT …/findings/sync { tracker, min_severity }`, Developer / Tester): at
107
+ most 20 per run, a comment on big changes, closed when fixed. Switching it
108
+ on or ignoring findings is the team's call, not an agent's.
80
109
  - Does not work for: native apps, local or private addresses (localhost, IPs,
81
- `.local`), header credentials, base URLs per locale.
110
+ `.local`), header credentials, stacks listed by crawl, script or file.
82
111
  - For those, or for the full walk on a schedule, write a GitHub Actions
83
112
  workflow instead: `kaloko schedule export github --scenario <file> --env <name>
84
113
  [--every week] [--at 5]`. It names the repository secrets it needs.
@@ -52,6 +52,9 @@ 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 |
57
+ | `list_findings` | a scenario's open results grouped into findings (`scenario`, `environment`; filters `locale`, `step`, `check`, `status`): fingerprint `check\|site\|locale\|step`, severity, pages affected, up to 10 sample pages with `edit_url`, first / last seen, status (open, acknowledged, ignored with a reason, fixed) and the tracker issue. Read-only: fix the worst first; acknowledging or ignoring is the team's call |
55
58
  | `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
59
  | `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
60