@cairn-tool/cairn 3.4.0 → 3.6.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 -4
- package/dist/agent/manifest.d.ts +17 -0
- package/dist/agent/manifest.js +59 -1
- package/dist/agent/manifest.js.map +1 -1
- package/dist/agent/parser.js +209 -24
- package/dist/agent/parser.js.map +1 -1
- package/dist/agent/render.js +64 -3
- package/dist/agent/render.js.map +1 -1
- package/dist/cli.js +46 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/qa-list.d.ts +7 -0
- package/dist/commands/qa-list.js +69 -0
- package/dist/commands/qa-list.js.map +1 -0
- package/dist/commands/qa-run.d.ts +3 -0
- package/dist/commands/qa-run.js +106 -0
- package/dist/commands/qa-run.js.map +1 -0
- package/dist/commands/qa-summary.d.ts +6 -0
- package/dist/commands/qa-summary.js +38 -0
- package/dist/commands/qa-summary.js.map +1 -0
- package/dist/config-schema.d.ts +2 -0
- package/dist/config-schema.js +4 -0
- package/dist/config-schema.js.map +1 -1
- package/dist/config.d.ts +12 -2
- package/dist/config.js +36 -7
- package/dist/config.js.map +1 -1
- package/dist/contract/registry.js +43 -0
- package/dist/contract/registry.js.map +1 -1
- package/dist/contract/schemas/index.js +2 -0
- package/dist/contract/schemas/index.js.map +1 -1
- package/dist/contract/schemas/qa.d.ts +2 -0
- package/dist/contract/schemas/qa.js +111 -0
- package/dist/contract/schemas/qa.js.map +1 -0
- package/dist/qa/agent.d.ts +25 -0
- package/dist/qa/agent.js +142 -0
- package/dist/qa/agent.js.map +1 -0
- package/dist/qa/agents/claude-code.d.ts +2 -0
- package/dist/qa/agents/claude-code.js +121 -0
- package/dist/qa/agents/claude-code.js.map +1 -0
- package/dist/qa/agents/cursor.d.ts +5 -0
- package/dist/qa/agents/cursor.js +143 -0
- package/dist/qa/agents/cursor.js.map +1 -0
- package/dist/qa/agents/index.d.ts +32 -0
- package/dist/qa/agents/index.js +83 -0
- package/dist/qa/agents/index.js.map +1 -0
- package/dist/qa/agents/types.d.ts +61 -0
- package/dist/qa/agents/types.js +18 -0
- package/dist/qa/agents/types.js.map +1 -0
- package/dist/qa/async.d.ts +27 -0
- package/dist/qa/async.js +91 -0
- package/dist/qa/async.js.map +1 -0
- package/dist/qa/casefile.d.ts +35 -0
- package/dist/qa/casefile.js +181 -0
- package/dist/qa/casefile.js.map +1 -0
- package/dist/qa/cases.d.ts +51 -0
- package/dist/qa/cases.js +144 -0
- package/dist/qa/cases.js.map +1 -0
- package/dist/qa/config.d.ts +50 -0
- package/dist/qa/config.js +105 -0
- package/dist/qa/config.js.map +1 -0
- package/dist/qa/demo.d.ts +10 -0
- package/dist/qa/demo.js +194 -0
- package/dist/qa/demo.js.map +1 -0
- package/dist/qa/format.d.ts +38 -0
- package/dist/qa/format.js +208 -0
- package/dist/qa/format.js.map +1 -0
- package/dist/qa/harness.d.ts +68 -0
- package/dist/qa/harness.js +528 -0
- package/dist/qa/harness.js.map +1 -0
- package/dist/qa/keys.d.ts +12 -0
- package/dist/qa/keys.js +96 -0
- package/dist/qa/keys.js.map +1 -0
- package/dist/qa/layout.d.ts +102 -0
- package/dist/qa/layout.js +278 -0
- package/dist/qa/layout.js.map +1 -0
- package/dist/qa/lock.d.ts +5 -0
- package/dist/qa/lock.js +114 -0
- package/dist/qa/lock.js.map +1 -0
- package/dist/qa/options.d.ts +38 -0
- package/dist/qa/options.js +165 -0
- package/dist/qa/options.js.map +1 -0
- package/dist/qa/outcome.d.ts +30 -0
- package/dist/qa/outcome.js +162 -0
- package/dist/qa/outcome.js.map +1 -0
- package/dist/qa/reports.d.ts +51 -0
- package/dist/qa/reports.js +121 -0
- package/dist/qa/reports.js.map +1 -0
- package/dist/qa/schedule.d.ts +55 -0
- package/dist/qa/schedule.js +145 -0
- package/dist/qa/schedule.js.map +1 -0
- package/dist/qa/screen.d.ts +31 -0
- package/dist/qa/screen.js +146 -0
- package/dist/qa/screen.js.map +1 -0
- package/dist/qa/slot.d.ts +28 -0
- package/dist/qa/slot.js +126 -0
- package/dist/qa/slot.js.map +1 -0
- package/dist/qa/tracker.d.ts +36 -0
- package/dist/qa/tracker.js +357 -0
- package/dist/qa/tracker.js.map +1 -0
- package/dist/query/execute.js +0 -0
- package/dist/query/execute.js.map +1 -1
- package/dist/scripts/resolve.js +1 -4
- package/dist/scripts/resolve.js.map +1 -1
- package/dist/usage/providers/cursor.js +1 -1
- package/dist/usage/providers/cursor.js.map +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { isInside } from "../config-schema.js";
|
|
4
|
+
import { runtime } from "../runtime.js";
|
|
5
|
+
import { defaultModels, resolveBinary, SUPPORTED_AGENTS } from "./agents/index.js";
|
|
6
|
+
import { catalog, discover, displayCommandFor, elidedCommandFor, shellJoin, } from "./cases.js";
|
|
7
|
+
import { DEFAULT_PARALLEL, DEFAULT_TIMEOUT_MIN, parseOnly, resolveDir, UserError, } from "./config.js";
|
|
8
|
+
export const NO_CASES = "no eligible cases (every _plans/tc-N.yaml already has a tc-N/ folder)";
|
|
9
|
+
function isDir(path) {
|
|
10
|
+
try {
|
|
11
|
+
return statSync(path).isDirectory();
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function positiveInt(raw, flag, fallback) {
|
|
18
|
+
if (raw === undefined || raw === "")
|
|
19
|
+
return fallback;
|
|
20
|
+
const parsed = Number.parseInt(raw, 10);
|
|
21
|
+
if (!Number.isInteger(parsed) || String(parsed) !== String(raw).trim() || parsed < 1) {
|
|
22
|
+
throw new UserError(`${flag} must be an integer >= 1`);
|
|
23
|
+
}
|
|
24
|
+
return parsed;
|
|
25
|
+
}
|
|
26
|
+
function anyInt(raw, flag) {
|
|
27
|
+
if (raw === undefined || raw === "")
|
|
28
|
+
return null;
|
|
29
|
+
const parsed = Number.parseInt(raw, 10);
|
|
30
|
+
if (!Number.isInteger(parsed) || String(parsed) !== String(raw).trim()) {
|
|
31
|
+
throw new UserError(`${flag} must be an integer`);
|
|
32
|
+
}
|
|
33
|
+
return parsed;
|
|
34
|
+
}
|
|
35
|
+
export function refuseWindows() {
|
|
36
|
+
if (process.platform === "win32") {
|
|
37
|
+
throw new UserError("qa is POSIX-only; Windows support is tracked in docs/future-enhancements.md");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function parseModelFlags(values, defaults) {
|
|
41
|
+
const models = { ...defaults };
|
|
42
|
+
if (!values || values.length === 0) {
|
|
43
|
+
const unique = new Set(Object.values(models));
|
|
44
|
+
return { label: unique.size === 1 ? [...unique][0] : "per-agent", models };
|
|
45
|
+
}
|
|
46
|
+
for (const raw of values) {
|
|
47
|
+
const eq = raw.indexOf("=");
|
|
48
|
+
if (eq === -1) {
|
|
49
|
+
if (!raw)
|
|
50
|
+
throw new UserError("--model must not be empty");
|
|
51
|
+
for (const name of Object.keys(models))
|
|
52
|
+
models[name] = raw;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
const agent = raw.slice(0, eq);
|
|
56
|
+
const slug = raw.slice(eq + 1);
|
|
57
|
+
if (!SUPPORTED_AGENTS.includes(agent)) {
|
|
58
|
+
throw new UserError(`--model agent must be one of ${SUPPORTED_AGENTS.join(", ")}, not ${JSON.stringify(agent)}`);
|
|
59
|
+
}
|
|
60
|
+
if (!slug)
|
|
61
|
+
throw new UserError(`--model ${agent}= must not be empty`);
|
|
62
|
+
models[agent] = slug;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const unique = new Set(Object.values(models));
|
|
66
|
+
return { label: unique.size === 1 ? [...unique][0] : "per-agent", models };
|
|
67
|
+
}
|
|
68
|
+
function qaConfig() {
|
|
69
|
+
return runtime().config.qa ?? {};
|
|
70
|
+
}
|
|
71
|
+
function modelValues(raw) {
|
|
72
|
+
if (raw.model === undefined) {
|
|
73
|
+
const configured = qaConfig().model;
|
|
74
|
+
return configured === undefined ? undefined : [configured];
|
|
75
|
+
}
|
|
76
|
+
return Array.isArray(raw.model) ? raw.model : [raw.model];
|
|
77
|
+
}
|
|
78
|
+
export function buildRunOptions(raw, format) {
|
|
79
|
+
refuseWindows();
|
|
80
|
+
const configured = qaConfig();
|
|
81
|
+
if (!raw.repo)
|
|
82
|
+
throw new UserError("--repo is required");
|
|
83
|
+
const repo = resolveDir(raw.repo, process.cwd(), "--repo");
|
|
84
|
+
const runsRaw = raw.runsDir ?? configured.runsDir;
|
|
85
|
+
if (!runsRaw)
|
|
86
|
+
throw new UserError("--runs-dir is required");
|
|
87
|
+
const runs = resolveDir(runsRaw, repo, "--runs-dir");
|
|
88
|
+
if (!isInside(repo, runs)) {
|
|
89
|
+
throw new UserError(`--runs-dir must resolve under --repo: ${runs}`);
|
|
90
|
+
}
|
|
91
|
+
if (!isDir(join(runs, "_plans")))
|
|
92
|
+
throw new UserError(`no _plans/ under ${runs}`);
|
|
93
|
+
const parallel = positiveInt(raw.parallel, "--parallel", configured.parallel ?? DEFAULT_PARALLEL);
|
|
94
|
+
const { label, models } = parseModelFlags(modelValues(raw), defaultModels());
|
|
95
|
+
const only = parseOnly(raw.only ?? null);
|
|
96
|
+
const loaded = catalog(runs, repo, models);
|
|
97
|
+
if (loaded.errors.length > 0) {
|
|
98
|
+
throw new UserError(`${loaded.errors.length} case file(s) could not be loaded:\n ${loaded.errors.join("\n ")}`);
|
|
99
|
+
}
|
|
100
|
+
const allCases = loaded.cases;
|
|
101
|
+
const cases = discover(allCases, only, anyInt(raw.limit, "--limit"));
|
|
102
|
+
const agent = raw.agent ?? configured.agent ?? null;
|
|
103
|
+
if (agent)
|
|
104
|
+
resolveBinary(allCases[0]?.agentName ?? SUPPORTED_AGENTS[0], agent);
|
|
105
|
+
const json = format === "json";
|
|
106
|
+
const ci = Boolean(process.env["CI"]);
|
|
107
|
+
const noTui = raw.noTui === true || json || ci || format === "json";
|
|
108
|
+
return {
|
|
109
|
+
options: {
|
|
110
|
+
repo,
|
|
111
|
+
runsDir: runs,
|
|
112
|
+
parallel,
|
|
113
|
+
model: label,
|
|
114
|
+
only,
|
|
115
|
+
limit: anyInt(raw.limit, "--limit"),
|
|
116
|
+
dryRun: raw.dryRun === true,
|
|
117
|
+
showPrompts: raw.showPrompts === true,
|
|
118
|
+
noTui,
|
|
119
|
+
timeoutMs: positiveInt(raw.timeoutMin, "--timeout-min", DEFAULT_TIMEOUT_MIN) * 60 * 1000,
|
|
120
|
+
agent,
|
|
121
|
+
json,
|
|
122
|
+
},
|
|
123
|
+
cases,
|
|
124
|
+
allCases,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* A relative --runs-dir resolves against --repo when one is given, and against the cwd
|
|
129
|
+
* otherwise — the same rule in every subcommand. `qa run` resolving against --repo while
|
|
130
|
+
* `qa list` resolved against the cwd meant identical flags named different directories
|
|
131
|
+
* depending on where you stood.
|
|
132
|
+
*/
|
|
133
|
+
export function resolveRunsDir(raw, flag = "--runs-dir", repo) {
|
|
134
|
+
refuseWindows();
|
|
135
|
+
const configured = qaConfig().runsDir;
|
|
136
|
+
const value = raw ?? configured;
|
|
137
|
+
if (!value)
|
|
138
|
+
throw new UserError(`${flag} is required`);
|
|
139
|
+
return resolveDir(value, repo ?? process.cwd(), flag);
|
|
140
|
+
}
|
|
141
|
+
export function printDryRun(prepared) {
|
|
142
|
+
const { cases, options } = prepared;
|
|
143
|
+
if (cases.length === 0)
|
|
144
|
+
return `${NO_CASES}\n`;
|
|
145
|
+
const out = [];
|
|
146
|
+
out.push(`${cases.length} eligible case(s)`);
|
|
147
|
+
out.push(` repo ${options.repo}`);
|
|
148
|
+
if (options.agent)
|
|
149
|
+
out.push(` agent ${options.agent}`);
|
|
150
|
+
out.push("");
|
|
151
|
+
for (const kase of cases) {
|
|
152
|
+
out.push(`${kase.name} ${kase.fileRel} ${kase.model} ${kase.constraint}`);
|
|
153
|
+
out.push(` cwd ${kase.repo}`);
|
|
154
|
+
const argv = options.showPrompts
|
|
155
|
+
? displayCommandFor(kase, options.agent)
|
|
156
|
+
: elidedCommandFor(kase, options.agent);
|
|
157
|
+
out.push(` ${shellJoin(argv)}`);
|
|
158
|
+
out.push("");
|
|
159
|
+
}
|
|
160
|
+
return `${out.join("\n")}\n`;
|
|
161
|
+
}
|
|
162
|
+
export function caseStatus(kase) {
|
|
163
|
+
return existsSync(kase.outDir) ? "done" : "pending";
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=options.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options.js","sourceRoot":"","sources":["../../src/qa/options.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Case } from "./cases.js";
|
|
2
|
+
export interface Usage {
|
|
3
|
+
inputTokens?: number;
|
|
4
|
+
outputTokens?: number;
|
|
5
|
+
cacheReadTokens?: number;
|
|
6
|
+
cacheWriteTokens?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface UsageCells {
|
|
9
|
+
inn: string;
|
|
10
|
+
out: string;
|
|
11
|
+
cache_r: string;
|
|
12
|
+
cache_w: string;
|
|
13
|
+
}
|
|
14
|
+
/** PASS / FAIL / UNVERIFIED from `_results.md`, or — if unreadable. */
|
|
15
|
+
export declare function readVerdict(folder: string): string;
|
|
16
|
+
/** Returns null only when NO usage object was seen at all — zeros are meaningful. */
|
|
17
|
+
export declare function usageTotal(usages: readonly (Usage | null | undefined)[]): Usage | null;
|
|
18
|
+
export declare function usageFragment(usage: Usage): string;
|
|
19
|
+
export declare function usageCells(usage: Usage | null | undefined): UsageCells;
|
|
20
|
+
/** Exact token counts for markdown logs, or em dashes. */
|
|
21
|
+
export declare function usageMd(usage: Usage | null | undefined): UsageCells;
|
|
22
|
+
export declare function outcomeLabel(verdict: string): string;
|
|
23
|
+
export declare const FAIL_OUTCOME: Readonly<Record<string, string>>;
|
|
24
|
+
export declare const STATUS_COLOR: Readonly<Record<string, string>>;
|
|
25
|
+
export declare const OUTCOME_COLOR: Readonly<Record<string, string>>;
|
|
26
|
+
/**
|
|
27
|
+
* Return [status, note]. Promotes `tc-N-temp/` to `tc-N/` only on a clean success.
|
|
28
|
+
* The order of these checks is load-bearing.
|
|
29
|
+
*/
|
|
30
|
+
export declare function classify(kase: Case, exitCode: number | null, timedOut: boolean): [string, string];
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { existsSync, readFileSync, renameSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import { DIM, GREEN, RED, YELLOW, fmtCount } from "./format.js";
|
|
4
|
+
import { DONE_OK, REPORT, RESULTS } from "./config.js";
|
|
5
|
+
const VERDICTS = ["PASS", "FAIL", "UNVERIFIED"];
|
|
6
|
+
const DASH = "—";
|
|
7
|
+
/** PASS / FAIL / UNVERIFIED from `_results.md`, or — if unreadable. */
|
|
8
|
+
export function readVerdict(folder) {
|
|
9
|
+
const path = join(folder, RESULTS);
|
|
10
|
+
let text;
|
|
11
|
+
try {
|
|
12
|
+
// No statSync guard: readFileSync already throws ENOENT for a missing file
|
|
13
|
+
// and EISDIR for a directory, both of which land here as DASH. Checking
|
|
14
|
+
// first bought nothing and opened a window between the check and the read.
|
|
15
|
+
text = readFileSync(path, "utf8");
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return DASH;
|
|
19
|
+
}
|
|
20
|
+
if (text.includes("## 4.")) {
|
|
21
|
+
const after = text.slice(text.indexOf("## 4.") + "## 4.".length);
|
|
22
|
+
const fifth = after.indexOf("## 5.");
|
|
23
|
+
const body = fifth === -1 ? after : after.slice(0, fifth);
|
|
24
|
+
for (const verdict of VERDICTS) {
|
|
25
|
+
if (new RegExp(String.raw `^\*\*${verdict}\*\*`, "m").test(body))
|
|
26
|
+
return verdict;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const match = /\*\*Verdict\*\*\s*\|\s*([^|\n]+)/.exec(text);
|
|
30
|
+
if (match) {
|
|
31
|
+
const cell = match[1].trim();
|
|
32
|
+
const word = cell ? (cell.replace(/^\*+|\*+$/g, "").split(/\s+/)[0] ?? "") : "";
|
|
33
|
+
if (VERDICTS.includes(word))
|
|
34
|
+
return word;
|
|
35
|
+
}
|
|
36
|
+
return DASH;
|
|
37
|
+
}
|
|
38
|
+
const int = (value) => Math.trunc(value ?? 0);
|
|
39
|
+
/** Returns null only when NO usage object was seen at all — zeros are meaningful. */
|
|
40
|
+
export function usageTotal(usages) {
|
|
41
|
+
let inn = 0;
|
|
42
|
+
let out = 0;
|
|
43
|
+
let cacheR = 0;
|
|
44
|
+
let cacheW = 0;
|
|
45
|
+
let found = false;
|
|
46
|
+
for (const usage of usages) {
|
|
47
|
+
if (!usage)
|
|
48
|
+
continue;
|
|
49
|
+
found = true;
|
|
50
|
+
inn += int(usage.inputTokens);
|
|
51
|
+
out += int(usage.outputTokens);
|
|
52
|
+
cacheR += int(usage.cacheReadTokens);
|
|
53
|
+
cacheW += int(usage.cacheWriteTokens);
|
|
54
|
+
}
|
|
55
|
+
if (!found)
|
|
56
|
+
return null;
|
|
57
|
+
return { inputTokens: inn, outputTokens: out, cacheReadTokens: cacheR, cacheWriteTokens: cacheW };
|
|
58
|
+
}
|
|
59
|
+
export function usageFragment(usage) {
|
|
60
|
+
const inn = fmtCount(int(usage.inputTokens));
|
|
61
|
+
const out = fmtCount(int(usage.outputTokens));
|
|
62
|
+
const cacheR = fmtCount(int(usage.cacheReadTokens));
|
|
63
|
+
const cacheW = fmtCount(int(usage.cacheWriteTokens));
|
|
64
|
+
return `in ${inn} out ${out} cache-r ${cacheR} cache-w ${cacheW}`;
|
|
65
|
+
}
|
|
66
|
+
export function usageCells(usage) {
|
|
67
|
+
if (!usage)
|
|
68
|
+
return { inn: DASH, out: DASH, cache_r: DASH, cache_w: DASH };
|
|
69
|
+
return {
|
|
70
|
+
inn: fmtCount(int(usage.inputTokens)),
|
|
71
|
+
out: fmtCount(int(usage.outputTokens)),
|
|
72
|
+
cache_r: fmtCount(int(usage.cacheReadTokens)),
|
|
73
|
+
cache_w: fmtCount(int(usage.cacheWriteTokens)),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** Exact token counts for markdown logs, or em dashes. */
|
|
77
|
+
export function usageMd(usage) {
|
|
78
|
+
if (!usage)
|
|
79
|
+
return { inn: DASH, out: DASH, cache_r: DASH, cache_w: DASH };
|
|
80
|
+
return {
|
|
81
|
+
inn: String(int(usage.inputTokens)),
|
|
82
|
+
out: String(int(usage.outputTokens)),
|
|
83
|
+
cache_r: String(int(usage.cacheReadTokens)),
|
|
84
|
+
cache_w: String(int(usage.cacheWriteTokens)),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export function outcomeLabel(verdict) {
|
|
88
|
+
if (verdict === "PASS")
|
|
89
|
+
return "Passed";
|
|
90
|
+
if (verdict === "FAIL")
|
|
91
|
+
return "Failed";
|
|
92
|
+
if (verdict === "UNVERIFIED")
|
|
93
|
+
return "Unverified";
|
|
94
|
+
return verdict;
|
|
95
|
+
}
|
|
96
|
+
export const FAIL_OUTCOME = {
|
|
97
|
+
timeout: "Timeout",
|
|
98
|
+
error: "Error",
|
|
99
|
+
"no-output": "No output",
|
|
100
|
+
"no-folder": "No folder",
|
|
101
|
+
};
|
|
102
|
+
export const STATUS_COLOR = {
|
|
103
|
+
Prior: DIM,
|
|
104
|
+
Pending: "",
|
|
105
|
+
Running: YELLOW,
|
|
106
|
+
Done: GREEN,
|
|
107
|
+
Failed: RED,
|
|
108
|
+
};
|
|
109
|
+
export const OUTCOME_COLOR = {
|
|
110
|
+
Passed: GREEN,
|
|
111
|
+
Failed: RED,
|
|
112
|
+
Unverified: YELLOW,
|
|
113
|
+
};
|
|
114
|
+
const isDir = (path) => {
|
|
115
|
+
try {
|
|
116
|
+
return statSync(path).isDirectory();
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
const isFile = (path) => {
|
|
123
|
+
try {
|
|
124
|
+
return statSync(path).isFile();
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Return [status, note]. Promotes `tc-N-temp/` to `tc-N/` only on a clean success.
|
|
132
|
+
* The order of these checks is load-bearing.
|
|
133
|
+
*/
|
|
134
|
+
export function classify(kase, exitCode, timedOut) {
|
|
135
|
+
if (timedOut)
|
|
136
|
+
return ["timeout", ""];
|
|
137
|
+
if (exitCode !== 0 && exitCode !== null)
|
|
138
|
+
return ["error", ""];
|
|
139
|
+
if (!isDir(kase.tempDir))
|
|
140
|
+
return ["no-folder", "agent never created the temp folder"];
|
|
141
|
+
if (!(isFile(join(kase.tempDir, REPORT)) && isFile(join(kase.tempDir, RESULTS)))) {
|
|
142
|
+
return ["no-output", "temp folder is missing _report.md or _results.md"];
|
|
143
|
+
}
|
|
144
|
+
if (existsSync(kase.outDir)) {
|
|
145
|
+
return [
|
|
146
|
+
"error",
|
|
147
|
+
`${basename(kase.outDir)}/ already exists; left ${basename(kase.tempDir)}/ in place`,
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
renameSync(kase.tempDir, kase.outDir);
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
155
|
+
return [
|
|
156
|
+
"error",
|
|
157
|
+
`could not rename ${basename(kase.tempDir)}/ to ${basename(kase.outDir)}/: ${message}`,
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
return [DONE_OK, ""];
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=outcome.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outcome.js","sourceRoot":"","sources":["../../src/qa/outcome.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Usage } from "./outcome.js";
|
|
2
|
+
export interface Rec {
|
|
3
|
+
name: string;
|
|
4
|
+
/** The model this case actually ran under — cases no longer share one. */
|
|
5
|
+
model: string;
|
|
6
|
+
status: string;
|
|
7
|
+
exitCode: number | null;
|
|
8
|
+
/** Seconds from the start of the session to when this case was dispatched. */
|
|
9
|
+
startedS: number;
|
|
10
|
+
durationS: number;
|
|
11
|
+
/** Python stores a literal int 0 for skipped rows; json.dumps renders that `0`, not `0.0`. */
|
|
12
|
+
durationIsInt: boolean;
|
|
13
|
+
usage: Usage | null;
|
|
14
|
+
note: string;
|
|
15
|
+
sessionId: string;
|
|
16
|
+
logDir: string;
|
|
17
|
+
tools: number;
|
|
18
|
+
}
|
|
19
|
+
export interface SessionStats {
|
|
20
|
+
ok: number;
|
|
21
|
+
fail: number;
|
|
22
|
+
skipped: number;
|
|
23
|
+
durationS: number;
|
|
24
|
+
tools: number;
|
|
25
|
+
usage: Usage | null;
|
|
26
|
+
wallS: number;
|
|
27
|
+
}
|
|
28
|
+
export interface SessionInfo {
|
|
29
|
+
runId: string;
|
|
30
|
+
pid: number;
|
|
31
|
+
/** What `--model` supplied; a case with its own `model:` key overrides it. */
|
|
32
|
+
defaultModel: string;
|
|
33
|
+
parallel: number;
|
|
34
|
+
repo: string;
|
|
35
|
+
runsDir: string;
|
|
36
|
+
sessionLog: string;
|
|
37
|
+
logRoot: string;
|
|
38
|
+
}
|
|
39
|
+
/** A marker that survives JSON.stringify as a string and is unquoted afterwards. */
|
|
40
|
+
export declare const pyFloatMarker: (value: number) => string;
|
|
41
|
+
/**
|
|
42
|
+
* json.dumps(payload, indent=2) — including ensure_ascii, which escapes every non-ASCII
|
|
43
|
+
* character (the em dashes in `outcome` among them). JSON.stringify does not escape those.
|
|
44
|
+
*/
|
|
45
|
+
export declare function pyJsonDumps(payload: unknown): string;
|
|
46
|
+
/** Python: round(x, 1) — half-to-even, though real durations never land on a midpoint. */
|
|
47
|
+
export declare function round1(value: number): number;
|
|
48
|
+
export declare function writeSummaryJson(info: SessionInfo, records: readonly Rec[], outcomeOf: (rec: Rec) => string): void;
|
|
49
|
+
export declare function renderSessionLog(info: SessionInfo, records: readonly Rec[], stats: SessionStats, outcomeOf: (rec: Rec) => string): string;
|
|
50
|
+
export declare function writeSessionLog(info: SessionInfo, records: readonly Rec[], stats: SessionStats, outcomeOf: (rec: Rec) => string): void;
|
|
51
|
+
export declare function consoleSummary(info: SessionInfo, stats: SessionStats, trackerPath: string | null): string;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fmtDuration } from "./format.js";
|
|
4
|
+
import { usageFragment, usageMd } from "./outcome.js";
|
|
5
|
+
/** Python's repr for a float: an integral value keeps its `.0`. */
|
|
6
|
+
function pyFloat(value) {
|
|
7
|
+
if (!Number.isFinite(value))
|
|
8
|
+
return String(value);
|
|
9
|
+
return Number.isInteger(value) ? `${value}.0` : String(value);
|
|
10
|
+
}
|
|
11
|
+
const FLOAT_PREFIX = "@@FLOAT:";
|
|
12
|
+
const FLOAT_SUFFIX = "@@";
|
|
13
|
+
/** A marker that survives JSON.stringify as a string and is unquoted afterwards. */
|
|
14
|
+
export const pyFloatMarker = (value) => `${FLOAT_PREFIX}${pyFloat(value)}${FLOAT_SUFFIX}`;
|
|
15
|
+
const FLOAT_RE = /"@@FLOAT:([^"@]*)@@"/g;
|
|
16
|
+
const NON_ASCII_RE = /[\u0080-\uffff]/g;
|
|
17
|
+
/**
|
|
18
|
+
* json.dumps(payload, indent=2) — including ensure_ascii, which escapes every non-ASCII
|
|
19
|
+
* character (the em dashes in `outcome` among them). JSON.stringify does not escape those.
|
|
20
|
+
*/
|
|
21
|
+
export function pyJsonDumps(payload) {
|
|
22
|
+
const text = JSON.stringify(payload, null, 2).replace(FLOAT_RE, (_m, num) => num);
|
|
23
|
+
return text.replace(NON_ASCII_RE, (ch) => `\\u${ch.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
24
|
+
}
|
|
25
|
+
/** Python: round(x, 1) — half-to-even, though real durations never land on a midpoint. */
|
|
26
|
+
export function round1(value) {
|
|
27
|
+
const scaled = value * 10;
|
|
28
|
+
const nearest = Math.round(scaled);
|
|
29
|
+
const rounded = Math.abs(scaled - Math.trunc(scaled)) === 0.5 && nearest % 2 !== 0 ? nearest - 1 : nearest;
|
|
30
|
+
return rounded / 10;
|
|
31
|
+
}
|
|
32
|
+
export function writeSummaryJson(info, records, outcomeOf) {
|
|
33
|
+
mkdirSync(info.logRoot, { recursive: true });
|
|
34
|
+
const payload = {
|
|
35
|
+
run_id: info.runId,
|
|
36
|
+
pid: info.pid,
|
|
37
|
+
default_model: info.defaultModel,
|
|
38
|
+
parallel: info.parallel,
|
|
39
|
+
repo: info.repo,
|
|
40
|
+
runs_dir: info.runsDir,
|
|
41
|
+
session_log: info.sessionLog,
|
|
42
|
+
cases: records.map((r) => ({
|
|
43
|
+
name: r.name,
|
|
44
|
+
model: r.model,
|
|
45
|
+
status: r.status,
|
|
46
|
+
exit_code: r.exitCode,
|
|
47
|
+
started_s: pyFloatMarker(round1(r.startedS)),
|
|
48
|
+
duration_s: r.durationIsInt ? round1(r.durationS) : pyFloatMarker(round1(r.durationS)),
|
|
49
|
+
tools: r.tools,
|
|
50
|
+
usage: r.usage,
|
|
51
|
+
note: r.note,
|
|
52
|
+
session_id: r.sessionId,
|
|
53
|
+
log_dir: r.logDir,
|
|
54
|
+
outcome: outcomeOf(r),
|
|
55
|
+
})),
|
|
56
|
+
};
|
|
57
|
+
writeFileSync(join(info.logRoot, "summary.json"), `${pyJsonDumps(payload)}\n`, "utf8");
|
|
58
|
+
}
|
|
59
|
+
export function renderSessionLog(info, records, stats, outcomeOf) {
|
|
60
|
+
const usage = stats.usage;
|
|
61
|
+
const tok = usage ? usageFragment(usage) : "tokens —";
|
|
62
|
+
const md = usageMd(usage);
|
|
63
|
+
const lines = [
|
|
64
|
+
`# cairn qa ${info.runId} pid ${info.pid}`,
|
|
65
|
+
"",
|
|
66
|
+
`- **Launched:** ${info.runId} UTC`,
|
|
67
|
+
`- **PID:** ${info.pid}`,
|
|
68
|
+
`- **Default model:** \`${info.defaultModel}\``,
|
|
69
|
+
`- **Repo:** \`${info.repo}\``,
|
|
70
|
+
`- **Runs dir:** \`${info.runsDir}\``,
|
|
71
|
+
`- **Parallel:** ${info.parallel}`,
|
|
72
|
+
`- **Wall clock:** ${fmtDuration(stats.wallS)}`,
|
|
73
|
+
"",
|
|
74
|
+
"## Totals",
|
|
75
|
+
"",
|
|
76
|
+
`- **ok / fail / skipped:** ${stats.ok} / ${stats.fail} / ${stats.skipped}`,
|
|
77
|
+
`- **Sum of case durations:** ${fmtDuration(stats.durationS)} ` +
|
|
78
|
+
"(can exceed wall clock because cases run in parallel)",
|
|
79
|
+
`- **Tools:** ${stats.tools}`,
|
|
80
|
+
`- **Tokens:** ${tok}`,
|
|
81
|
+
` - in ${md.inn} · out ${md.out} · cache-r ${md.cache_r} · cache-w ${md.cache_w}`,
|
|
82
|
+
"",
|
|
83
|
+
"## Cases",
|
|
84
|
+
"",
|
|
85
|
+
"| Case | Model | Status | Outcome | Duration | Tools | In | Out | Cache-r | Cache-w | Note |",
|
|
86
|
+
"| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |",
|
|
87
|
+
];
|
|
88
|
+
for (const rec of records) {
|
|
89
|
+
const cells = usageMd(rec.usage);
|
|
90
|
+
const note = rec.note ? rec.note.replaceAll("|", "\\|") : "—";
|
|
91
|
+
lines.push(`| ${rec.name} | ${rec.model} | ${rec.status} | ${outcomeOf(rec)} | ` +
|
|
92
|
+
`${fmtDuration(rec.durationS)} | ${rec.tools} | ` +
|
|
93
|
+
`${cells.inn} | ${cells.out} | ${cells.cache_r} | ${cells.cache_w} | ` +
|
|
94
|
+
`${note} |`);
|
|
95
|
+
}
|
|
96
|
+
if (records.length === 0) {
|
|
97
|
+
lines.push("| — | — | — | — | — | — | — | — | — | — | " + "no cases finished yet |");
|
|
98
|
+
}
|
|
99
|
+
lines.push("");
|
|
100
|
+
return lines.join("\n");
|
|
101
|
+
}
|
|
102
|
+
export function writeSessionLog(info, records, stats, outcomeOf) {
|
|
103
|
+
mkdirSync(dirname(info.sessionLog), { recursive: true });
|
|
104
|
+
writeFileSync(info.sessionLog, renderSessionLog(info, records, stats, outcomeOf), "utf8");
|
|
105
|
+
}
|
|
106
|
+
export function consoleSummary(info, stats, trackerPath) {
|
|
107
|
+
const usage = stats.usage;
|
|
108
|
+
const tok = usage ? usageFragment(usage) : "tokens —";
|
|
109
|
+
const lines = [
|
|
110
|
+
`cairn qa ${info.runId} pid ${info.pid}`,
|
|
111
|
+
`${stats.ok} ok / ${stats.fail} fail / ${stats.skipped} skipped`,
|
|
112
|
+
`elapsed ${fmtDuration(stats.wallS)} ` +
|
|
113
|
+
`case-time ${fmtDuration(stats.durationS)} tools ${stats.tools}`,
|
|
114
|
+
tok,
|
|
115
|
+
`run log ${info.sessionLog}`,
|
|
116
|
+
];
|
|
117
|
+
if (trackerPath !== null)
|
|
118
|
+
lines.push(`tracker ${trackerPath}`);
|
|
119
|
+
return lines.join("\n");
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=reports.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reports.js","sourceRoot":"","sources":["../../src/qa/reports.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Case } from "./cases.js";
|
|
2
|
+
/**
|
|
3
|
+
* The work queue plus the per-case concurrency constraints.
|
|
4
|
+
*
|
|
5
|
+
* A `parallel: true` case is unconstrained (except by the per-backend cap). A
|
|
6
|
+
* `parallel: false` case with tags holds each of those tags as a mutex while it
|
|
7
|
+
* runs. A `parallel: false` case with no tags is a barrier: nothing runs beside
|
|
8
|
+
* it, and nothing behind it starts while it waits for the agents in front to finish.
|
|
9
|
+
*
|
|
10
|
+
* Invariant: with `running === 0` and a non-empty queue this never returns null —
|
|
11
|
+
* no tag can be held, an exclusive case is startable, and a per-backend cap is
|
|
12
|
+
* skipped when the queue is idle so a cap can never deadlock an idle queue.
|
|
13
|
+
*/
|
|
14
|
+
export declare class Scheduler {
|
|
15
|
+
private readonly pending;
|
|
16
|
+
private readonly heldTags;
|
|
17
|
+
private readonly caps;
|
|
18
|
+
private readonly agentRunning;
|
|
19
|
+
private running;
|
|
20
|
+
private exclusive;
|
|
21
|
+
private waiters;
|
|
22
|
+
/** Bumped by every wake. A worker samples it before `take()` so a wake cannot be lost. */
|
|
23
|
+
private generation;
|
|
24
|
+
/** Set by `take()` when it returns null, so the TUI can say why nothing is starting. */
|
|
25
|
+
private blocked;
|
|
26
|
+
constructor(cases: readonly Case[], caps?: ReadonlyMap<string, number>);
|
|
27
|
+
/** Cases not yet dispatched. */
|
|
28
|
+
get size(): number;
|
|
29
|
+
get inFlight(): number;
|
|
30
|
+
/** Sample before `take()`; hand back to `changed()` so a wake in between is not missed. */
|
|
31
|
+
get generationId(): number;
|
|
32
|
+
private capFor;
|
|
33
|
+
private atCap;
|
|
34
|
+
/**
|
|
35
|
+
* The first case that may start right now, with its tags reserved. Null when the queue is empty
|
|
36
|
+
* or every startable candidate is blocked.
|
|
37
|
+
*
|
|
38
|
+
* Invariant: with `running === 0` and a non-empty queue this never returns null — no tag can be
|
|
39
|
+
* held and an exclusive case is startable — so a queue can never stall with nothing in flight.
|
|
40
|
+
*/
|
|
41
|
+
take(): Case | null;
|
|
42
|
+
private claim;
|
|
43
|
+
/** Give back whatever `take()` reserved, then wake every parked worker to re-scan. */
|
|
44
|
+
release(kase: Case): void;
|
|
45
|
+
/** Release parked workers without freeing anything — for stop and drain. */
|
|
46
|
+
wake(): void;
|
|
47
|
+
/**
|
|
48
|
+
* Park until something is released, or the harness wakes us to shut down. `seen` is the
|
|
49
|
+
* generation sampled before the `take()` that came back empty: if a wake landed in between,
|
|
50
|
+
* this returns immediately rather than sleeping through it.
|
|
51
|
+
*/
|
|
52
|
+
changed(seen: number): Promise<void>;
|
|
53
|
+
/** Why nothing can start, for the pane region when no agent is running. */
|
|
54
|
+
blockedReason(): string | null;
|
|
55
|
+
}
|