ccqa 1.46.1 → 1.47.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/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.46.1",
3
+ "version": "1.47.0",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {
@@ -26,6 +26,10 @@
26
26
  "./hub-client": {
27
27
  "types": "./dist/hub-client/index.d.mts",
28
28
  "import": "./dist/hub-client/index.mjs"
29
+ },
30
+ "./judge": {
31
+ "types": "./dist/runtime/judge.d.mts",
32
+ "import": "./dist/runtime/judge.mjs"
29
33
  }
30
34
  },
31
35
  "files": [
@@ -0,0 +1,46 @@
1
+ //#region src/runtime/judge.d.ts
2
+ /** One decided claim. */
3
+ interface Verdict {
4
+ ok: boolean;
5
+ /** Why, in one sentence. Carried into the failure message so a red test says what was wrong. */
6
+ reason: string;
7
+ }
8
+ /**
9
+ * The slice of Playwright's `Page` this needs. Structural because ccqa does
10
+ * not depend on Playwright, which also lets a caller pass anything else that
11
+ * can hand over text.
12
+ */
13
+ interface TextSource {
14
+ innerText(selector: string): Promise<string>;
15
+ }
16
+ /**
17
+ * Fails the test unless a model agrees the claim holds for the text read from
18
+ * `from` (a selector; omitted, the page's body). The reason the model gave
19
+ * rides in the failure, so a red test says what was wrong.
20
+ *
21
+ * A selector matching several elements judges the first, as Playwright's
22
+ * page-level `innerText` does — narrow it if that is not what you mean.
23
+ */
24
+ declare function judgeByLlm(page: TextSource, claim: string, from?: string): Promise<void>;
25
+ interface ClaimInput {
26
+ /** The claim to decide, as written in the spec's `judgeByLlm`. */
27
+ claim: string;
28
+ /** The text it is decided against. */
29
+ text: string;
30
+ model?: string;
31
+ cwd?: string;
32
+ }
33
+ /**
34
+ * Decides a claim about text a run cannot predict — a generated answer, a
35
+ * summary. A model that will not answer, or answers something this cannot
36
+ * read, is an unmade decision rather than a passing one: it throws, so a
37
+ * claim never goes silently unjudged.
38
+ */
39
+ declare function decideClaim(input: ClaimInput): Promise<Verdict>;
40
+ /**
41
+ * The model is asked for bare JSON but sometimes wraps it in prose or a fence.
42
+ * An answer with no readable verdict is a failure to decide, not a false one.
43
+ */
44
+ declare function parseVerdict(answer: string): Verdict;
45
+ //#endregion
46
+ export { ClaimInput, TextSource, Verdict, decideClaim, judgeByLlm, parseVerdict };
@@ -0,0 +1,78 @@
1
+ import { a as truncate, p as invokeClaudeStreaming, r as extractJsonCandidates } from "../diagnose-CSQzwS5f.mjs";
2
+ //#region src/runtime/judge.ts
3
+ const SYSTEM_PROMPT = [
4
+ "You decide whether a claim holds for a piece of text taken from a web page under test.",
5
+ "",
6
+ "Judge only what the claim says. Do not reward text that is merely well-formed,",
7
+ "and do not fail text for wording, length, or formatting the claim does not mention.",
8
+ "An empty or error-like text fails any claim about content.",
9
+ "",
10
+ "Answer with one line of JSON and nothing else: {\"ok\": true|false, \"reason\": \"<one sentence>\"}"
11
+ ].join("\n");
12
+ /** Beyond this the tail is dropped, so one runaway page cannot fill the model's context. */
13
+ const MAX_TEXT_CHARS = 2e4;
14
+ /** A judge runs inside a test, so a turn that will not finish has to fail rather than hold the run. */
15
+ const JUDGE_TIMEOUT_MS = 6e4;
16
+ /**
17
+ * Fails the test unless a model agrees the claim holds for the text read from
18
+ * `from` (a selector; omitted, the page's body). The reason the model gave
19
+ * rides in the failure, so a red test says what was wrong.
20
+ *
21
+ * A selector matching several elements judges the first, as Playwright's
22
+ * page-level `innerText` does — narrow it if that is not what you mean.
23
+ */
24
+ async function judgeByLlm(page, claim, from = "body") {
25
+ const verdict = await decideClaim({
26
+ claim,
27
+ text: await page.innerText(from)
28
+ });
29
+ if (!verdict.ok) throw new Error(`judgeByLlm: the claim did not hold (${verdict.reason || "no reason given"})\n claim: ${claim}\n read from: ${from}`);
30
+ }
31
+ /**
32
+ * Decides a claim about text a run cannot predict — a generated answer, a
33
+ * summary. A model that will not answer, or answers something this cannot
34
+ * read, is an unmade decision rather than a passing one: it throws, so a
35
+ * claim never goes silently unjudged.
36
+ */
37
+ async function decideClaim(input) {
38
+ const { result, isError, errorDetail } = await invokeClaudeStreaming({
39
+ prompt: [
40
+ "## Claim",
41
+ input.claim.trim(),
42
+ "",
43
+ "## Text",
44
+ truncate(input.text.trim(), MAX_TEXT_CHARS) || "(the page yielded no text)"
45
+ ].join("\n"),
46
+ systemPrompt: SYSTEM_PROMPT,
47
+ allowedTools: [],
48
+ maxTurns: 1,
49
+ disableThinking: true,
50
+ timeoutMs: JUDGE_TIMEOUT_MS,
51
+ ...input.model ? { model: input.model } : {},
52
+ ...input.cwd ? { cwd: input.cwd } : {}
53
+ }, () => {});
54
+ if (isError) throw new Error(`judgeByLlm: the model did not answer (${errorDetail || result})`);
55
+ return parseVerdict(result);
56
+ }
57
+ /**
58
+ * The model is asked for bare JSON but sometimes wraps it in prose or a fence.
59
+ * An answer with no readable verdict is a failure to decide, not a false one.
60
+ */
61
+ function parseVerdict(answer) {
62
+ for (const candidate of extractJsonCandidates(answer)) {
63
+ let parsed;
64
+ try {
65
+ parsed = JSON.parse(candidate);
66
+ } catch {
67
+ continue;
68
+ }
69
+ const verdict = parsed;
70
+ if (typeof verdict?.ok === "boolean") return {
71
+ ok: verdict.ok,
72
+ reason: typeof verdict.reason === "string" ? verdict.reason : ""
73
+ };
74
+ }
75
+ throw new Error(`judgeByLlm: no verdict in the model's answer (${truncate(answer, 200)})`);
76
+ }
77
+ //#endregion
78
+ export { decideClaim, judgeByLlm, parseVerdict };
@@ -1,4 +1,4 @@
1
- import { i as sanitizeStepId, t as EVIDENCE_DIR_ENV } from "../evidence-constants-C425F7ZG.mjs";
1
+ import { i as sanitizeStepId, t as EVIDENCE_DIR_ENV } from "../evidence-constants-Cm_S_5od.mjs";
2
2
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  //#region src/runtime/step-evidence.ts
@@ -1,5 +1,5 @@
1
- import { i as sanitizeStepId, n as FAILURE_SOURCE, r as FAILURE_STEP_ID, t as EVIDENCE_DIR_ENV } from "../evidence-constants-C425F7ZG.mjs";
2
- import { n as spawnAB, t as sleepSync } from "../spawn-ab-Bm34WBui.mjs";
1
+ import { i as sanitizeStepId, n as FAILURE_SOURCE, r as FAILURE_STEP_ID, t as EVIDENCE_DIR_ENV } from "../evidence-constants-Cm_S_5od.mjs";
2
+ import { n as spawnAB, t as sleepSync } from "../spawn-ab-CR_Sr7wh.mjs";
3
3
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
4
4
  import { dirname, isAbsolute, join, resolve } from "node:path";
5
5
  //#region src/runtime/test-helpers.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.46.1",
3
+ "version": "1.47.0",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {
@@ -26,6 +26,10 @@
26
26
  "./hub-client": {
27
27
  "types": "./dist/hub-client/index.d.mts",
28
28
  "import": "./dist/hub-client/index.mjs"
29
+ },
30
+ "./judge": {
31
+ "types": "./dist/runtime/judge.d.mts",
32
+ "import": "./dist/runtime/judge.mjs"
29
33
  }
30
34
  },
31
35
  "files": [