kaloko 0.14.0 → 0.15.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/README.md +6 -2
- package/package.json +1 -1
- package/schemas/evidence.v1.json +257 -0
- package/schemas/run.v1.json +67 -1
- package/schemas/scenario.v1.json +34 -1
- package/skills/kaloko/SKILL.md +23 -4
- package/skills/kaloko/reference/acceptance.md +37 -28
- package/skills/kaloko/reference/apps.md +1 -1
- package/skills/kaloko/reference/design.md +2 -1
- package/skills/kaloko/reference/evaluation.md +42 -1
- package/skills/kaloko/reference/evidence.md +80 -0
- package/skills/kaloko/reference/integrations.md +6 -1
- package/skills/kaloko/reference/mcp.md +3 -0
- package/skills/kaloko/reference/stacks.md +1 -1
- package/src/cli.mjs +68 -8
- package/src/lib/build.mjs +10 -1
- package/src/lib/capture.mjs +7 -0
- package/src/lib/cli-args.mjs +1 -0
- package/src/lib/draft.mjs +1 -1
- package/src/lib/evaluate.mjs +7 -2
- package/src/lib/evidence-core.mjs +300 -0
- package/src/lib/health-core.mjs +205 -0
- package/src/lib/health.mjs +219 -0
- package/src/lib/integrations-cli.mjs +3 -1
- package/src/lib/jev.mjs +4 -2
- package/src/lib/manifest-core.mjs +23 -10
- package/src/lib/pack-help.mjs +53 -51
- package/src/lib/packs.mjs +67 -55
- package/src/lib/pr.mjs +2 -0
- package/src/lib/run.mjs +13 -1
- package/src/lib/signoff.mjs +86 -6
- package/templates/scenario.yml +1 -1
- package/viewer/index.html +541 -43
|
@@ -39,28 +39,35 @@ sharpen each purpose and add the criteria the page must really meet.
|
|
|
39
39
|
## Check packs
|
|
40
40
|
|
|
41
41
|
`packs: [seo]` or `packs: [seo, usability]` on a node adds a fixed set of
|
|
42
|
-
|
|
42
|
+
health checks (ids `HC1xx` for `seo`, `HC2xx` for `usability`; acceptance
|
|
43
|
+
criteria are `AC`, health checks `HC`, and the number is the check's: the
|
|
44
|
+
same check kept in acceptance or adopted is `AC103` for `HC103`) at load time:
|
|
43
45
|
`seo` checks one H1, title and description length, canonical, hreflang for
|
|
44
46
|
every locale, Open Graph, indexing, viewport, `html lang`, overflow and
|
|
45
47
|
console; `usability` checks unlabeled inputs and link texts and asks the
|
|
46
|
-
evaluator questions that fit the step's page type.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
for
|
|
48
|
+
evaluator questions that fit the step's page type. Pack checks are health,
|
|
49
|
+
not acceptance: they are evaluated on every walk but never decide a step's
|
|
50
|
+
verdict (see [evaluation.md#health](evaluation.md#health)). The `design` pack
|
|
51
|
+
always counts for acceptance; `health: { acceptance: [seo] }` on the scenario
|
|
52
|
+
keeps another pack in acceptance, which only the person decides. A node's own
|
|
53
|
+
criterion with the same id wins and counts for acceptance (that is what
|
|
54
|
+
`kaloko health adopt` writes). Pack checks do not count as drift in
|
|
55
|
+
`--coverage`; the plan lists only what the brief asks for. Use packs for
|
|
56
|
+
public pages and landings; task flows keep their own criteria.
|
|
50
57
|
|
|
51
58
|
Set `page` on every step (or once on the scenario) so the usability pack
|
|
52
59
|
asks the right questions:
|
|
53
60
|
|
|
54
61
|
| `page` | for | asks |
|
|
55
62
|
| --- | --- | --- |
|
|
56
|
-
| `landing` | public and marketing pages | purpose in the first screen (`
|
|
57
|
-
| `work` | app screens where people work: overview, canvas, detail, inbox item | the heading says what the screen is for (`
|
|
58
|
-
| `list` | lists and tables: runs, projects, inbox | `
|
|
59
|
-
| `settings` | settings, billing, tokens | `
|
|
60
|
-
| `form` | sign-up, sign-in, a form to fill in | `
|
|
61
|
-
| `doc` | documentation, guides, terms | `
|
|
62
|
-
|
|
63
|
-
Every type also gets `
|
|
63
|
+
| `landing` | public and marketing pages | purpose in the first screen (`HC201`), exactly one primary call to action (`HC202`) |
|
|
64
|
+
| `work` | app screens where people work: overview, canvas, detail, inbox item | the heading says what the screen is for (`HC205`) |
|
|
65
|
+
| `list` | lists and tables: runs, projects, inbox | `HC205`, each item says what it is and how to open it (`HC207`) |
|
|
66
|
+
| `settings` | settings, billing, tokens | `HC205`, each setting says what it changes and how it is saved (`HC208`) |
|
|
67
|
+
| `form` | sign-up, sign-in, a form to fill in | `HC205`, how to send it and what happens next (`HC206`) |
|
|
68
|
+
| `doc` | documentation, guides, terms | `HC201`, headings and short paragraphs (`HC209`) |
|
|
69
|
+
|
|
70
|
+
Every type also gets `HC203` and `HC204`. Without `page` a step with the
|
|
64
71
|
`seo` pack, or any step of a scenario allowed on production, is `landing`;
|
|
65
72
|
everything else is `work`. Do not put `landing` on signed-in screens: "one
|
|
66
73
|
primary call to action" is a marketing rule, and a work screen rightly offers
|
|
@@ -78,34 +85,35 @@ When a scenario walks several locales, a text check (`text_matches`,
|
|
|
78
85
|
reads in one language only (it looks for diacritics and common Czech or
|
|
79
86
|
English words; one plain word such as a brand name is left alone).
|
|
80
87
|
|
|
81
|
-
- `a11y` (`
|
|
88
|
+
- `a11y` (`HC3xx`): images with alt, links and buttons with a name, labelled
|
|
82
89
|
fields, heading levels not skipped, WCAG AA text contrast, a `main`
|
|
83
|
-
landmark, unique ids (`
|
|
84
|
-
screenshot the walk also presses Tab through the page: `
|
|
90
|
+
landmark, unique ids (`HC301`–`HC307`, read from the page). After the
|
|
91
|
+
screenshot the walk also presses Tab through the page: `HC308` tab order
|
|
85
92
|
(hidden focus, big jumps back, positive `tabindex`, controls left out or
|
|
86
|
-
mouse-only), `
|
|
87
|
-
screenshotted focused and unfocused), `
|
|
88
|
-
dialog may keep focus), `
|
|
89
|
-
stops, `
|
|
90
|
-
reader announces). `
|
|
91
|
-
`
|
|
93
|
+
mouse-only), `HC309` visible focus with at least 3 : 1 contrast (each stop
|
|
94
|
+
screenshotted focused and unfocused), `HC310` no keyboard trap (a modal
|
|
95
|
+
dialog may keep focus), `HC311` a skip link or main content within three
|
|
96
|
+
stops, `HC312` names from Chrome's accessibility tree (what a screen
|
|
97
|
+
reader announces). `HC313` landmarks (one `main`, labelled repeats) and
|
|
98
|
+
`HC314` ARIA (unknown roles and attributes, references to missing ids,
|
|
92
99
|
`aria-hidden` over focusable content) are read from the page. The first
|
|
93
100
|
five focus stops are kept as screenshots and the canvas can draw the tab
|
|
94
101
|
order on the capture. The walk only moves focus: nothing is clicked or
|
|
95
102
|
typed, and the page is left unfocused at the top. Stack steps walk the
|
|
96
103
|
keyboard on their first five pages. Still a subset of WCAG 2.2 AA; it
|
|
97
104
|
does not replace an audit with a screen reader.
|
|
98
|
-
- `perf` (`
|
|
105
|
+
- `perf` (`HC4xx`): LCP ≤ 2.5 s, CLS ≤ 0.1, TTFB ≤ 800 ms, page weight ≤ 3 MB,
|
|
99
106
|
measured in this walk's browser since the page load (lab values: the
|
|
100
107
|
machine and network of the walk matter; compare runs, not absolutes).
|
|
101
|
-
Override a limit with an own criterion
|
|
108
|
+
Override a limit with an own criterion with the check's number as an AC
|
|
109
|
+
id (`AC401` for `HC401`), e.g.
|
|
102
110
|
`{ id: AC401, text: LCP within 4 s, check: { type: deterministic, assert: perf_lcp, max: 4000 } }`.
|
|
103
|
-
- `console` (`
|
|
111
|
+
- `console` (`HC501`): the evaluator reads the step's console errors with its
|
|
104
112
|
purpose and decides whether they touch what the step accepts (pass: only
|
|
105
113
|
third-party noise; needs review: unsure; fail: relevant). Without errors it
|
|
106
114
|
passes without a call. Keep `no_console_errors` where any error must fail.
|
|
107
115
|
|
|
108
|
-
- `errors` (`
|
|
116
|
+
- `errors` (`HC6xx`) is for error pages. It checks:
|
|
109
117
|
- a real 4xx/5xx status (a "soft 404" answering 200 fails);
|
|
110
118
|
- a link to the home page and navigation or search;
|
|
111
119
|
- no stack trace, SQL error or framework debug page;
|
|
@@ -120,7 +128,7 @@ step with `packs: [errors]`:
|
|
|
120
128
|
- **404:** use `path: /no-such-page-{random}`. `{random}` becomes a new
|
|
121
129
|
token on every walk, so no cache or an accidentally created page makes it
|
|
122
130
|
pass. `kaloko draft` adds this step on its own.
|
|
123
|
-
- **Exact status:** override AC601, for example
|
|
131
|
+
- **Exact status:** override HC601 with your own AC601, for example
|
|
124
132
|
`{ id: AC601, text: Answers 410, check: { type: deterministic, assert: http_status, value: 410 } }`.
|
|
125
133
|
`http_status` also takes `min`/`max`; the default is 200–299.
|
|
126
134
|
- **Every step:** each capture records the document's HTTP status. The
|
|
@@ -192,7 +200,8 @@ When the work has a repository and an issue number, make them visible:
|
|
|
192
200
|
- scenario `id` carries the ticket (`TASK-1273-club-activation`, `WEB-12-checkout`); the CLI derives the issue number from it or from the branch (`feat/1273-…`) and links it to the repository's origin (`acme/shop#1273`);
|
|
193
201
|
- set `issue: { number, repo?, url? }` explicitly when the id or branch cannot carry it (for example a Plane item);
|
|
194
202
|
- the project is the repository name unless `project:` in `kaloko.config.yml` says otherwise; a single flow may sort itself elsewhere with `project: <slug>` in its scenario (optional). Projects are records of the organization: `kaloko projects` or MCP `list_projects` show the slugs to use; admins create them in Settings → Projects or assign a scenario in the overview, which wins over the scenario file;
|
|
195
|
-
- the canvas header, the organization overview and `kaloko feedback` show `repo#number · title`, so name the scenario `title` for humans, not for the ticket
|
|
203
|
+
- the canvas header, the organization overview and `kaloko feedback` show `repo#number · title`, so name the scenario `title` for humans, not for the ticket;
|
|
204
|
+
- requirement IDs from the team's tracker or QMS go into `refs:` on the scenario, a step or a criterion (`refs: [URS-12, FS-3.4]`, free text, never invented); they flow into the acceptance record and the trace matrix ([evidence.md](evidence.md)).
|
|
196
205
|
|
|
197
206
|
## Session and consent
|
|
198
207
|
|
|
@@ -216,7 +216,7 @@ dialog, webview and group.
|
|
|
216
216
|
- `app_no_truncation`: texts ending in "…";
|
|
217
217
|
- `app_no_crash`: FATAL EXCEPTION or ANR in logcat, a "keeps stopping"
|
|
218
218
|
dialog, or the iOS app no longer running.
|
|
219
|
-
- **Pack `app` (
|
|
219
|
+
- **Pack `app` (HC7xx):** those four checks, plus a semantic question about
|
|
220
220
|
the screen's purpose and next action.
|
|
221
221
|
- **Web-only checks** (`no_overflow`, `http_status`, meta, `a11y_contrast`,
|
|
222
222
|
`perf_*`, `url_*`, `style_*`, `js_equals`): these are "not evaluated" on
|
|
@@ -353,7 +353,8 @@ Turn on `packs: [design]` (usually `[design, a11y]`) for the steps in the implem
|
|
|
353
353
|
| AC802 | structure: headings, landmarks, actions and form fields as in the reference |
|
|
354
354
|
| AC803 | tokens: authored colors, fonts, sizes, spacing and radii come from the token set |
|
|
355
355
|
|
|
356
|
-
Contrast and the other accessibility basics come from the `a11y` pack
|
|
356
|
+
The design checks count for acceptance. Contrast and the other accessibility basics come from the `a11y` pack
|
|
357
|
+
(`packs: [design, a11y]`) as health: advisory, reported apart (see [evaluation.md#health](evaluation.md#health)).
|
|
357
358
|
|
|
358
359
|
`kaloko evaluate` fetches the design reference (of the design branch linked to the git branch, if any) and
|
|
359
360
|
fills the checks. Findings name what differs and the token to use; fix the implementation, not the reference.
|
|
@@ -68,6 +68,46 @@ evaluators:
|
|
|
68
68
|
Report the reason with each `needs_review` or failing criterion; when the
|
|
69
69
|
evaluator saw the screenshot, say so.
|
|
70
70
|
|
|
71
|
+
## Health
|
|
72
|
+
|
|
73
|
+
The checks of packs (`seo`, `a11y`, `perf`, `usability`, `console`,
|
|
74
|
+
`errors`, `app`) are the run's **health** (CS *kondice*): advisory, stored in
|
|
75
|
+
`node.health` and `summary.health` of `run.json`, never in `criteria`. They
|
|
76
|
+
do not change a step's status, the run verdict, the review queue, carried
|
|
77
|
+
approvals, required approvers, the acceptance record or exit codes. The
|
|
78
|
+
`design` pack and packs listed in `health.acceptance` stay acceptance
|
|
79
|
+
criteria. Runs shared before the split keep their stored verdicts; their pack
|
|
80
|
+
criteria show as health.
|
|
81
|
+
|
|
82
|
+
- **Check it before you hand over.** `kaloko walk` and `kaloko evaluate`
|
|
83
|
+
print one health line; `kaloko health` lists the findings by pack with
|
|
84
|
+
severity (error: people or search engines are shut out; warning: the rest;
|
|
85
|
+
info: the evaluator was unsure), what is new or fixed since the previous run
|
|
86
|
+
of the scenario, and a fix for each check. `--json` for the structure,
|
|
87
|
+
`--remote` (or MCP `get_health`) for a shared run.
|
|
88
|
+
- **Fix what your change can fix**, walk the step again and check that the
|
|
89
|
+
finding is gone. Leave the rest and say so.
|
|
90
|
+
- **Never hide health.** Do not remove a pack, raise a limit, narrow a
|
|
91
|
+
selector or add `environments:` to make a finding disappear. A limit or a
|
|
92
|
+
check that does not fit the project is the person's call; name it in the
|
|
93
|
+
report.
|
|
94
|
+
- **Ids.** Health checks are `HC<n>` (`home:HC103`), acceptance criteria
|
|
95
|
+
`AC<n>`. Runs from before 7 Oct 2026 say `AC103` for the health check;
|
|
96
|
+
read it as `HC103`, and both ids work in commands and MCP.
|
|
97
|
+
- **Adopting is the team's decision.** `kaloko health adopt <step>:<check>
|
|
98
|
+
[--text "…"]` writes the check into the scenario as an acceptance
|
|
99
|
+
criterion of the step (`home:HC103` becomes `AC103` with `from: HC103`),
|
|
100
|
+
so it counts for acceptance from the next walk. Do it only when
|
|
101
|
+
the person asks. Checks people adopted on the canvas (or through
|
|
102
|
+
`adopt_health_check`) wait on the service until `kaloko health pull` writes
|
|
103
|
+
them into the scenario; pulling them is routine, then walk those steps.
|
|
104
|
+
- **Report it apart from the verdicts**: "Health: 2 errors (1 new), 5
|
|
105
|
+
warnings; fixed HC108 on home". Never present a health finding as a
|
|
106
|
+
failing criterion, or a clean health as acceptance.
|
|
107
|
+
- `kaloko health --fail-on error` exits 1 on errors; use it in CI only where
|
|
108
|
+
the team asked for that gate. Hosted runs and CI runs alert on new errors
|
|
109
|
+
by themselves (inbox, mail, channels, webhook `health.worse`).
|
|
110
|
+
|
|
71
111
|
## Triage findings (experimental)
|
|
72
112
|
|
|
73
113
|
When the organization switched the experiment on, `kaloko evaluate` also asks
|
|
@@ -111,7 +151,8 @@ turns them off; they do not count against the hosted allowance.
|
|
|
111
151
|
|
|
112
152
|
After `kaloko build`, report: the summary line, each failing or
|
|
113
153
|
`needs_review` criterion with step, locale, viewport and evidence, blocked
|
|
114
|
-
requests if any,
|
|
154
|
+
requests if any, the health counts (apart from the verdicts), and the preview
|
|
155
|
+
path. Do not claim a criterion passed when
|
|
115
156
|
it was not evaluated.
|
|
116
157
|
|
|
117
158
|
`needs_review` is not a failure: look at the screenshot and state your
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Evidence for your own documentation (regulated work)
|
|
2
|
+
|
|
3
|
+
Use this when a team's own repository turns Kaloko runs into its documents: test protocols, reports, trace
|
|
4
|
+
matrices, release summaries for a quality system. Kaloko provides the evidence and a way to check it; the team's
|
|
5
|
+
own templates and skills write the documents. Kaloko never formats anything for a particular QMS, and you do not put
|
|
6
|
+
the team's SOP structure into Kaloko.
|
|
7
|
+
|
|
8
|
+
The background for people (what a record proves, the two signing paths, known gaps) is in Kaloko's help, page
|
|
9
|
+
*Kaloko in regulated work (GxP)*. Evidence is a Business feature.
|
|
10
|
+
|
|
11
|
+
## The contract
|
|
12
|
+
|
|
13
|
+
Everything below is `qawalk.evidence.v1` (`schemas/evidence.v1.json` in the `kaloko` package, every field
|
|
14
|
+
described). Within v1 fields are only added; read the fields you need and ignore the rest.
|
|
15
|
+
|
|
16
|
+
- `acceptance`: `none`, `accepted` or `returned`. Write your document from this field, never from the presence of
|
|
17
|
+
others.
|
|
18
|
+
- `signing`: `in_kaloko` when a signature made in Kaloko stands, `none` otherwise. With `none` the approval, if any,
|
|
19
|
+
is signed elsewhere; your document says so ("executed in Kaloko; approval recorded in <your eQMS>").
|
|
20
|
+
- `record.sha256` and `record.kind` (`acceptance` or `execution`): the signed record the evidence came from. Put the
|
|
21
|
+
hash into your document so a reader can tie it to the package.
|
|
22
|
+
- `scenario` (`version`, `file`, `sha256`, `refs`), `environment`, `git`, `tools` (CLI, Node.js, OS, browsers,
|
|
23
|
+
service version).
|
|
24
|
+
- `steps[].criteria[]`: the criterion text as walked, `refs`, and `results[]` per locale and viewport with
|
|
25
|
+
`assessed_by` (`person`, `deterministic`, `ai`, `none`) and `assessor` (an e-mail or a model id). `advisory: true`
|
|
26
|
+
means an AI result no person decided: never present it as a person's verdict.
|
|
27
|
+
- `steps[].decision`, `verdict`, `approvals`: people's decisions apart from tokens' and agents'
|
|
28
|
+
(`counts_as_person`).
|
|
29
|
+
- `signatures[]` (name, e-mail, `signed_at`, `time_zone`, `local_time`, `meaning`, `auth`, `withdrawn`) and
|
|
30
|
+
`decision_changes[]` with reasons.
|
|
31
|
+
- `audit[]`: the run's audit trail; `trace[]`: the trace matrix, one row per requirement, criterion, step, locale and
|
|
32
|
+
viewport (columns in `TRACE_COLUMNS` order, the same as `trace.csv`).
|
|
33
|
+
- A release (`kind: release`) has `runs[]` (each a run evidence), `missing[]` with reasons and one `trace[]`.
|
|
34
|
+
|
|
35
|
+
## Getting it
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx kaloko evidence <run-id> --json > evidence.json # one run, any shared run
|
|
39
|
+
npx kaloko evidence --release 2.4.0 --product lims --json > release.json
|
|
40
|
+
npx kaloko evidence <run-id> --csv > trace.csv # the trace matrix only
|
|
41
|
+
npx kaloko signoff <run-id> --package --out evidence/ # the signed zip, checked on download
|
|
42
|
+
npx kaloko signoff --verify evidence/kaloko-evidence-<run-id>.zip # check it again later, no token needed
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Over the API with a read token: `GET /api/v1/orgs/<org>/runs/<id>/evidence`, `…/evidence.zip`, `…/trace.csv`,
|
|
46
|
+
`…/audit.csv`, and `GET /api/v1/orgs/<org>/evidence?product=<p>&version=<v>`. Over MCP: `get_evidence` with `run_id`, or
|
|
47
|
+
`product` and `version`.
|
|
48
|
+
|
|
49
|
+
## When it is ready
|
|
50
|
+
|
|
51
|
+
An organization admin adds a webhook with the event `evidence.ready` (Settings → Integrations → Webhooks). It fires
|
|
52
|
+
when a run is shared, accepted or returned, and when a release is recorded or updated. It is signed like every Kaloko
|
|
53
|
+
webhook (`Kaloko-Signature: t=<unix>,v1=<hex HMAC-SHA256 of "<t>.<body>">`); check the signature and the age of `t`
|
|
54
|
+
before you trust it. The body:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{ "id": "evt_…", "type": "evidence.ready", "created_at": "2026-10-07T10:00:00.000Z", "org": "acme",
|
|
58
|
+
"data": { "kind": "run", "reason": "accepted", "run": { "id": "2026-10-07-batch-x1y2z3", "scenario": "BATCH", "environment": "validation", "project": "lims", "verdict": "accepted" },
|
|
59
|
+
"acceptance": "accepted", "signing": "none", "schema": "qawalk.evidence.v1",
|
|
60
|
+
"evidence_url": "https://kaloko.app/api/v1/orgs/acme/runs/…/evidence", "package_url": "…/evidence.zip", "trace_csv_url": "…/trace.csv" } }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The URLs need a token (`Authorization: Bearer …`); the webhook never carries the evidence itself.
|
|
64
|
+
|
|
65
|
+
A neutral client-side job, for example a small service or a CI job triggered by the webhook:
|
|
66
|
+
|
|
67
|
+
1. Check `Kaloko-Signature` with the webhook's secret; drop events older than five minutes.
|
|
68
|
+
2. Download `package_url` with a read token into your own storage.
|
|
69
|
+
3. Run `npx kaloko signoff --verify <zip>`; stop and alert on any failure.
|
|
70
|
+
4. Read `evidence.json` from the zip and hand it to your generator with your templates.
|
|
71
|
+
5. Store the generated document next to the package and the record's SHA-256; your eQMS takes it from there.
|
|
72
|
+
|
|
73
|
+
## Rules for an agent
|
|
74
|
+
|
|
75
|
+
- Never accept, approve or sign for a person. Tokens and agents cannot sign; where an organization signs in Kaloko,
|
|
76
|
+
only a person does, in the app.
|
|
77
|
+
- Never present `advisory` results as decisions, and never drop the AI model name from a result.
|
|
78
|
+
- Do not turn the signature policy on or off; that is an admin's decision in Settings → Security.
|
|
79
|
+
- Add `refs:` to scenarios, steps and criteria when the team gives you requirement IDs; do not invent them.
|
|
80
|
+
- Keep the package as you got it. To change anything, generate a new document; the package stays the evidence.
|
|
@@ -34,7 +34,7 @@ Ask before filing issues in bulk; one issue per real problem, not per criterion.
|
|
|
34
34
|
|
|
35
35
|
Slack and Teams channels are chosen per project by an admin, with the events
|
|
36
36
|
each wants (new run, returned, accepted, comment, mention, release recorded,
|
|
37
|
-
waiting for approval) and quiet hours. Nothing to do from the CLI: sharing,
|
|
37
|
+
waiting for approval, health got worse) and quiet hours. Nothing to do from the CLI: sharing,
|
|
38
38
|
returning and recording a release reach the right channels. Restricted projects
|
|
39
39
|
reach only channels that name them.
|
|
40
40
|
|
|
@@ -72,6 +72,11 @@ 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
|
+
- Health alerts: a hosted run (and a run shared from CI) compares its health
|
|
76
|
+
with the previous run of the scenario and alerts the followers, the channels
|
|
77
|
+
and the `health.worse` webhook on a new error-level finding. Each schedule
|
|
78
|
+
sets it to `error` (default), `warning` or `off` on kaloko.app or with
|
|
79
|
+
`kaloko schedule add --health-alert`; changing it is the person's call.
|
|
75
80
|
- Does not work for: native apps, local or private addresses (localhost, IPs,
|
|
76
81
|
`.local`), header credentials, base URLs per locale.
|
|
77
82
|
- For those, or for the full walk on a schedule, write a GitHub Actions
|
|
@@ -49,6 +49,8 @@ is null. Projects and products come complete, without pages.
|
|
|
49
49
|
| `get_started` | where the organization stands (`empty`, `sample`, `active`), its sample run, the next steps and a prompt; call it in a new project, see [first-run.md](first-run.md) |
|
|
50
50
|
| `add_sample_run` | adds the sample run (or returns the one there is); needs an upload or full token and the Developer / Tester role |
|
|
51
51
|
| `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 |
|
|
52
|
+
| `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) |
|
|
53
|
+
| `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 |
|
|
52
54
|
| `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 |
|
|
53
55
|
|
|
54
56
|
## Comment tools
|
|
@@ -63,6 +65,7 @@ is null. Projects and products come complete, without pages.
|
|
|
63
65
|
| `answer_finding` | answers a finding (`run_id`, `node`, `locale`, `viewport`, `finding`, `kind`, `answer`: right, wrong or comment with the `comment_id` from `add_comment`); scope comment |
|
|
64
66
|
| `get_triage_report` | the organization's Clef numbers of the last 90 days (read-only): findings shown and answered, precision and calibration per kind, findings turned into comments, median hours to the first review with and without findings, triage requests per useful finding. Report them as they are; the go/no-go is a person's |
|
|
65
67
|
| `get_signoff_record` | the signed acceptance record of an accepted run (Business): approvals with provenance, criteria, capture hashes, signature |
|
|
68
|
+
| `get_evidence` | the evidence of a run (any shared run) or a release (Business, `qawalk.evidence.v1`): acceptance and signing state, criteria as walked with `refs`, who assessed each result (AI advisory), screenshots with SHA-256, decisions, signatures, audit trail, trace matrix ([evidence.md](evidence.md)) |
|
|
66
69
|
| `resolve_comment` | close a thread with a `note` that says what changed (it becomes the last reply) |
|
|
67
70
|
| `reopen_comment`, `edit_comment`, `delete_comment` | reopen; edit or delete your own comment |
|
|
68
71
|
| `create_issue` | a Jira or Linear issue from a step (`run_id`, `step`, `to`: jira or linear, optional `criterion`, `title`, `note`, `locale`, `viewport`, `target`): screenshot, criterion, notes and a link back; the step's thread resolves when the issue is done. Developer / Tester, scope comment; see [integrations.md](integrations.md) |
|
|
@@ -29,7 +29,7 @@ step script only to prepare each page, for example to dismiss a dialog
|
|
|
29
29
|
kind: stack
|
|
30
30
|
title: Magazine articles
|
|
31
31
|
purpose: Every article reads well and is findable.
|
|
32
|
-
packs: [seo] #
|
|
32
|
+
packs: [seo] # health checks of every page (advisory)
|
|
33
33
|
items:
|
|
34
34
|
sitemap: /sitemap.xml # or urls / file / crawl / script (exactly one)
|
|
35
35
|
match: '^/clanky/' # regex on the path with its query; exclude: '…' drops pages
|
package/src/cli.mjs
CHANGED
|
@@ -19,7 +19,7 @@ import { rememberEnvironment } from './lib/secrets.mjs'
|
|
|
19
19
|
import { evaluateRun, semanticPrefetch } from './lib/evaluate.mjs'
|
|
20
20
|
import { buildManifest, writePreview, attentionLines } from './lib/build.mjs'
|
|
21
21
|
import { stabilityCmd, pullDrawn } from './lib/stability.mjs'
|
|
22
|
-
import { signoffCmd } from './lib/signoff.mjs'
|
|
22
|
+
import { signoffCmd, evidenceCmd } from './lib/signoff.mjs'
|
|
23
23
|
import { localized, readJson, UserError, UsageError, globToRegExp, failureStreak, expiryNote, SUPPORT_LINE, SUPPORT_EMAIL, NO_ORG, EXIT, parseDuration } from './lib/util.mjs'
|
|
24
24
|
import { noteLegacy, legacyHint, resetLegacy, legacyInUse } from './lib/legacy.mjs'
|
|
25
25
|
import { packageRoot } from './lib/schema.mjs'
|
|
@@ -35,6 +35,7 @@ import { DEFAULTS as JEV_DEFAULTS } from './lib/jev.mjs'
|
|
|
35
35
|
import { expandPacks } from './lib/packs.mjs'
|
|
36
36
|
import { upsertPrComment } from './lib/pr.mjs'
|
|
37
37
|
import { formatReviewQueue, shareReviewLine } from './lib/review.mjs'
|
|
38
|
+
import { healthCmd } from './lib/health.mjs'
|
|
38
39
|
import { htmlViewAllowed } from './lib/htmlview.mjs'
|
|
39
40
|
import { service, serviceUrl } from './lib/service.mjs'
|
|
40
41
|
import { parseArgs, helpFor, checkOptions, closest } from './lib/cli-args.mjs'
|
|
@@ -235,6 +236,16 @@ Results
|
|
|
235
236
|
review [--run id] [--scenario id] [--json]
|
|
236
237
|
What a person still has to decide on a shared run (failed, unsure, changed, returned,
|
|
237
238
|
commented steps), approvals carried over unchanged steps, and the review-mode link
|
|
239
|
+
health [--run id] [--scenario id] [--remote] [--fail-on error|warning] [--json]
|
|
240
|
+
Health of a run, advisory and never part of a verdict: the checks of packs (SEO,
|
|
241
|
+
accessibility, performance, usability, console, error pages) by severity and pack,
|
|
242
|
+
and what is new or fixed since the previous run of the scenario (the current local
|
|
243
|
+
run; --remote, a shared run id or --scenario ask kaloko.app); --fail-on exits 1
|
|
244
|
+
when there are findings at that level
|
|
245
|
+
health adopt <step>:<check> [--text "…"]
|
|
246
|
+
Make a health check an acceptance criterion of the step in the scenario file, as is
|
|
247
|
+
or with your text; it counts for the step's verdict from the next walk
|
|
248
|
+
health pull [--scenario id] Write the checks people adopted on kaloko.app into the scenario file
|
|
238
249
|
comment [--step id | --flow] --note "…" [--status rejected|approved] [--run id] [--scenario id]
|
|
239
250
|
comment --reply|--resolve|--reopen|--edit|--delete <id> [--note "…"] [--flow]
|
|
240
251
|
Comment, answer and close feedback from the CLI (resolve with "Fixed in <commit>")
|
|
@@ -274,8 +285,18 @@ Results
|
|
|
274
285
|
The signed acceptance record of an accepted run (Business): JSON and PDF with who
|
|
275
286
|
approved what and when, criteria results and capture hashes, checked against the
|
|
276
287
|
service's public key
|
|
277
|
-
signoff
|
|
278
|
-
|
|
288
|
+
signoff <run-id> --package [--out dir] [--lang cs|en] [--json]
|
|
289
|
+
The evidence package of any shared run, accepted or not (Business): the signed
|
|
290
|
+
record (JSON, PDF), evidence.json, the trace matrix (CSV, JSON), the audit trail,
|
|
291
|
+
the screenshots and a signed MANIFEST.json; checked after download
|
|
292
|
+
signoff --verify <file.json|file.pdf|file.zip> [--key file|url] [--json]
|
|
293
|
+
Check a record or an evidence package someone handed over (no token needed); exit 1
|
|
294
|
+
when it does not verify
|
|
295
|
+
evidence <run-id> [--csv] [--out file] [--json]
|
|
296
|
+
evidence --release <version> [--product p] [--csv] [--out file] [--json]
|
|
297
|
+
The evidence of a run or a release (Business, schema qawalk.evidence.v1): acceptance
|
|
298
|
+
and signing state, criteria as walked with requirement refs, results, screenshots
|
|
299
|
+
with SHA-256, decisions, signatures, audit trail; --csv prints the trace matrix
|
|
279
300
|
stability --pull [--scenario id] [--json]
|
|
280
301
|
Regions people drew on screenshots on kaloko.app (step detail → Mark a changing
|
|
281
302
|
region) go into the scenario's ignore: of their steps; walk them again after
|
|
@@ -296,9 +317,12 @@ Jira and Linear (connected by an admin in Settings → Integrations)
|
|
|
296
317
|
|
|
297
318
|
Hosted runs (read-only scenarios on Kaloko's browsers, Business plan)
|
|
298
319
|
schedule add --scenario <file> --env <name> [--every day|week|manual] [--at 5] [--weekday mon] [--account key]
|
|
320
|
+
[--health-alert off|error|warning]
|
|
299
321
|
Kaloko opens the scenario's pages every day or week (hour in UTC) and shares the
|
|
300
322
|
run as you: public pages, HTTP Basic from the environment, or a saved session
|
|
301
|
-
(--account). Steps that click or type are left out; they need the full walk
|
|
323
|
+
(--account). Steps that click or type are left out; they need the full walk.
|
|
324
|
+
When a walk's health is worse than the previous one, the team hears about it:
|
|
325
|
+
new errors (default), new errors or warnings, or off
|
|
302
326
|
schedule list | run <id> | pause <id> | resume <id> | remove <id> [--json]
|
|
303
327
|
schedule hook <id> [--remove] [--json]
|
|
304
328
|
A URL and secret that start the hosted run from a deploy (Vercel, Netlify) or
|
|
@@ -424,7 +448,9 @@ async function walkVerdicts ({ prefetch, run, scenario, config, root }) {
|
|
|
424
448
|
const t0 = Date.now()
|
|
425
449
|
await prefetch.done()
|
|
426
450
|
const report = await evaluateRun({ run, scenario, config, root })
|
|
427
|
-
|
|
451
|
+
// health checks (advisory) are counted apart; they never make a step fail
|
|
452
|
+
const health = report.findings.filter((f) => f.health); const accept = report.findings.filter((f) => !f.health)
|
|
453
|
+
const fails = accept.filter((f) => f.verdict === 'fail'); const unsure = accept.filter((f) => f.verdict === 'needs_review')
|
|
428
454
|
say(`\nEvaluated ${report.evaluated} capture(s) in ${Math.round((Date.now() - t0) / 100) / 10} s: ${fails.length ? `${fails.length} failing` : 'nothing failing'}${unsure.length ? `, ${unsure.length} for a person to look at` : ''}.`)
|
|
429
455
|
const line = (f) => `${f.node}${f.item ? `/${f.item}` : ''} ${f.locale}/${f.viewport} ${f.id}${f.why ? `: ${String(f.why).slice(0, 140)}` : ''}`
|
|
430
456
|
// pack criteria say how to fix them; a fix repeated across steps and locales is printed once
|
|
@@ -433,6 +459,16 @@ async function walkVerdicts ({ prefetch, run, scenario, config, root }) {
|
|
|
433
459
|
if (fails.length) say(` ✕ ${fails.slice(0, 15).map(withFix).join('\n ✕ ')}${fails.length > 15 ? `\n … ${fails.length - 15} more (kaloko build lists them all)` : ''}`)
|
|
434
460
|
if (unsure.length) say(` ? ${unsure.slice(0, 8).map(line).join('\n ? ')}${unsure.length > 8 ? `\n … ${unsure.length - 8} more` : ''}`)
|
|
435
461
|
if (report.skipped.length) say(` Semantic criteria not evaluated: ${[...new Set(report.skipped.map((x) => x.split(': ').slice(1).join(': ')))].slice(0, 2).join('; ')}`)
|
|
462
|
+
if (health.length) say(healthLine(health))
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** One line on the health findings of an evaluation: counts by severity (one per check and step), never a verdict. */
|
|
466
|
+
function healthLine (findings) {
|
|
467
|
+
const worst = new Map()
|
|
468
|
+
const rank = { error: 3, warning: 2, info: 1 }
|
|
469
|
+
for (const f of findings) { const k = `${f.node}:${f.id}`; if ((rank[f.severity] || 0) > (rank[worst.get(k)] || 0)) worst.set(k, f.severity) }
|
|
470
|
+
const n = (s) => [...worst.values()].filter((x) => x === s).length
|
|
471
|
+
return `Health (advisory, not part of any verdict): ${n('error')} error(s), ${n('warning')} warning(s)${n('info') ? `, ${n('info')} unsure` : ''}. Fix what you can before review; kaloko health lists them.`
|
|
436
472
|
}
|
|
437
473
|
|
|
438
474
|
const list = (v) => (typeof v === 'string' ? v.split(',').map((s) => s.trim()).filter(Boolean) : null)
|
|
@@ -1842,11 +1878,12 @@ const commands = {
|
|
|
1842
1878
|
for (const x of report.skipped) { const [where, ...why] = x.split(': '); const k = why.join(': '); byReason.set(k, [...(byReason.get(k) || []), where]) }
|
|
1843
1879
|
say(`Semantic criteria not evaluated:\n - ${[...byReason].slice(0, 8).map(([why, where]) => `${where.length > 1 ? `${where.length} captures (${where.slice(0, 3).join(', ')}${where.length > 3 ? ', …' : ''})` : where[0]}: ${why}`).join('\n - ')}`)
|
|
1844
1880
|
}
|
|
1881
|
+
if ((report.findings || []).some((f) => f.health)) say(healthLine(report.findings.filter((f) => f.health)))
|
|
1845
1882
|
if (report.outdated.length) say(`Criteria changed after capture of ${report.outdated.join(', ')}: not evaluated. Run "kaloko evaluate --recheck --steps ${report.outdated.join(',')}": it rechecks the stored captures against the new criteria, deterministic and semantic, without walking again.`)
|
|
1846
1883
|
// --json: the counts and every verdict a person or agent has to act on (fail, needs_review)
|
|
1847
1884
|
if (args.json) {
|
|
1848
1885
|
const pick = (f) => ({ step: f.node, ...(f.item ? { item: f.item } : {}), locale: f.locale, viewport: f.viewport, criterion: f.id, verdict: f.verdict, why: f.why || null, fix: f.fix || null })
|
|
1849
|
-
say(JSON.stringify({ run: run.meta.id, evaluated: report.evaluated, semantic: { model: report.model, calls: report.semantic_calls, cached: report.semantic_cached, via: report.via.qawalk ? 'kaloko' : report.via['own-key'] ? 'own-key' : null }, vision: report.vision || null, triage: report.triage || null, findings: (report.findings || []).filter((f) => f.verdict === 'fail' || f.verdict === 'needs_review').map(pick), skipped: report.skipped, outdated: report.outdated }, null, 2))
|
|
1886
|
+
say(JSON.stringify({ run: run.meta.id, evaluated: report.evaluated, semantic: { model: report.model, calls: report.semantic_calls, cached: report.semantic_cached, via: report.via.qawalk ? 'kaloko' : report.via['own-key'] ? 'own-key' : null }, vision: report.vision || null, triage: report.triage || null, findings: (report.findings || []).filter((f) => !f.health && (f.verdict === 'fail' || f.verdict === 'needs_review')).map(pick), health: (report.findings || []).filter((f) => f.health).map((f) => ({ ...pick(f), pack: f.pack, severity: f.severity })), skipped: report.skipped, outdated: report.outdated }, null, 2))
|
|
1850
1887
|
}
|
|
1851
1888
|
},
|
|
1852
1889
|
|
|
@@ -1858,7 +1895,7 @@ const commands = {
|
|
|
1858
1895
|
const why = attentionLines(manifest)
|
|
1859
1896
|
// --json: the run id, its summary, the preview file and the AI verdicts to act on
|
|
1860
1897
|
if (args.json) { say(JSON.stringify({ run: manifest.id, summary: s, preview: file, manifest: join(run.dir, 'run.json'), attention: why.map((l) => l.trim()) }, null, 2)); return file }
|
|
1861
|
-
say(`${manifest.id}: ${s.pass} pass · ${s.partial} partial · ${s.fail} fail · ${s.skip} not checked\nPreview: ${file}`)
|
|
1898
|
+
say(`${manifest.id}: ${s.pass} pass · ${s.partial} partial · ${s.fail} fail · ${s.skip} not checked${s.health ? `\nHealth (advisory): ${s.health.error} error(s), ${s.health.warning} warning(s) in ${s.health.checks} checks (kaloko health)` : ''}\nPreview: ${file}`)
|
|
1862
1899
|
if (why.length) say(`AI verdicts to act on (score and why):\n${why.join('\n')}`)
|
|
1863
1900
|
return file
|
|
1864
1901
|
},
|
|
@@ -2099,6 +2136,9 @@ const commands = {
|
|
|
2099
2136
|
// how many steps still wait for a person (review mode); an older service answers 404 and the line is left out
|
|
2100
2137
|
const queue = await client.request('GET', `/orgs/${config.org}/review-queue?run=${encodeURIComponent(data.run.id)}`).catch(() => null)
|
|
2101
2138
|
if (queue?.counts) say(` review: ${queue.counts.pending} of ${queue.counts.queue} step(s) wait for a person${queue.counts.carried ? `, ${queue.counts.carried} unchanged kept their approval` : ''} (kaloko review; review mode: ${queue.review_url})`)
|
|
2139
|
+
// health checks people adopted into acceptance on kaloko.app (an older service leaves the field out)
|
|
2140
|
+
const adopted = data.health_adoptions || []
|
|
2141
|
+
if (adopted.length) say(` adopted into acceptance, not in the scenario file yet (kaloko health pull writes them):\n${adopted.map((a) => ` + ${a.step}:${a.check} "${a.text}" — ${a.created_by}${a.created_via === 'agent' ? ' (agent)' : ''}`).join('\n')}`)
|
|
2102
2142
|
if (!data.items.length && !flow.length) { say(' No open rejections or comments.'); return }
|
|
2103
2143
|
// returned steps first: a reviewer said what should change
|
|
2104
2144
|
const returned = data.items.filter((r) => r.status === 'rejected')
|
|
@@ -2141,10 +2181,30 @@ const commands = {
|
|
|
2141
2181
|
const { config, root } = await context(args)
|
|
2142
2182
|
let runId = args._[0] || (typeof args.run === 'string' ? args.run : null)
|
|
2143
2183
|
if (!runId) { try { const r = await resolveRun({ root, output: config.output }); if (r.meta.shared) runId = r.meta.id } catch {} }
|
|
2144
|
-
if (!runId) throw new UsageError('Usage: kaloko signoff <run-id> [--out dir] | kaloko signoff --verify <file.json|file.pdf>')
|
|
2184
|
+
if (!runId) throw new UsageError('Usage: kaloko signoff <run-id> [--package] [--out dir] | kaloko signoff --verify <file.json|file.pdf|file.zip>')
|
|
2145
2185
|
return signoffCmd({ config, args, say, client: service(config), runId })
|
|
2146
2186
|
},
|
|
2147
2187
|
|
|
2188
|
+
/** The evidence of a run or a release (qawalk.evidence.v1) for a client's own generator. */
|
|
2189
|
+
async evidence (args) {
|
|
2190
|
+
const { config, root } = await context(args)
|
|
2191
|
+
let runId = args._[0] || (typeof args.run === 'string' ? args.run : null)
|
|
2192
|
+
if (!runId && typeof args.release !== 'string') { try { const r = await resolveRun({ root, output: config.output }); if (r.meta.shared) runId = r.meta.id } catch {} }
|
|
2193
|
+
return evidenceCmd({ config, args, say, client: service(config), runId })
|
|
2194
|
+
},
|
|
2195
|
+
|
|
2196
|
+
/** Health (theme K): advisory checks of the packs, adopting one into acceptance, pulling adoptions (health.mjs). */
|
|
2197
|
+
async health (args) {
|
|
2198
|
+
return healthCmd({
|
|
2199
|
+
args, say,
|
|
2200
|
+
deps: {
|
|
2201
|
+
context, runContext, buildManifest, service,
|
|
2202
|
+
localRunExists: async (a) => { try { const { root, config } = await context(a); await resolveRun({ root, output: config.output, run: a.run }); return true } catch { return false } },
|
|
2203
|
+
sharedRunId: async (a) => { const { root, config } = await context(a); const r = await resolveRun({ root, output: config.output }); if (!r.meta.shared) throw new UserError('The current run is not shared yet: run kaloko health without --remote for the local one, or kaloko share first.'); return r.meta.id }
|
|
2204
|
+
}
|
|
2205
|
+
})
|
|
2206
|
+
},
|
|
2207
|
+
|
|
2148
2208
|
async review (args) {
|
|
2149
2209
|
const { config, root } = await context(args)
|
|
2150
2210
|
const client = service(config)
|
package/src/lib/build.mjs
CHANGED
|
@@ -25,7 +25,7 @@ export async function buildManifest ({ run, scenario, config, root, catalog = []
|
|
|
25
25
|
const mailKeys = viewportNames.filter((k) => variantOf(meta.scope, k).browser === 'chromium')
|
|
26
26
|
const nodes = []
|
|
27
27
|
for (const node of scenario.nodes) {
|
|
28
|
-
if (node.kind === 'stack') { const p = pos.get(node.id); nodes.push(await buildStack({ run, node, base: { id: node.id, kind: node.kind, title: node.title, path: node.path ?? null, purpose: node.purpose ?? '', col: p.col, lane: p.lane }, captures, attachments })); continue }
|
|
28
|
+
if (node.kind === 'stack') { const p = pos.get(node.id); nodes.push(await buildStack({ run, node, base: { id: node.id, kind: node.kind, title: node.title, path: node.path ?? null, purpose: node.purpose ?? '', col: p.col, lane: p.lane, ...(node.refs?.length ? { refs: node.refs } : {}) }, captures, attachments })); continue }
|
|
29
29
|
const caps = []
|
|
30
30
|
// pages of a stack live in its items index; normal steps take their own captures only
|
|
31
31
|
for (const c of captures.filter((x) => x.node === node.id && !x.item)) {
|
|
@@ -74,6 +74,8 @@ export async function buildManifest ({ run, scenario, config, root, catalog = []
|
|
|
74
74
|
app_version: meta.app_version ?? meta.version ?? null,
|
|
75
75
|
environment: meta.environment,
|
|
76
76
|
executor: meta.executor,
|
|
77
|
+
// what walked it: the CLI, Node.js, the OS and the browser engines seen in the captures (acceptance records)
|
|
78
|
+
tools: toolsOf(meta, nodes),
|
|
77
79
|
started_at: meta.started_at,
|
|
78
80
|
finished_at: new Date().toISOString(),
|
|
79
81
|
scope: meta.scope,
|
|
@@ -91,6 +93,13 @@ export async function buildManifest ({ run, scenario, config, root, catalog = []
|
|
|
91
93
|
}
|
|
92
94
|
}
|
|
93
95
|
|
|
96
|
+
/** The tools of a run: kaloko and its version, Node.js, the OS, and every browser version a capture reports. */
|
|
97
|
+
export function toolsOf (meta, nodes) {
|
|
98
|
+
const browsers = new Set()
|
|
99
|
+
for (const n of nodes) for (const vps of Object.values(n.captures || {})) for (const c of Object.values(vps || {})) if (c?.browser_version) browsers.add(c.browser_version)
|
|
100
|
+
return { cli: meta.tools?.cli || { name: 'kaloko', version: null }, node: meta.tools?.node || process.version, os: meta.tools?.os || `${process.platform} ${process.arch}`, browsers: [...browsers].sort() }
|
|
101
|
+
}
|
|
102
|
+
|
|
94
103
|
/** The recording entry of a sidecar as the manifest carries it: file paths, duration, size, markers. */
|
|
95
104
|
async function recEntry (run, e) {
|
|
96
105
|
const r = await readJson(join(run.dir, e.file), null)
|
package/src/lib/capture.mjs
CHANGED
|
@@ -11,6 +11,11 @@ import { maskSpec, maskList, inPage, maskInPage, rememberPii, knownPii, maskMail
|
|
|
11
11
|
* Encodes a PNG screenshot inside the page (no native dependency): the image as WebP (or PNG, lossless) and a
|
|
12
12
|
* WebP thumbnail for the canvas grid; `marks` are numbered circles drawn on the image (documentation captures).
|
|
13
13
|
*/
|
|
14
|
+
/** "chromium 131.0.6778.33" or null: the browser of a page (Playwright), never failing a capture. */
|
|
15
|
+
export function browserVersion (page) {
|
|
16
|
+
try { const b = page.context().browser(); const v = b?.version?.(); return v ? `${b.browserType?.().name?.() || 'browser'} ${v}` : null } catch { return null }
|
|
17
|
+
}
|
|
18
|
+
|
|
14
19
|
export async function encodeImage (page, png, { format = 'webp', quality = 0.82, thumbWidth = 640, thumbQuality = 0.8, marks = [], scale = 1, masks = [] } = {}) {
|
|
15
20
|
const result = await page.evaluate(async ({ dataUrl, format, quality, thumbWidth, thumbQuality, marks, scale, masks }) => {
|
|
16
21
|
const img = new Image()
|
|
@@ -300,6 +305,8 @@ export async function captureStep ({ page, node, locale, viewports, run, console
|
|
|
300
305
|
node: node.id, locale, viewport, url,
|
|
301
306
|
...variantFields(scope, viewport),
|
|
302
307
|
captured_at: new Date().toISOString(),
|
|
308
|
+
// the engine and its version, for the record of what walked the run (acceptance records, evidence)
|
|
309
|
+
browser_version: browserVersion(page),
|
|
303
310
|
criteria_hash: criteriaHash(node),
|
|
304
311
|
console_errors: errors, blocked_requests: blocked,
|
|
305
312
|
...result,
|
package/src/lib/cli-args.mjs
CHANGED
|
@@ -29,6 +29,7 @@ export const HIDDEN = {
|
|
|
29
29
|
history: ['lang'],
|
|
30
30
|
tokens: ['env', 'version'],
|
|
31
31
|
stability: ['scenario'],
|
|
32
|
+
health: ['text'],
|
|
32
33
|
status: ['branch'],
|
|
33
34
|
feedback: ['branch'],
|
|
34
35
|
share: ['branch', 'name', 'notes', 'notes-preview', 'strict', 'mine', 'theirs', 'lang'],
|
package/src/lib/draft.mjs
CHANGED
|
@@ -65,7 +65,7 @@ export function draftScenario (pages, { id, title } = {}) {
|
|
|
65
65
|
`Source of truth for intent. Machine scenario: next to this file. Drafted by \`kaloko draft\` from ${pages.length} page(s) of ${origin} on ${new Date().toISOString().slice(0, 10)}; sharpen purposes and criteria before relying on it.`, '',
|
|
66
66
|
'## Goal and scope', '', `Public pages of ${host} read well, are findable, accessible and fast, as a visitor sees them. Read-only on production; nothing is submitted.`, '',
|
|
67
67
|
'## Steps and criteria', '',
|
|
68
|
-
'Every page step also runs the packs `seo` (
|
|
68
|
+
'Every page step also runs the packs `seo` (HC1xx), `a11y` (HC3xx), `perf` (HC4xx) and `console` (HC501); the error page runs `errors` (HC6xx). Pack checks are the health of the pages: advisory, reported apart, never part of a verdict. Adopt one into the criteria below (`kaloko health adopt <step>:<check>`) when it must decide acceptance.', '',
|
|
69
69
|
...nodes.flatMap((n) => [`### ${n.id} — ${n.title} (\`screen\`)`, '', `- Path: \`${n.path}\``, `- Purpose: ${n.purpose}`, ...n.criteria.map((c) => `- ${c.id}: ${c.text}.`), '']),
|
|
70
70
|
'## Environments and limits', '', `production (read-only, as a visitor): add \`environments.production: { base_url: ${origin}, readonly: true }\` to kaloko.config.yml if it is not there.`, '',
|
|
71
71
|
'## Change log', '', '| Date | Commit | Change |', '|---|---|---|', `| ${new Date().toISOString().slice(0, 10)} | — | Drafted from the pages by kaloko draft. |`, ''
|
package/src/lib/evaluate.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { needsDrift, driftForCapture, loadTokens, driftCacheDir } from './drift.
|
|
|
11
11
|
import { packHelp } from './pack-help.mjs'
|
|
12
12
|
import { tokenCriteria, storedValueChecks, STYLE_ASSERTS } from './assertions.mjs'
|
|
13
13
|
import { appliesToState } from './manifest-core.mjs'
|
|
14
|
+
import { isHealthCriterion, findingSeverity, toAcceptanceId } from './health-core.mjs'
|
|
14
15
|
|
|
15
16
|
/** Latest capture per node/locale/viewport. */
|
|
16
17
|
export function latestCaptures (entries) {
|
|
@@ -19,7 +20,10 @@ export function latestCaptures (entries) {
|
|
|
19
20
|
return [...map.values()]
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
// `acceptance` (a pack kept in acceptance) changes what decides the verdict, not what a capture checks
|
|
24
|
+
// a pack check hashes in its AC form whatever its role (HC302 in health, AC302 in acceptance): moving a pack in or out of
|
|
25
|
+
// acceptance, and the HC ids themselves (7 Oct 2026), leave the captures of before valid
|
|
26
|
+
export const criteriaHash = (node) => sha256((node.criteria || []).map(({ acceptance, ...c }) => (c.pack ? { ...c, id: toAcceptanceId(c.id) } : c)))
|
|
23
27
|
/** A criterion with `environments` (e.g. [production]) applies only there; without a known environment it applies. */
|
|
24
28
|
export const appliesOn = (c, environment) => !environment || !Array.isArray(c.environments) || !c.environments.length || c.environments.includes(environment)
|
|
25
29
|
/** Environment and step state (#46): a criterion with `states:` applies only to captures of those states. */
|
|
@@ -177,7 +181,8 @@ export async function evaluateRun ({ run, scenario, config, root, fetchImpl }) {
|
|
|
177
181
|
await writeJson(paths.step(stepIdOf(entry), entry.locale, entry.viewport, 'eval.json'), evaluation)
|
|
178
182
|
report.evaluated++
|
|
179
183
|
// what did not pass, for the walk summary and agents: step, locale, viewport, criterion and why
|
|
180
|
-
|
|
184
|
+
// health checks (pack criteria outside acceptance) are reported apart: advisory, never a verdict
|
|
185
|
+
for (const c of criteria) if (c.verdict === 'fail' || c.verdict === 'needs_review') { const def = (node.criteria || []).find((x) => x.id === c.id); report.findings.push({ node: entry.node, ...(entry.item ? { item: entry.item } : {}), locale: entry.locale, viewport: entry.viewport, id: c.id, verdict: c.verdict, type: c.type, why: c.evidence || (c.score != null ? `AI ${Math.round(c.score * 100)} % yes${c.reason_source === 'model' ? `: ${c.reason}` : ''}` : c.note) || null, ...(c.reason ? { reason: c.reason } : {}), ...(def?.pack && packHelp(c.id) ? { fix: packHelp(c.id).fix } : {}), ...(isHealthCriterion(def) ? { health: true, pack: def.pack, severity: findingSeverity(c.verdict, c.id) } : {}) }) }
|
|
181
186
|
}
|
|
182
187
|
}
|
|
183
188
|
} finally { if (drift?.browser) await drift.browser.close(); await vision?.close(); await triage?.close() }
|