anpord 0.1.11 → 0.1.12
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 +56 -78
- package/dist/bin.cjs +6 -82
- package/dist/bin.mjs +6 -82
- package/dist/client-BnimnYiJ.cjs +345 -0
- package/dist/{client-C9m0uLCz.d.cts → client-Byoa41pX.d.mts} +346 -34
- package/dist/{client-TFCLuMv8.d.mts → client-CFTj9ad0.d.cts} +346 -34
- package/dist/client-D22IiHGL.mjs +304 -0
- package/dist/{compiler-Ap8DM-JU.cjs → compiler-BFxukiJy.cjs} +56 -28
- package/dist/{compiler-DXlpU_In.mjs → compiler-CBMPXIzV.mjs} +46 -18
- package/dist/config.cjs +1 -1
- package/dist/config.d.cts +1 -1
- package/dist/config.d.mts +1 -1
- package/dist/config.mjs +1 -1
- package/dist/{errors-C9bQUcA3.mjs → errors-B0YknR5V.mjs} +3 -19
- package/dist/{errors-Boum34f5.cjs → errors-BX1wry8K.cjs} +3 -19
- package/dist/{errors-BK3f7Rdr.d.cts → errors-aGrd21jy.d.cts} +0 -3
- package/dist/{errors-BK3f7Rdr.d.mts → errors-aGrd21jy.d.mts} +0 -3
- package/dist/eval-judges-BkmiVLLq.cjs +59 -0
- package/dist/eval-judges-CwTvTq7N.mjs +42 -0
- package/dist/eval-judges-DPPUftbh.d.cts +48 -0
- package/dist/eval-judges-DPPUftbh.d.mts +48 -0
- package/dist/eval.cjs +1 -1
- package/dist/eval.d.cts +40 -6
- package/dist/eval.d.mts +40 -6
- package/dist/eval.mjs +1 -1
- package/dist/evals-B04M0apj.cjs +682 -0
- package/dist/{evals-BEgEmfiO.d.cts → evals-BUBzpke_.d.cts} +53 -9
- package/dist/{evals-BEgEmfiO.d.mts → evals-BUBzpke_.d.mts} +53 -9
- package/dist/evals-CvOTyvCX.mjs +539 -0
- package/dist/index.cjs +23 -99
- package/dist/index.d.cts +13 -33
- package/dist/index.d.mts +13 -33
- package/dist/index.mjs +23 -99
- package/dist/source.d.cts +1 -1
- package/dist/source.d.mts +1 -1
- package/dist/{types-H9L450Iu.d.cts → types-nr4-1c3W.d.cts} +87 -32
- package/dist/{types-oaMF6-E1.d.mts → types-oXWOHA-X.d.mts} +87 -32
- package/dist/validators.cjs +10 -0
- package/dist/validators.d.cts +8 -0
- package/dist/validators.d.mts +8 -0
- package/dist/validators.mjs +9 -0
- package/package.json +11 -1
- package/dist/client-5K3dMI5Q.mjs +0 -849
- package/dist/client-Ci0woWLW.cjs +0 -890
- package/dist/harness-profile-BMjsN460.mjs +0 -43
- package/dist/harness-profile-CDQpPLPd.cjs +0 -78
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# anpord
|
|
2
2
|
|
|
3
|
-
TypeScript SDK for [Anpord](https://anpord.com).
|
|
3
|
+
TypeScript SDK for [Anpord](https://anpord.com). Define coding-agent evals, run them across agents and sandboxes, and manage versioned prompts.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -8,116 +8,71 @@ TypeScript SDK for [Anpord](https://anpord.com). Run coding agent evals across h
|
|
|
8
8
|
npm install anpord
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Define an eval
|
|
12
12
|
|
|
13
13
|
```ts
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
const anpord = new Anpord({ apiKey: process.env.ANPORD_API_KEY });
|
|
14
|
+
import { defineEval, empty } from "anpord";
|
|
17
15
|
|
|
18
|
-
|
|
16
|
+
export default defineEval({
|
|
17
|
+
name: "greeting",
|
|
18
|
+
source: empty,
|
|
19
|
+
prompt: "{{task}}",
|
|
19
20
|
cases: [
|
|
20
21
|
{
|
|
21
|
-
name: "writes
|
|
22
|
+
name: "writes hello",
|
|
22
23
|
variables: { task: "Create hello.txt containing exactly hello" },
|
|
23
|
-
verify:
|
|
24
|
+
verify: 'test "$(cat hello.txt)" = hello',
|
|
24
25
|
},
|
|
25
26
|
],
|
|
26
|
-
|
|
27
|
-
tasks: [{ harness: "codex", model: "gpt-5.6-sol", provider: "daytona" }],
|
|
27
|
+
tasks: [{ harness: "codex", model: "model-id", provider: "daytona" }],
|
|
28
28
|
trials: 3,
|
|
29
29
|
});
|
|
30
|
-
|
|
31
|
-
for (const cell of run.cells) {
|
|
32
|
-
console.log(cell.caseName, cell.distribution?.passRate);
|
|
33
|
-
}
|
|
34
30
|
```
|
|
35
31
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
```ts
|
|
39
|
-
import type { Validator } from "anpord";
|
|
40
|
-
|
|
41
|
-
export const hasGreeting: Validator = async ({ readText }) =>
|
|
42
|
-
(await readText("hello.txt")) === "hello";
|
|
32
|
+
```sh
|
|
33
|
+
npx anpord eval ./greeting.eval.ts
|
|
43
34
|
```
|
|
44
35
|
|
|
45
|
-
|
|
36
|
+
With no file, the CLI discovers every `**/*.eval.ts` suite.
|
|
46
37
|
|
|
47
|
-
|
|
48
|
-
import { defineEval } from "anpord";
|
|
49
|
-
import { hasGreeting } from "./validators/greeting";
|
|
38
|
+
## Mock MCP and CLI
|
|
50
39
|
|
|
51
|
-
|
|
52
|
-
name: "greeting",
|
|
53
|
-
cases: [
|
|
54
|
-
{
|
|
55
|
-
name: "greeting",
|
|
56
|
-
variables: { task: "Write hello.txt" },
|
|
57
|
-
validate: hasGreeting,
|
|
58
|
-
},
|
|
59
|
-
],
|
|
60
|
-
prompt: "{{task}}",
|
|
61
|
-
tasks: [{ harness: "codex", model: "gpt-5.6-sol", provider: "daytona" }],
|
|
62
|
-
trials: 3,
|
|
63
|
-
});
|
|
64
|
-
```
|
|
40
|
+
Install Zod for the examples:
|
|
65
41
|
|
|
66
42
|
```sh
|
|
67
|
-
|
|
43
|
+
npm install zod
|
|
68
44
|
```
|
|
69
45
|
|
|
70
|
-
With no paths, the CLI discovers every `**/*.eval.ts` file and starts the
|
|
71
|
-
suites concurrently. Pass files or directories to run a smaller set.
|
|
72
|
-
|
|
73
|
-
## Mock MCP servers
|
|
74
|
-
|
|
75
|
-
Define local MCP dependencies once and Anpord gives every built-in agent trial
|
|
76
|
-
a fresh stdio server:
|
|
77
|
-
|
|
78
46
|
```ts
|
|
79
47
|
import { z } from "zod";
|
|
80
|
-
import { defineEval } from "anpord";
|
|
81
48
|
import { server, tool } from "anpord/mcp";
|
|
82
49
|
|
|
83
|
-
const
|
|
84
|
-
|
|
50
|
+
const User = z.object({ id: z.string(), name: z.string() });
|
|
51
|
+
|
|
52
|
+
export const usersMcp = server({
|
|
53
|
+
name: "users",
|
|
85
54
|
version: "1.0.0",
|
|
86
55
|
tools: [
|
|
87
56
|
tool({
|
|
88
57
|
name: "users_get",
|
|
89
58
|
inputSchema: z.object({ id: z.string() }),
|
|
90
|
-
outputSchema:
|
|
59
|
+
outputSchema: User,
|
|
91
60
|
handler: ({ id }) => ({ id, name: "Ada" }),
|
|
92
61
|
}),
|
|
93
62
|
],
|
|
94
63
|
});
|
|
95
|
-
|
|
96
|
-
export default defineEval({
|
|
97
|
-
mcp: [api],
|
|
98
|
-
// cases, prompt, tasks, and trials
|
|
99
|
-
});
|
|
100
64
|
```
|
|
101
65
|
|
|
102
|
-
Handlers return plain values or promises. Validators can inspect exact calls
|
|
103
|
-
with `await context.mcp.calls("example")`. No service credentials are needed.
|
|
104
|
-
|
|
105
|
-
## Mock CLIs
|
|
106
|
-
|
|
107
|
-
Define a local executable with the same Standard Schema fixtures and attach it
|
|
108
|
-
to any eval:
|
|
109
|
-
|
|
110
66
|
```ts
|
|
111
67
|
import { z } from "zod";
|
|
112
|
-
import { defineEval } from "anpord";
|
|
113
68
|
import { cli, command } from "anpord/cli";
|
|
114
69
|
|
|
115
|
-
const
|
|
116
|
-
path: "
|
|
70
|
+
export const usersCli = cli({
|
|
71
|
+
path: "users",
|
|
117
72
|
version: "1.0.0",
|
|
118
73
|
commands: [
|
|
119
74
|
command({
|
|
120
|
-
path: ["
|
|
75
|
+
path: ["get"],
|
|
121
76
|
inputSchema: z.object({ id: z.string() }),
|
|
122
77
|
outputSchema: z.object({ id: z.string(), name: z.string() }),
|
|
123
78
|
options: { id: { type: "string" } },
|
|
@@ -125,24 +80,47 @@ const client = cli({
|
|
|
125
80
|
}),
|
|
126
81
|
],
|
|
127
82
|
});
|
|
83
|
+
```
|
|
128
84
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
85
|
+
Attach definitions with `mcp: [usersMcp]` or `cli: [usersCli]`. Handlers use plain TypeScript. Schemas infer handler types and validate calls at runtime.
|
|
86
|
+
|
|
87
|
+
## Model judges
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { judge } from "anpord/validators";
|
|
91
|
+
|
|
92
|
+
const correctness = judge({
|
|
93
|
+
name: "correctness",
|
|
94
|
+
harness: "codex",
|
|
95
|
+
model: "gpt-5.6-sol",
|
|
96
|
+
rubric: "The answer matches the reference without inventing facts.",
|
|
97
|
+
expected: "Ada",
|
|
98
|
+
choices: { correct: 1, incorrect: 0 },
|
|
132
99
|
});
|
|
133
100
|
```
|
|
134
101
|
|
|
135
|
-
Every
|
|
136
|
-
calls with `await context.cli.calls("example")`.
|
|
102
|
+
Set `validate: correctness`, or combine it with code: `validate: [checkToolCalls, correctness]`. Every check must pass. Judges use the organization's harness connection, including Codex subscriptions. For direct OpenAI calls, use `provider: "openai"` and configure an OpenAI key in the environment connection.
|
|
137
103
|
|
|
138
|
-
|
|
104
|
+
See [model judges](https://docs.anpord.com/evals/judges) for isolation, authentication, and unscored failures.
|
|
105
|
+
|
|
106
|
+
## Use the API
|
|
139
107
|
|
|
140
108
|
```ts
|
|
141
|
-
|
|
142
|
-
|
|
109
|
+
import { Anpord } from "anpord";
|
|
110
|
+
import { compileEval } from "anpord/eval";
|
|
111
|
+
|
|
112
|
+
const anpord = new Anpord();
|
|
113
|
+
const run = await anpord.evals.startAndWait(
|
|
114
|
+
await compileEval("./greeting.eval.ts")
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
console.log(run.status, run.cells);
|
|
118
|
+
await anpord.dispose();
|
|
143
119
|
```
|
|
144
120
|
|
|
145
|
-
|
|
121
|
+
The client reads `ANPORD_API_KEY`.
|
|
122
|
+
|
|
123
|
+
See the [documentation](https://docs.anpord.com) for cases, validators, profiles, mock interfaces, prompt releases, and the API reference.
|
|
146
124
|
|
|
147
125
|
## License
|
|
148
126
|
|
package/dist/bin.cjs
CHANGED
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const require_client = require("./client-
|
|
3
|
-
const require_errors = require("./errors-
|
|
2
|
+
const require_client = require("./client-BnimnYiJ.cjs");
|
|
3
|
+
const require_errors = require("./errors-BX1wry8K.cjs");
|
|
4
4
|
const require_config = require("./config.cjs");
|
|
5
|
-
const require_compiler = require("./compiler-
|
|
5
|
+
const require_compiler = require("./compiler-BFxukiJy.cjs");
|
|
6
6
|
let _effect_cli = require("@effect/cli");
|
|
7
7
|
let _effect_platform = require("@effect/platform");
|
|
8
8
|
let _effect_platform_node = require("@effect/platform-node");
|
|
9
9
|
let effect = require("effect");
|
|
10
10
|
let yaml = require("yaml");
|
|
11
11
|
//#region package.json
|
|
12
|
-
var version$1 = "0.1.
|
|
12
|
+
var version$1 = "0.1.12";
|
|
13
13
|
//#endregion
|
|
14
14
|
//#region ../template/src/extract.ts
|
|
15
|
-
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* Escaped braces are read here too, so a name the renderer will treat as
|
|
19
|
-
* literal text is never reported as a variable the editor should draw.
|
|
20
|
-
*/
|
|
15
|
+
/** Reads escapes too, so text the renderer leaves literal is never reported as
|
|
16
|
+
* a variable. */
|
|
21
17
|
function extractVariables(template) {
|
|
22
18
|
const names = [];
|
|
23
19
|
for (const [, open, close, name] of template.matchAll(require_errors.tokenMatcher())) if (open === void 0 && close === void 0 && name !== void 0) names.push(name);
|
|
@@ -25,13 +21,6 @@ function extractVariables(template) {
|
|
|
25
21
|
}
|
|
26
22
|
//#endregion
|
|
27
23
|
//#region src/cli/declarations.ts
|
|
28
|
-
/**
|
|
29
|
-
* The generated file has to open by importing the module it augments.
|
|
30
|
-
*
|
|
31
|
-
* Without it `declare module` shadows the module rather than adding to it, and
|
|
32
|
-
* every import of the SDK stops resolving with an error naming the missing
|
|
33
|
-
* export rather than the file that hid it.
|
|
34
|
-
*/
|
|
35
24
|
const PREAMBLE = [
|
|
36
25
|
"// Generated by `anpord generate`. Do not edit.",
|
|
37
26
|
"// Run it again after changing which variables a prompt uses.",
|
|
@@ -40,7 +29,6 @@ const PREAMBLE = [
|
|
|
40
29
|
"declare module \"anpord\" {",
|
|
41
30
|
" interface AnpordPromptVariables {"
|
|
42
31
|
].join("\n");
|
|
43
|
-
/** Quoted, because an id may carry characters an identifier cannot. */
|
|
44
32
|
const entry = (id, names) => {
|
|
45
33
|
return ` "${id}": ${names.length === 0 ? "Record<string, never>" : `{ ${names.map((name) => `"${name}": string`).join("; ")} }`};`;
|
|
46
34
|
};
|
|
@@ -108,8 +96,6 @@ const promptContent = (prompt) => effect.Effect.sync(() => {
|
|
|
108
96
|
process.stdout.write(prompt.content);
|
|
109
97
|
if (!prompt.content.endsWith("\n")) process.stdout.write("\n");
|
|
110
98
|
});
|
|
111
|
-
/** A result the caller may pipe into another tool, so it belongs on stdout
|
|
112
|
-
* alongside the prompt content rather than beside the status messages. */
|
|
113
99
|
const row = (line) => effect.Effect.sync(() => {
|
|
114
100
|
process.stdout.write(`${line}\n`);
|
|
115
101
|
});
|
|
@@ -192,8 +178,6 @@ var CaseFileNotJson = class extends effect.Data.TaggedError("CaseFileNotJson") {
|
|
|
192
178
|
return `${this.path} is not JSON: ${this.reason}`;
|
|
193
179
|
}
|
|
194
180
|
};
|
|
195
|
-
/** Names the case rather than the byte offset, because the author fixing it
|
|
196
|
-
* reads the file by case, not by position. */
|
|
197
181
|
var CaseFileNotEvalsJson = class extends effect.Data.TaggedError("CaseFileNotEvalsJson") {
|
|
198
182
|
get message() {
|
|
199
183
|
return `${this.path} is not an evals-json file: ${this.reason}`;
|
|
@@ -206,29 +190,17 @@ var CaseFileEmpty = class extends effect.Data.TaggedError("CaseFileEmpty") {
|
|
|
206
190
|
};
|
|
207
191
|
//#endregion
|
|
208
192
|
//#region src/imports/typescript-literal.ts
|
|
209
|
-
/** A backslash-escaped double-quoted literal. Backslash first, so the escapes
|
|
210
|
-
* added after it are not themselves escaped. */
|
|
211
193
|
const quoted = (value) => `"${value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"").replaceAll("\n", "\\n").replaceAll("\r", "\\r").replaceAll(" ", "\\t")}"`;
|
|
212
|
-
/** A template literal, which keeps a multi-line prompt readable in the
|
|
213
|
-
* generated file. A backtick ends the literal and `${` opens a substitution,
|
|
214
|
-
* so both are escaped; a lone `$` is left alone because only the pair means
|
|
215
|
-
* anything. A trailing backslash would escape the closing backtick. */
|
|
216
194
|
const templated = (value) => `\`${value.replaceAll("\\", "\\\\").replaceAll("`", "\\`").replaceAll("${", "\\${")}\``;
|
|
217
|
-
/** A comment cannot carry the sequence that closes it, and a newline would end
|
|
218
|
-
* a line comment and let the rest of the text run as code. */
|
|
219
195
|
const commentSafe = (value) => value.replaceAll("*/", "*\\/").replaceAll(/\r?\n/g, " ");
|
|
220
196
|
//#endregion
|
|
221
197
|
//#region src/imports/unwritten-check.ts
|
|
222
198
|
const PLACEHOLDER = "unwritten";
|
|
223
|
-
/** The author's own words, kept whole, because they are the specification for
|
|
224
|
-
* the check that replaces the line beneath them. */
|
|
225
199
|
const proseLine = (text) => [
|
|
226
200
|
" /* Write this check, then delete the line under it: */",
|
|
227
201
|
` /* ${commentSafe(text)} */`,
|
|
228
202
|
` ${PLACEHOLDER}(${quoted(text)}),`
|
|
229
203
|
].join("\n");
|
|
230
|
-
/** Local rather than imported, so the generated file carries its own proof
|
|
231
|
-
* that an unconverted assertion fails. Deleting the last call deletes it. */
|
|
232
204
|
const placeholderBlock = [
|
|
233
205
|
"/* A check nobody has written yet. It is false, so the case stays red until",
|
|
234
206
|
" the sentence above it becomes a real check. The argument is that",
|
|
@@ -259,8 +231,6 @@ const nameOf = (subject) => `${slug$1(subject.name ?? "case", "case")}-${subject
|
|
|
259
231
|
const scorerLine = (assertion) => [` /* ${commentSafe(assertion.text)} */`, ` ${SCORER_OF[assertion.kind]}(answer, [${assertion.needles.map(quoted).join(", ")}]),`].join("\n");
|
|
260
232
|
const scorerLines = (subject) => subject.assertions.map((assertion) => isStructured(assertion) ? scorerLine(assertion) : proseLine(assertion)).join("\n");
|
|
261
233
|
const expectationComment = (subject) => subject.expected_output === void 0 ? [] : [` /* The file's expected output: ${commentSafe(subject.expected_output)} */`];
|
|
262
|
-
/** The JSON names fixture directories by convention and carries none of their
|
|
263
|
-
* contents, so the author supplies the files the name stood for. */
|
|
264
234
|
const sourceComment = (subject) => subject.files.length === 0 ? " /* This case named no fixture directory. Add the files it starts from. */" : ` /* This case named ${subject.files.map(commentSafe).join(", ")}. The JSON carries the directory names, not their contents, so add the files here. */`;
|
|
265
235
|
const caseBlock$1 = (subject) => [
|
|
266
236
|
" {",
|
|
@@ -319,16 +289,12 @@ const renderEvalSuite = (file) => {
|
|
|
319
289
|
};
|
|
320
290
|
//#endregion
|
|
321
291
|
//#region src/imports/evals-json-schema.ts
|
|
322
|
-
/** The three checks that convert mechanically, because each is a needle list
|
|
323
|
-
* and a rule over it rather than a description of intent. */
|
|
324
292
|
const AssertionKind = effect.Schema.Literal("content_contains_any", "content_contains_all", "content_contains_none");
|
|
325
293
|
const StructuredAssertion = effect.Schema.Struct({
|
|
326
294
|
kind: AssertionKind,
|
|
327
295
|
needles: effect.Schema.Array(effect.Schema.String),
|
|
328
296
|
text: effect.Schema.String
|
|
329
297
|
});
|
|
330
|
-
/** Both dialects occur, sometimes in sibling files: a bare string is prose one
|
|
331
|
-
* person wrote for another, an object is a mechanical check. */
|
|
332
298
|
const Assertion = effect.Schema.Union(effect.Schema.String, StructuredAssertion);
|
|
333
299
|
const EvalsJsonCase = effect.Schema.Struct({
|
|
334
300
|
assertions: effect.Schema.Array(Assertion),
|
|
@@ -356,8 +322,6 @@ const idAt = (parsed, index) => {
|
|
|
356
322
|
const found = parsed?.evals?.[index];
|
|
357
323
|
return found?.id === void 0 ? `${index}` : String(found.id);
|
|
358
324
|
};
|
|
359
|
-
/** The case as its author sees it. A path into the decoded value tells them
|
|
360
|
-
* nothing; the id is what they search the file for. */
|
|
361
325
|
const located = (parsed, path) => {
|
|
362
326
|
const [head, index, ...rest] = path;
|
|
363
327
|
if (head !== "evals" || typeof index !== "number") return path.map(String).join(".");
|
|
@@ -393,8 +357,6 @@ var CaseFileNotYaml = class extends effect.Data.TaggedError("CaseFileNotYaml") {
|
|
|
393
357
|
return `${this.path} is not YAML: ${this.reason}`;
|
|
394
358
|
}
|
|
395
359
|
};
|
|
396
|
-
/** Names the file rather than a path into the decoded value, because one case
|
|
397
|
-
* is one file and the file name is what the author searches for. */
|
|
398
360
|
var CaseFileNotYamlCase = class extends effect.Data.TaggedError("CaseFileNotYamlCase") {
|
|
399
361
|
get message() {
|
|
400
362
|
return `${this.path} is not a yaml case: ${this.reason}`;
|
|
@@ -411,20 +373,12 @@ const slug = (value, fallback) => {
|
|
|
411
373
|
const cleaned = value.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/^-+|-+$/g, "");
|
|
412
374
|
return cleaned === "" ? fallback : cleaned;
|
|
413
375
|
};
|
|
414
|
-
/** Every line is prose a person wrote for a model judge, so none of it
|
|
415
|
-
* converts and the whole list is what a human still owes. A case with no
|
|
416
|
-
* lines owes one too: something has to say what a good answer is. */
|
|
417
376
|
const tallyOf = (files) => ({
|
|
418
377
|
cases: files.length,
|
|
419
378
|
converted: 0,
|
|
420
379
|
needsAuthor: files.reduce((total, file) => total + Math.max(file.subject.judge_context.length, 1), 0)
|
|
421
380
|
});
|
|
422
|
-
/** Anpord has no step budget, so the number is kept as a note rather than
|
|
423
|
-
* dropped: the suite it came from ran under it, and a case that needed ten
|
|
424
|
-
* steps is a different case from one that needed a hundred. */
|
|
425
381
|
const budgetComment = (subject) => ` /* The file allowed ${subject.max_steps} steps. Anpord does not cap steps, so this is a note, not a limit. */`;
|
|
426
|
-
/** A file with no judge context says nothing about what a good answer is, so
|
|
427
|
-
* it is owed a check like any other rather than passing by default. */
|
|
428
382
|
const UNJUDGED = "This case named no judge context. Write what a good answer is.";
|
|
429
383
|
const judgeLines = (subject) => (subject.judge_context.length === 0 ? [UNJUDGED] : subject.judge_context).map((line) => proseLine(line)).join("\n");
|
|
430
384
|
const caseBlock = (file) => [
|
|
@@ -446,8 +400,6 @@ const caseBlock = (file) => [
|
|
|
446
400
|
" },",
|
|
447
401
|
" },"
|
|
448
402
|
].join("\n");
|
|
449
|
-
/** A directory has no name of its own in the files, so a suite built from
|
|
450
|
-
* several is named generically and the author renames it. */
|
|
451
403
|
const suiteName = (files) => files.length === 1 ? slug(files[0]?.subject.name ?? "", "imported-suite") : "imported-suite";
|
|
452
404
|
const renderYamlSuite = (files) => `${[
|
|
453
405
|
"import { defineEval, files } from \"anpord\";",
|
|
@@ -469,12 +421,7 @@ const renderYamlSuite = (files) => `${[
|
|
|
469
421
|
].join("\n")}\n`;
|
|
470
422
|
//#endregion
|
|
471
423
|
//#region src/imports/yaml-cases-schema.ts
|
|
472
|
-
/** The runner's own default when a file omits the field, kept here so an
|
|
473
|
-
* imported case carries the budget it actually ran under. */
|
|
474
424
|
const DEFAULT_MAX_STEPS = 15;
|
|
475
|
-
/** A missing `judge_context` is a case whose author wrote nothing about how to
|
|
476
|
-
* judge it, which the runner reads as one generic line. It is not an error, so
|
|
477
|
-
* it decodes to an empty list and the generated case says so. */
|
|
478
425
|
const YamlCase = effect.Schema.Struct({
|
|
479
426
|
judge_context: effect.Schema.optionalWith(effect.Schema.Array(effect.Schema.String), { default: () => [] }),
|
|
480
427
|
max_steps: effect.Schema.optionalWith(effect.Schema.Int, { default: () => DEFAULT_MAX_STEPS }),
|
|
@@ -484,10 +431,6 @@ const YamlCase = effect.Schema.Struct({
|
|
|
484
431
|
const decodeYamlCase = effect.Schema.decodeUnknown(YamlCase, { errors: "all" });
|
|
485
432
|
//#endregion
|
|
486
433
|
//#region src/imports/yaml-document.ts
|
|
487
|
-
/** A YAML document can parse into a value while still carrying errors the
|
|
488
|
-
* author needs to see, so the errors are read rather than the throw relied on.
|
|
489
|
-
* The value is returned as `unknown` because the schema, not the parser,
|
|
490
|
-
* decides what shape it has. */
|
|
491
434
|
const parseYamlDocument = (body) => effect.Effect.try({
|
|
492
435
|
catch: (cause) => cause instanceof Error ? cause.message : String(cause),
|
|
493
436
|
try: () => {
|
|
@@ -519,8 +462,6 @@ const readCase = (path) => effect.Effect.gen(function* () {
|
|
|
519
462
|
subject: yield* decode(path, parsed)
|
|
520
463
|
};
|
|
521
464
|
});
|
|
522
|
-
/** Sorted, so the same directory always produces the same suite and a diff of
|
|
523
|
-
* two imports shows what changed rather than what moved. */
|
|
524
465
|
const caseFilesIn = (directory) => effect.Effect.gen(function* () {
|
|
525
466
|
return (yield* (yield* _effect_platform.FileSystem.FileSystem).readDirectory(directory).pipe(effect.Effect.mapError((cause) => new CaseDirectoryUnreadable({
|
|
526
467
|
cause,
|
|
@@ -547,20 +488,13 @@ const importYamlCases = (path) => effect.Effect.gen(function* () {
|
|
|
547
488
|
}).pipe(effect.Effect.withSpan("Imports.yamlCases"));
|
|
548
489
|
//#endregion
|
|
549
490
|
//#region src/cli/import-formats.ts
|
|
550
|
-
/** A format is an entry here, so adding one adds an entry rather than editing
|
|
551
|
-
* the command. Each name carries the importer it names, so the parsed option
|
|
552
|
-
* is the importer itself and no lookup can miss. */
|
|
553
491
|
const FORMATS = [["evals-json", importEvalsJson], ["yaml", importYamlCases]];
|
|
554
492
|
//#endregion
|
|
555
493
|
//#region src/cli/eval-import.ts
|
|
556
|
-
/** A path rather than a file, because a format may keep one case per file and
|
|
557
|
-
* import a directory of them as one suite. */
|
|
558
494
|
const caseFile = _effect_cli.Args.path({ name: "path" }).pipe(_effect_cli.Args.withDescription("The case file, or a directory of them, to read"));
|
|
559
495
|
const format = _effect_cli.Options.choiceWithValue("format", FORMATS).pipe(_effect_cli.Options.withDescription("The case file's format"));
|
|
560
496
|
const out$1 = _effect_cli.Options.file("out").pipe(_effect_cli.Options.withDescription("Where to write the suite; it goes to stdout when omitted"), _effect_cli.Options.optional);
|
|
561
497
|
const plural = (count, one) => `${count} ${count === 1 ? one : `${one}s`}`;
|
|
562
|
-
/** The count of unwritten checks leads, because a suite that reports a pass
|
|
563
|
-
* for a check nobody wrote is the failure this product exists to prevent. */
|
|
564
498
|
const summaryOf = (tally) => {
|
|
565
499
|
const read = `Read ${plural(tally.cases, "case")}, converted ${plural(tally.converted, "assertion")}.`;
|
|
566
500
|
return tally.needsAuthor === 0 ? read : `${read} ${plural(tally.needsAuthor, "assertion")} could not be converted and ${tally.needsAuthor === 1 ? "needs" : "need"} a human: each is written as prose, and the suite fails until you write the check it describes.`;
|
|
@@ -835,12 +769,6 @@ const promote = _effect_cli.Command.make("promote", {
|
|
|
835
769
|
} });
|
|
836
770
|
return yield* note(`${id} v${pin} is now ${to}`);
|
|
837
771
|
})).pipe(_effect_cli.Command.withDescription("Point a channel at a version"));
|
|
838
|
-
/**
|
|
839
|
-
* Read from the stream rather than by opening `/dev/stdin` as a file. The path
|
|
840
|
-
* only names the pipe, so reading it races whoever is writing: a body arriving
|
|
841
|
-
* in more than one chunk, which is what a pipe does under load, was read as
|
|
842
|
-
* whatever had landed by then.
|
|
843
|
-
*/
|
|
844
772
|
const readStdin = effect.Effect.async((resume) => {
|
|
845
773
|
let body = "";
|
|
846
774
|
process.stdin.setEncoding("utf8");
|
|
@@ -865,8 +793,6 @@ const push = _effect_cli.Command.make("push", {
|
|
|
865
793
|
return yield* note(`${id} is now v${prompt.version}`);
|
|
866
794
|
})).pipe(_effect_cli.Command.withDescription("Add a version to a prompt"));
|
|
867
795
|
const out = _effect_cli.Options.file("out").pipe(_effect_cli.Options.withDescription("Where to write the declarations"), _effect_cli.Options.withDefault("anpord-env.d.ts"));
|
|
868
|
-
/** Reading one prompt at a time because the list carries no content, bounded
|
|
869
|
-
* so a large organisation does not open a connection per prompt. */
|
|
870
796
|
const READ_AT_ONCE = 8;
|
|
871
797
|
const writeDeclarations = ({ out: path }) => effect.Effect.gen(function* () {
|
|
872
798
|
const api = yield* require_client.AnpordApi;
|
|
@@ -878,8 +804,6 @@ const writeDeclarations = ({ out: path }) => effect.Effect.gen(function* () {
|
|
|
878
804
|
});
|
|
879
805
|
const DESCRIPTION = "Write TypeScript declarations for prompt variables";
|
|
880
806
|
const generate = _effect_cli.Command.make("generate", { out }, writeDeclarations).pipe(_effect_cli.Command.withDescription(DESCRIPTION));
|
|
881
|
-
/** A second command rather than an alias, because a command carries one name
|
|
882
|
-
* and the shorter one is what anybody types twice. */
|
|
883
807
|
const gen = _effect_cli.Command.make("gen", { out }, writeDeclarations).pipe(_effect_cli.Command.withDescription(DESCRIPTION));
|
|
884
808
|
const withClient = (command) => _effect_cli.Command.provide(command, require_config.ClientLayer);
|
|
885
809
|
const commands = [
|