@josueavalosjim/taste-check 0.6.0 → 0.7.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 +30 -3
- package/bin/taste-check.mjs +69 -6
- package/package.json +2 -1
- package/skills/taste-check-judge/SKILL.md +82 -0
- package/src/config.mjs +7 -5
- package/src/index.mjs +1 -1
- package/src/judge.mjs +92 -37
package/README.md
CHANGED
|
@@ -290,6 +290,35 @@ inside a tool is somebody else's taste with the tool's authority behind it.
|
|
|
290
290
|
Checklist lines are list items in your file; a heading or a paragraph is prose
|
|
291
291
|
and is not judged.
|
|
292
292
|
|
|
293
|
+
### Letting an agent carry the call
|
|
294
|
+
|
|
295
|
+
A shell command is one way to reach a model and a poor one for an agent, which
|
|
296
|
+
is already a model and can spawn a genuinely fresh context of its own instead
|
|
297
|
+
of shelling out to a second copy of itself. So the judge splits in two:
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
taste-check judge --emit # the prompt, and nothing else happens
|
|
301
|
+
taste-check judge --verdict reply.json # or - for stdin
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
`--emit` prints the prompt and stops. It still refuses when there are no
|
|
305
|
+
screenshots or no checklist, because a prompt for nothing gets a confident
|
|
306
|
+
verdict about nothing. `--verdict` checks the reply against the checklist
|
|
307
|
+
exactly as the shell route does: every line answered once, no invented lines,
|
|
308
|
+
valid verdicts. Taking the agent route does not buy a softer grading.
|
|
309
|
+
|
|
310
|
+
`judge.command` is optional when you use this. There is a skill for it:
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
taste-check judge --skill # prints the path to SKILL.md
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Copy it wherever your agent keeps skills. It carries the mechanism, which is
|
|
317
|
+
that the agent must not be the judge: it is reading the session that built the
|
|
318
|
+
thing, and the reasoning that justified each choice is still sitting there
|
|
319
|
+
ready to justify it again. It carries no design rules, for the same reason
|
|
320
|
+
nothing else here does.
|
|
321
|
+
|
|
293
322
|
### What the exit code means here
|
|
294
323
|
|
|
295
324
|
A verdict is an opinion, so a `fail` prints as a note and the command exits 0.
|
|
@@ -328,7 +357,7 @@ from anywhere.
|
|
|
328
357
|
| `judge.checklist` | Your checklist file. List items are judged, prose is not. |
|
|
329
358
|
| `judge.shots` | Screenshots to hand the judge. Matching nothing is a failure. |
|
|
330
359
|
| `judge.shotCommand` | Optional command run first to produce those screenshots. |
|
|
331
|
-
| `judge.command` | The model command. Prompt on stdin, image paths as arguments. |
|
|
360
|
+
| `judge.command` | The model command. Prompt on stdin, image paths as arguments. Optional if an agent carries the call. |
|
|
332
361
|
| `judge.failOn` | `"never"` (default) or `"fail"`. Whether a verdict blocks. |
|
|
333
362
|
| `runtime.url` | The page to measure. A `file://` URL works. |
|
|
334
363
|
| `runtime.endpoint` | An existing CDP endpoint. Given one, taste-check connects rather than launching, and never closes a browser it did not start. |
|
|
@@ -453,8 +482,6 @@ Not built. Written down so the shape is clear.
|
|
|
453
482
|
|
|
454
483
|
**YAML configs**, once there is a reason to take on a parser.
|
|
455
484
|
|
|
456
|
-
**A way to run the judge from an agent skill**, not only from a shell.
|
|
457
|
-
|
|
458
485
|
**`lab()` and `lch()`**, which need the D50 white point and a chromatic
|
|
459
486
|
adaptation step that `oklch()` does not. Completeness rather than reach, so
|
|
460
487
|
it sits behind the others. Worth doing the same way when it happens: derive it,
|
package/bin/taste-check.mjs
CHANGED
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
* not a quiet skip.
|
|
9
9
|
*/
|
|
10
10
|
import { readFileSync } from 'node:fs';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
11
12
|
|
|
12
13
|
import { load } from '../src/config.mjs';
|
|
13
|
-
import { judge, run, runtime } from '../src/index.mjs';
|
|
14
|
+
import { gradeVerdict, judge, prepareJudge, run, runtime } from '../src/index.mjs';
|
|
14
15
|
import { failed, toJson, toText } from '../src/report.mjs';
|
|
15
16
|
import { toSarif } from '../src/sarif.mjs';
|
|
16
17
|
|
|
@@ -23,6 +24,10 @@ const USAGE = `taste-check
|
|
|
23
24
|
Options:
|
|
24
25
|
-c, --config <path> Config file (default: tastecheck.config.json)
|
|
25
26
|
--only <name> Run one check: contrast or treatments
|
|
27
|
+
--emit judge only: print the prompt and stop, for an agent
|
|
28
|
+
to carry to a model itself
|
|
29
|
+
--verdict <path> judge only: grade a reply from a file, or - for stdin
|
|
30
|
+
--skill Print the path to the bundled agent skill
|
|
26
31
|
--format <kind> text (default), json, or sarif
|
|
27
32
|
--json Alias for --format json
|
|
28
33
|
-h, --help This
|
|
@@ -43,6 +48,11 @@ is already up. It measures what is actually painted, compositing every
|
|
|
43
48
|
background layer behind an element rather than stopping at the first opaque
|
|
44
49
|
one, and it can put the page into a state first.
|
|
45
50
|
|
|
51
|
+
An agent can carry the model call instead of a shell command. --emit prints
|
|
52
|
+
the prompt and the images; the agent asks a fresh context and pipes the JSON
|
|
53
|
+
back to --verdict -, which checks it against the checklist the same way. Run
|
|
54
|
+
taste-check judge --skill for the bundled skill that wires this up.
|
|
55
|
+
|
|
46
56
|
The judge is a separate command because it runs a model, and a model's
|
|
47
57
|
verdict is not reproducible. Its verdicts print as notes and do not affect
|
|
48
58
|
the exit code unless judge.failOn is set to "fail". Whether the judge ran
|
|
@@ -50,7 +60,15 @@ at all is a different question: no screenshots, a command that failed, or
|
|
|
50
60
|
a reply that skipped a checklist line all exit 1 either way.`;
|
|
51
61
|
|
|
52
62
|
function parseArgs(argv) {
|
|
53
|
-
const options = {
|
|
63
|
+
const options = {
|
|
64
|
+
config: 'tastecheck.config.json',
|
|
65
|
+
only: null,
|
|
66
|
+
format: 'text',
|
|
67
|
+
command: 'check',
|
|
68
|
+
emit: false,
|
|
69
|
+
verdict: null,
|
|
70
|
+
skill: false,
|
|
71
|
+
};
|
|
54
72
|
// One positional, and only in first position, so a stray argument is an
|
|
55
73
|
// error rather than something silently ignored.
|
|
56
74
|
if (argv[0] === 'judge' || argv[0] === 'runtime') {
|
|
@@ -61,7 +79,11 @@ function parseArgs(argv) {
|
|
|
61
79
|
const arg = argv[i];
|
|
62
80
|
const next = () => {
|
|
63
81
|
const value = argv[i + 1];
|
|
64
|
-
|
|
82
|
+
// A bare "-" is a value, not a flag: it is the conventional name for
|
|
83
|
+
// stdin and --verdict takes it.
|
|
84
|
+
if (value === undefined || (value !== '-' && value.startsWith('-'))) {
|
|
85
|
+
throw new Error(`${arg} needs a value`);
|
|
86
|
+
}
|
|
65
87
|
i += 1;
|
|
66
88
|
return value;
|
|
67
89
|
};
|
|
@@ -74,13 +96,15 @@ function parseArgs(argv) {
|
|
|
74
96
|
throw new Error(`--only takes "contrast" or "treatments", not "${options.only}"`);
|
|
75
97
|
}
|
|
76
98
|
} else if (arg === '--json') options.format = 'json';
|
|
99
|
+
else if (arg === '--emit') options.emit = true;
|
|
100
|
+
else if (arg === '--skill') options.skill = true;
|
|
101
|
+
else if (arg === '--verdict') options.verdict = next();
|
|
77
102
|
else if (arg === '--format') {
|
|
78
103
|
options.format = next();
|
|
79
104
|
if (!['text', 'json', 'sarif'].includes(options.format)) {
|
|
80
105
|
throw new Error(`--format takes text, json or sarif, not "${options.format}"`);
|
|
81
106
|
}
|
|
82
|
-
}
|
|
83
|
-
else if (!arg.startsWith('-')) throw new Error(`unknown command "${arg}"`);
|
|
107
|
+
} else if (!arg.startsWith('-')) throw new Error(`unknown command "${arg}"`);
|
|
84
108
|
else throw new Error(`unknown option "${arg}"`);
|
|
85
109
|
}
|
|
86
110
|
return options;
|
|
@@ -108,6 +132,19 @@ if (options.version) {
|
|
|
108
132
|
process.exit(0);
|
|
109
133
|
}
|
|
110
134
|
|
|
135
|
+
// Printing the bundled skill needs no config: it is the same file every time.
|
|
136
|
+
if (options.skill) {
|
|
137
|
+
console.log(fileURLToPath(new URL('../skills/taste-check-judge/SKILL.md', import.meta.url)));
|
|
138
|
+
process.exit(0);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const flag of ['emit', 'verdict']) {
|
|
142
|
+
if (options[flag] && options.command !== 'judge') {
|
|
143
|
+
die(`--${flag} applies to \`taste-check judge\`, not to ${options.command}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (options.emit && options.verdict) die('--emit prints a prompt and --verdict grades a reply, so not both');
|
|
147
|
+
|
|
111
148
|
const loaded = load(options.config);
|
|
112
149
|
if (!loaded.ok) {
|
|
113
150
|
console.error(`taste-check: ${options.config} could not be used:\n`);
|
|
@@ -123,8 +160,34 @@ if (options.command !== 'check' && !loaded.config[options.command]) {
|
|
|
123
160
|
die(`${options.config} defines no "${options.command}" block.`);
|
|
124
161
|
}
|
|
125
162
|
|
|
163
|
+
// --emit stops before any model is involved, so it has no findings to report
|
|
164
|
+
// and no exit code to earn. It still refuses to hand back a prompt when the
|
|
165
|
+
// screenshots or the checklist are missing.
|
|
166
|
+
if (options.emit) {
|
|
167
|
+
const prepared = prepareJudge(loaded.config.judge, loaded.dir);
|
|
168
|
+
if (!prepared.ok) {
|
|
169
|
+
console.log(toText([prepared.result]));
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
console.log(prepared.prompt);
|
|
173
|
+
process.exit(0);
|
|
174
|
+
}
|
|
175
|
+
|
|
126
176
|
let results;
|
|
127
|
-
if (options.
|
|
177
|
+
if (options.verdict) {
|
|
178
|
+
const prepared = prepareJudge(loaded.config.judge, loaded.dir);
|
|
179
|
+
if (!prepared.ok) {
|
|
180
|
+
console.log(toText([prepared.result]));
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
let reply;
|
|
184
|
+
try {
|
|
185
|
+
reply = readFileSync(options.verdict === '-' ? 0 : options.verdict, 'utf8');
|
|
186
|
+
} catch {
|
|
187
|
+
die(`cannot read the verdict from ${options.verdict === '-' ? 'stdin' : options.verdict}`);
|
|
188
|
+
}
|
|
189
|
+
results = [gradeVerdict(reply, prepared, loaded.config.judge)];
|
|
190
|
+
} else if (options.command === 'judge') results = judge(loaded.config, loaded.dir);
|
|
128
191
|
else if (options.command === 'runtime') results = await runtime(loaded.config, loaded.dir);
|
|
129
192
|
else results = run(loaded.config, loaded.dir, { only: options.only });
|
|
130
193
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@josueavalosjim/taste-check",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Design review in CI with a line down the middle: measured checks that gate the build (WCAG contrast from your tokens or from a real rendered page, one-off values in your markup) and a fresh-eyes model judge whose verdicts stay advisory. Zero dependencies. Ships no design rules of its own.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"accessibility",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"bin",
|
|
35
35
|
"src",
|
|
36
36
|
"schema",
|
|
37
|
+
"skills",
|
|
37
38
|
"README.md",
|
|
38
39
|
"LICENSE"
|
|
39
40
|
],
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: taste-check-judge
|
|
3
|
+
description: Run the taste-check fresh-eyes judge on a design, using a fresh subagent as the judge rather than a shell command. Use after building or changing a screen, when asked to review or critique a design, or on any request to run the taste gate. Requires a tastecheck.config.json with a judge block.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# taste-check judge
|
|
7
|
+
|
|
8
|
+
You are the transport, not the judge.
|
|
9
|
+
|
|
10
|
+
`taste-check` builds the prompt and checks the answer. You carry the prompt to a
|
|
11
|
+
model and carry the reply back. The model you carry it to must not be you.
|
|
12
|
+
|
|
13
|
+
## Why not you
|
|
14
|
+
|
|
15
|
+
You are reading this inside a session that has context: what was built, what it
|
|
16
|
+
was for, which tradeoffs were made and why. That context is exactly what
|
|
17
|
+
disqualifies you from judging the result. The reasoning that justified each
|
|
18
|
+
choice is still sitting here ready to justify it again, and a review that
|
|
19
|
+
reaches for it is not a review.
|
|
20
|
+
|
|
21
|
+
So the judge is a separate agent with none of it. That is the whole mechanism,
|
|
22
|
+
and skipping it turns this into self-assessment with extra steps.
|
|
23
|
+
|
|
24
|
+
## Steps
|
|
25
|
+
|
|
26
|
+
**1. Produce the screenshots.** However this project does it. If the config has
|
|
27
|
+
a `shotCommand`, the next step runs it for you.
|
|
28
|
+
|
|
29
|
+
**2. Get the prompt.**
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
npx taste-check judge --emit
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
It prints the prompt, which already contains the framing and the user's
|
|
36
|
+
checklist. It exits 1 and prints nothing usable if there are no screenshots or
|
|
37
|
+
no checklist, which is deliberate: a judge with nothing to look at must not
|
|
38
|
+
produce a verdict.
|
|
39
|
+
|
|
40
|
+
**3. Ask a fresh agent.** Spawn a new general-purpose agent on the strongest
|
|
41
|
+
reasoning tier available. Give it:
|
|
42
|
+
|
|
43
|
+
- the prompt from step 2, verbatim
|
|
44
|
+
- the screenshot files it names
|
|
45
|
+
|
|
46
|
+
Give it nothing else. No summary of what changed. No statement of intent. No
|
|
47
|
+
prior conversation. Do not mention which lines you expect to fail, do not say
|
|
48
|
+
what you already fixed, and do not add a checklist item of your own. Every one
|
|
49
|
+
of those turns the verdict into your opinion with a second signature on it.
|
|
50
|
+
|
|
51
|
+
Ask it to reply with the JSON the prompt specifies and nothing else.
|
|
52
|
+
|
|
53
|
+
**4. Grade the reply.**
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
npx taste-check judge --verdict reply.json
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
or pipe it on stdin with `-`. This checks the reply against the checklist:
|
|
60
|
+
every line answered exactly once, no invented lines, valid verdicts. A judge
|
|
61
|
+
that quietly drops the hardest line is the failure this catches.
|
|
62
|
+
|
|
63
|
+
Exit code 1 means the judge could not run, or a verdict blocked under
|
|
64
|
+
`failOn: "fail"`. Verdicts are otherwise advisory and print as notes.
|
|
65
|
+
|
|
66
|
+
## Reporting back
|
|
67
|
+
|
|
68
|
+
Report what the judge said, including the passes. Do not soften a `fail` and do
|
|
69
|
+
not quietly drop an `unsure`.
|
|
70
|
+
|
|
71
|
+
If you disagree with a verdict, say so as a disagreement and leave the verdict
|
|
72
|
+
standing. "The judge flagged X, I think it is wrong because Y" is useful. Not
|
|
73
|
+
mentioning X is not.
|
|
74
|
+
|
|
75
|
+
Fix what you can, then run the whole thing again from step 1. A verdict on the
|
|
76
|
+
old screenshots is not a verdict on the new ones.
|
|
77
|
+
|
|
78
|
+
## What this skill does not contain
|
|
79
|
+
|
|
80
|
+
Any design rules. The checklist is the user's file, named in their config, and
|
|
81
|
+
if it is empty then this reports nothing and that is correct. A checklist that
|
|
82
|
+
arrived with a tool is somebody else's taste wearing the tool's authority.
|
package/src/config.mjs
CHANGED
|
@@ -133,14 +133,16 @@ function validateTreatments(treatments, errors) {
|
|
|
133
133
|
|
|
134
134
|
function validateJudge(judge, errors) {
|
|
135
135
|
rejectUnknown(judge, ['checklist', 'shots', 'shotCommand', 'command', 'failOn'], 'judge', errors);
|
|
136
|
-
|
|
137
|
-
|
|
136
|
+
if (typeof judge.checklist !== 'string' || !judge.checklist) {
|
|
137
|
+
errors.push('judge.checklist must be a non-empty string');
|
|
138
|
+
}
|
|
139
|
+
// command is optional: an agent carrying the call with --emit and --verdict
|
|
140
|
+
// never needs one, and demanding a placeholder would be theatre.
|
|
141
|
+
for (const key of ['command', 'shotCommand']) {
|
|
142
|
+
if (judge[key] !== undefined && (typeof judge[key] !== 'string' || !judge[key])) {
|
|
138
143
|
errors.push(`judge.${key} must be a non-empty string`);
|
|
139
144
|
}
|
|
140
145
|
}
|
|
141
|
-
if (judge.shotCommand !== undefined && (typeof judge.shotCommand !== 'string' || !judge.shotCommand)) {
|
|
142
|
-
errors.push('judge.shotCommand must be a non-empty string');
|
|
143
|
-
}
|
|
144
146
|
stringArray(judge.shots, 'judge.shots', errors);
|
|
145
147
|
if (judge.failOn !== undefined && judge.failOn !== 'never' && judge.failOn !== 'fail') {
|
|
146
148
|
errors.push(
|
package/src/index.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export { runContrast } from './contrast.mjs';
|
|
6
6
|
export { runTreatments } from './treatments.mjs';
|
|
7
|
-
export { runJudge, buildPrompt, checklistLines, extractJson } from './judge.mjs';
|
|
7
|
+
export { runJudge, prepareJudge, gradeVerdict, buildPrompt, checklistLines, checklistEntries, extractJson } from './judge.mjs';
|
|
8
8
|
export { runRuntime } from './runtime.mjs';
|
|
9
9
|
export { connect, findBrowser } from './cdp.mjs';
|
|
10
10
|
export { load, validate } from './config.mjs';
|
package/src/judge.mjs
CHANGED
|
@@ -154,64 +154,98 @@ export function extractJson(stdout) {
|
|
|
154
154
|
|
|
155
155
|
const VERDICTS = new Set(['pass', 'fail', 'unsure']);
|
|
156
156
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
157
|
+
const empty = (problems, failOn) => ({ name: 'judge', findings: [], problems, failOn, summary: '' });
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Everything the judge needs before a model is involved: the screenshots, the
|
|
161
|
+
* checklist, and the prompt built from them.
|
|
162
|
+
*
|
|
163
|
+
* Split out so the model call does not have to be a subprocess. A shell
|
|
164
|
+
* command is one way to reach a model and a poor one for an agent, which is
|
|
165
|
+
* already a model and can spawn a genuinely fresh context of its own rather
|
|
166
|
+
* than shelling out to a second copy of itself. This half prepares the call,
|
|
167
|
+
* `gradeVerdict` below checks the answer, and in between the transport is
|
|
168
|
+
* somebody else's problem.
|
|
169
|
+
*
|
|
170
|
+
* The preconditions are enforced here rather than at grading time. Handing
|
|
171
|
+
* back a prompt for zero screenshots would produce a confident verdict about
|
|
172
|
+
* nothing.
|
|
173
|
+
*/
|
|
174
|
+
export function prepareJudge(config, cwd) {
|
|
175
|
+
const { checklist: checklistPath, shots = [], shotCommand, failOn = 'never' } = config;
|
|
161
176
|
|
|
162
177
|
if (shotCommand) {
|
|
163
178
|
const made = runCommand(shotCommand, [], '', cwd);
|
|
164
|
-
if (!made.ok) {
|
|
165
|
-
problems.push(made.reason);
|
|
166
|
-
return { name: 'judge', findings, problems, failOn, summary: '' };
|
|
167
|
-
}
|
|
179
|
+
if (!made.ok) return { ok: false, result: empty([made.reason], failOn) };
|
|
168
180
|
}
|
|
169
181
|
|
|
170
182
|
const images = expand(shots, cwd);
|
|
171
183
|
if (!images.length) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
184
|
+
return {
|
|
185
|
+
ok: false,
|
|
186
|
+
result: empty(
|
|
187
|
+
[
|
|
188
|
+
`no screenshots matched ${shots.map((s) => `"${s}"`).join(', ')}. ` +
|
|
189
|
+
`A judge with nothing to look at cannot fail, so it does not get to pass either.`,
|
|
190
|
+
],
|
|
191
|
+
failOn,
|
|
192
|
+
),
|
|
193
|
+
};
|
|
177
194
|
}
|
|
178
195
|
|
|
179
196
|
let entries;
|
|
180
197
|
try {
|
|
181
198
|
entries = checklistEntries(readFileSync(resolve(cwd, checklistPath), 'utf8'));
|
|
182
199
|
} catch {
|
|
183
|
-
|
|
184
|
-
return { name: 'judge', findings, problems, failOn, summary: '' };
|
|
200
|
+
return { ok: false, result: empty([`cannot read the checklist at ${checklistPath}`], failOn) };
|
|
185
201
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
202
|
+
if (!entries.length) {
|
|
203
|
+
return {
|
|
204
|
+
ok: false,
|
|
205
|
+
result: empty(
|
|
206
|
+
[
|
|
207
|
+
`${checklistPath} has no checklist lines in it. Lines to judge are list ` +
|
|
208
|
+
`items ("- ..." or "1. ..."); everything else is treated as prose.`,
|
|
209
|
+
],
|
|
210
|
+
failOn,
|
|
211
|
+
),
|
|
212
|
+
};
|
|
194
213
|
}
|
|
195
214
|
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
215
|
+
const relative = images.map((i) => label(i, cwd));
|
|
216
|
+
return {
|
|
217
|
+
ok: true,
|
|
218
|
+
entries,
|
|
219
|
+
images,
|
|
220
|
+
relativeImages: relative,
|
|
221
|
+
prompt: buildPrompt(entries.map((e) => e.text), relative),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Check a reply against the checklist it was supposed to answer.
|
|
227
|
+
*
|
|
228
|
+
* This is the half that makes the whole thing trustworthy, and it does not
|
|
229
|
+
* care where the reply came from. A judge that quietly drops the hardest line
|
|
230
|
+
* is the failure mode to guard: every remaining verdict says pass, and the one
|
|
231
|
+
* nobody answered is the one that mattered.
|
|
232
|
+
*/
|
|
233
|
+
export function gradeVerdict(reply, prepared, config) {
|
|
234
|
+
const { checklist: checklistPath, failOn = 'never' } = config;
|
|
235
|
+
const findings = [];
|
|
236
|
+
const problems = [];
|
|
237
|
+
const { entries, images } = prepared;
|
|
238
|
+
const lines = entries.map((e) => e.text);
|
|
239
|
+
const lineNumbers = new Map(entries.map((e) => [e.text, e.line]));
|
|
201
240
|
|
|
202
|
-
const parsed = extractJson(reply
|
|
241
|
+
const parsed = extractJson(reply);
|
|
203
242
|
if (!parsed.ok) {
|
|
204
|
-
|
|
205
|
-
return { name: 'judge', findings, problems, failOn, summary: '' };
|
|
243
|
+
return empty([`${parsed.reason}. The judge must reply with the documented JSON shape.`], failOn);
|
|
206
244
|
}
|
|
207
245
|
if (!Array.isArray(parsed.value.findings)) {
|
|
208
|
-
|
|
209
|
-
return { name: 'judge', findings, problems, failOn, summary: '' };
|
|
246
|
+
return empty(['the reply has no "findings" array'], failOn);
|
|
210
247
|
}
|
|
211
248
|
|
|
212
|
-
// Cross-check both directions. A judge that quietly drops the hardest line
|
|
213
|
-
// is the failure mode to guard: the remaining verdicts all say pass, and
|
|
214
|
-
// the line nobody answered is the one that mattered.
|
|
215
249
|
const wanted = new Set(lines);
|
|
216
250
|
const answered = new Set();
|
|
217
251
|
for (const f of parsed.value.findings) {
|
|
@@ -246,6 +280,27 @@ export function runJudge(config, cwd) {
|
|
|
246
280
|
failOn,
|
|
247
281
|
summary: `${lines.length} ${lines.length === 1 ? 'line' : 'lines'} against ${images.length} ${
|
|
248
282
|
images.length === 1 ? 'screenshot' : 'screenshots'
|
|
249
|
-
} (${
|
|
283
|
+
} (${prepared.relativeImages.join(', ')})`,
|
|
250
284
|
};
|
|
251
285
|
}
|
|
286
|
+
|
|
287
|
+
/** The whole thing, with a configured command as the transport. */
|
|
288
|
+
export function runJudge(config, cwd) {
|
|
289
|
+
const { command, failOn = 'never' } = config;
|
|
290
|
+
const prepared = prepareJudge(config, cwd);
|
|
291
|
+
if (!prepared.ok) return prepared.result;
|
|
292
|
+
|
|
293
|
+
if (!command) {
|
|
294
|
+
return empty(
|
|
295
|
+
[
|
|
296
|
+
'judge.command is not set, so there is nothing to ask. Set it, or use ' +
|
|
297
|
+
'`taste-check judge --emit` and `--verdict` to let an agent carry the call.',
|
|
298
|
+
],
|
|
299
|
+
failOn,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const reply = runCommand(command, prepared.images, prepared.prompt, cwd);
|
|
304
|
+
if (!reply.ok) return empty([reply.reason], failOn);
|
|
305
|
+
return gradeVerdict(reply.stdout, prepared, config);
|
|
306
|
+
}
|