@eigenpal/cli 0.4.9 → 0.4.10

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/CHANGELOG.md CHANGED
@@ -211,6 +211,25 @@
211
211
 
212
212
  `@` path syntax (`--code @file.js`, `--inputs items=@arr.json`) matches `curl -d @file` / `gh --body-file -`. Bare paths and `-` (stdin) work too.
213
213
 
214
+ - 4137eb8: custom-script evaluator: the YAML now carries the entire
215
+ `function scoreScript(expected, actual) { ... }` declaration in a single
216
+ `function` field. Previous formats (`code`, then `body`) wrapped the
217
+ text implicitly; the new shape stores the function declaration verbatim
218
+ so the YAML matches the dashboard editor 1-for-1.
219
+
220
+ `workflow validate` and `workflow evaluators validate` flag YAML where
221
+ the function fails to parse, the signature does not match
222
+ `function scoreScript(expected, actual)` exactly, or the body contains
223
+ neither `return` nor `throw` — same checks the dashboard runs at form
224
+ save. The function must return a number in [0, 1]; throws are caught
225
+ and scored as 0.
226
+
227
+ Existing evaluator YAML must be ported by hand: rename `body` (or
228
+ `code`) to `function`, and add the `function scoreScript(expected,
229
+ actual) { ... }` declaration around your statements. Parameter order is
230
+ load-bearing — swapping `expected` and `actual` would silently invert
231
+ every score.
232
+
214
233
  - 3da0612: All list commands now support `--limit` / `--offset` pagination consistently.
215
234
  - CLI: `workflow execution list`, `workflow version list`, and `workflow step-type list` previously had no `--offset` (or no pagination at all). All six list commands now accept `--limit`/`--offset` with defaults of 50/0.
216
235
  - Server: `GET /api/workflows/:id/versions` now reads `?limit=` and `?offset=` query params (defaults 50/0; max limit 100). The `listVersions` repo method gained an optional `{ limit, offset }` argument.
@@ -283,6 +302,11 @@
283
302
 
284
303
  Interactive profiles are unchanged: when you log in via `eigenpal auth login`, the profile stores both apiKey and baseUrl, and every subsequent command derives baseUrl from that profile. Set `EIGENPAL_PROFILE=<name>` to switch profiles per-shell without modifying state.
285
304
 
305
+ - 542fe0e: CLI: surface evaluator entry-level fields (`name`, `description`, `weight`) so agents and humans can discover the optional `description` field on every evaluator.
306
+ - `workflow evaluator-type get <type>` now emits `entrySchema` alongside `configSchema`. Inspect with `eigenpal workflow evaluator-type get llm-judge | jq '.entrySchema'`.
307
+ - The skill reference now renders a "Common entry fields" table per type and a "Writing descriptions for stakeholders" section explaining the convention: each evaluator's `description` should be a one-sentence, plain-language summary aimed at non-technical reviewers in the dashboard.
308
+ - `init` templates `pdf-extraction` and `text-classification` now ship a `description:` line on their evaluator entries.
309
+
286
310
  - d41ee8b: CLI: fix `eigenpal status` error UX + harden `formatCliError` for real-world connection failures.
287
311
  - `eigenpal status` against an unreachable base URL no longer mis-diagnoses the failure as an auth problem. Previously, `EIGENPAL_BASE_URL=http://localhost:9999 eigenpal status` printed "Authentication check failed; try `eigenpal auth login`" — the wrong rabbit hole. The status command now rethrows non-auth errors (connection refused, DNS NXDOMAIN, timeout, HTML responses, 5xx) so `formatCliError` upstream can surface the right hint: `Could not connect to the server at <url> ... Set EIGENPAL_BASE_URL or pass --base-url <url>.` Only real 401/403 responses are treated as auth failures.
288
312
  - `formatCliError` now detects connection failures wrapped in any Error shape — bare `Error('ENOTFOUND ...')`, `Error` with `code: 'ECONNREFUSED'`, and `TypeError('fetch failed')` whose `.cause` carries the underlying error. Previously only `TypeError` whose message contained `fetch`/`URL` was recognized, so `getaddrinfo`-style errors fell through to the generic raw-message branch and lost the URL context.
package/dist/cli.js CHANGED
@@ -23327,6 +23327,141 @@ var init_dataset_archive = __esm(() => {
23327
23327
  };
23328
23328
  });
23329
23329
 
23330
+ // ../types/src/eval/score-function.ts
23331
+ function stripLeadingTrivia(text) {
23332
+ let i = 0;
23333
+ while (i < text.length) {
23334
+ const ch = text[i];
23335
+ if (ch === " " || ch === "\t" || ch === `
23336
+ ` || ch === "\r" || ch === "\v" || ch === "\f") {
23337
+ i++;
23338
+ continue;
23339
+ }
23340
+ if (ch === "/" && text[i + 1] === "/") {
23341
+ const nl = text.indexOf(`
23342
+ `, i + 2);
23343
+ if (nl === -1)
23344
+ return "";
23345
+ i = nl + 1;
23346
+ continue;
23347
+ }
23348
+ if (ch === "/" && text[i + 1] === "*") {
23349
+ const end = text.indexOf("*/", i + 2);
23350
+ if (end === -1)
23351
+ break;
23352
+ i = end + 2;
23353
+ continue;
23354
+ }
23355
+ break;
23356
+ }
23357
+ return text.slice(i);
23358
+ }
23359
+ function hasValidScoreSignature(fn) {
23360
+ return STRICT_HEADER_REGEX.test(stripLeadingTrivia(fn));
23361
+ }
23362
+ function isParseableScoreFunction(fn) {
23363
+ try {
23364
+ new Function(fn);
23365
+ return true;
23366
+ } catch {
23367
+ return false;
23368
+ }
23369
+ }
23370
+ function getScoreFunctionRuntimeIssue(fn) {
23371
+ if (/\basync\s+function\s+scoreScript\b/.test(fn)) {
23372
+ return "scoreScript must be synchronous (drop the `async` keyword — the sandbox calls it sync, so a Promise return scores 0)";
23373
+ }
23374
+ if (/\bimport\s*\(/.test(fn)) {
23375
+ return "dynamic `import(...)` is not supported in the score sandbox";
23376
+ }
23377
+ if (/\brequire\s*\(/.test(fn)) {
23378
+ return "`require(...)` is not available in the score sandbox (no Node module system)";
23379
+ }
23380
+ return null;
23381
+ }
23382
+ function extractScoreFunctionBody(fn) {
23383
+ const headerMatch = fn.match(ANY_HEADER_REGEX);
23384
+ if (!headerMatch || headerMatch.index === undefined) {
23385
+ return { body: fn, wrapperOk: false };
23386
+ }
23387
+ const start = headerMatch.index + headerMatch[0].length;
23388
+ let depth = 1;
23389
+ let i = start;
23390
+ let inLine = false;
23391
+ let inBlock = false;
23392
+ let strQuote = null;
23393
+ while (i < fn.length) {
23394
+ const ch = fn[i];
23395
+ const next = fn[i + 1];
23396
+ if (inLine) {
23397
+ if (ch === `
23398
+ `)
23399
+ inLine = false;
23400
+ } else if (inBlock) {
23401
+ if (ch === "*" && next === "/") {
23402
+ inBlock = false;
23403
+ i++;
23404
+ }
23405
+ } else if (strQuote) {
23406
+ if (ch === "\\")
23407
+ i++;
23408
+ else if (ch === strQuote)
23409
+ strQuote = null;
23410
+ } else if (ch === "/" && next === "/") {
23411
+ inLine = true;
23412
+ i++;
23413
+ } else if (ch === "/" && next === "*") {
23414
+ inBlock = true;
23415
+ i++;
23416
+ } else if (ch === '"' || ch === "'" || ch === "`") {
23417
+ strQuote = ch;
23418
+ } else if (ch === "{") {
23419
+ depth++;
23420
+ } else if (ch === "}") {
23421
+ depth--;
23422
+ if (depth === 0) {
23423
+ const inner = fn.slice(start, i).replace(/^\n+|\n+$/g, "");
23424
+ return { body: inner, wrapperOk: true };
23425
+ }
23426
+ }
23427
+ i++;
23428
+ }
23429
+ return { body: fn.slice(start), wrapperOk: false };
23430
+ }
23431
+ function defaultScoreFunction() {
23432
+ return `/**
23433
+ * Score the workflow output against the example's expected output.
23434
+ *
23435
+ * @param {Expected} expected the example's expected output
23436
+ * @param {Actual} actual the workflow's actual output
23437
+ * @returns {number} a score in [0, 1]; throws are caught and scored as 0
23438
+ */
23439
+ function scoreScript(expected, actual) {
23440
+ // Return a number in [0, 1]; throws are caught and scored as 0.
23441
+ return JSON.stringify(actual) === JSON.stringify(expected) ? 1 : 0;
23442
+ }
23443
+ `;
23444
+ }
23445
+ function wrapBodyAsScoreFunction(body) {
23446
+ const trimmed = body.replace(/^\n+|\n+$/g, "");
23447
+ return `/**
23448
+ * Score the workflow output against the example's expected output.
23449
+ *
23450
+ * @param {Expected} expected the example's expected output
23451
+ * @param {Actual} actual the workflow's actual output
23452
+ * @returns {number} a score in [0, 1]; throws are caught and scored as 0
23453
+ */
23454
+ function scoreScript(expected, actual) {
23455
+ ${trimmed}
23456
+ }
23457
+ `;
23458
+ }
23459
+ var STRICT_HEADER_REGEX, ANY_HEADER_REGEX;
23460
+ var init_score_function = __esm(() => {
23461
+ STRICT_HEADER_REGEX = /^(?:async\s+)?function\s+scoreScript\s*\(\s*expected\s*,\s*actual\s*\)\s*\{/;
23462
+ ANY_HEADER_REGEX = /function\s+scoreScript\s*\([^)]*\)\s*\{/;
23463
+ });
23464
+
23330
23465
  // ../types/src/eval/evaluator-config.ts
23331
23466
  function combineEvalScores(results) {
23332
23467
  let numerator = 0;
@@ -23341,9 +23476,10 @@ function combineEvalScores(results) {
23341
23476
  return null;
23342
23477
  return numerator / denominator;
23343
23478
  }
23344
- var EvaluatorTypeSchema, ScoreInUnitInterval, LLM_JUDGE_PROMPT_MIN_LENGTH = 10, ExactDiffConfigSchema, JudgeModeSchema, LabelsRecord, LlmJudgeConfigSchema, CustomScriptConfigSchema, EvaluatorBaseEntrySchema, EvaluatorConfigSchema, EvalConfigYamlSchema, EvaluatorResultSchema;
23479
+ var EvaluatorTypeSchema, ScoreInUnitInterval, LLM_JUDGE_PROMPT_MIN_LENGTH = 10, ExactDiffConfigSchema, JudgeModeSchema, LabelsRecord, LlmJudgeConfigSchema, CUSTOM_SCRIPT_CONTRACT, CUSTOM_SCRIPT_MAX_BYTES = 1e4, CUSTOM_SCRIPT_MAX_TIMEOUT_MS = 30000, CUSTOM_SCRIPT_MAX_MEMORY_MB = 50, CUSTOM_SCRIPT_FUNCTION_DESCRIPTION, CustomScriptConfigSchema, EvaluatorBaseEntrySchema, EvaluatorConfigSchema, EvalConfigYamlSchema, EvaluatorResultSchema;
23345
23480
  var init_evaluator_config = __esm(() => {
23346
23481
  init_zod();
23482
+ init_score_function();
23347
23483
  EvaluatorTypeSchema = exports_external.enum(["exact-diff", "llm-judge", "custom-script"]);
23348
23484
  ScoreInUnitInterval = exports_external.number().min(0, { message: "score must be >= 0" }).max(1, { message: "score must be <= 1" });
23349
23485
  ExactDiffConfigSchema = exports_external.object({
@@ -23372,14 +23508,41 @@ var init_evaluator_config = __esm(() => {
23372
23508
  }
23373
23509
  }
23374
23510
  });
23511
+ CUSTOM_SCRIPT_CONTRACT = {
23512
+ expected: "the example's expected output",
23513
+ actual: "the workflow's actual output",
23514
+ returns: "a number in [0, 1]",
23515
+ throws: "caught and scored as 0"
23516
+ };
23517
+ CUSTOM_SCRIPT_FUNCTION_DESCRIPTION = `Full JavaScript declaration of \`function scoreScript(expected, actual) { ... }\`. ` + `Receives \`expected\` (${CUSTOM_SCRIPT_CONTRACT.expected}) ` + `and \`actual\` (${CUSTOM_SCRIPT_CONTRACT.actual}), returns ${CUSTOM_SCRIPT_CONTRACT.returns}. ` + `Throws are ${CUSTOM_SCRIPT_CONTRACT.throws}.`;
23375
23518
  CustomScriptConfigSchema = exports_external.object({
23376
- code: exports_external.string().min(1, { message: "code required" }).describe("Sandboxed JavaScript. Receives `actual`, `expected`, `input` as variables and must return { score, label? } where score is in [0, 1]."),
23377
- timeoutMs: exports_external.number().int().positive().default(5000).describe("Maximum wall-clock time the script can run before it's killed and the run is marked failed."),
23378
- memoryLimitMb: exports_external.number().int().positive().default(10).describe("Maximum memory the sandbox may allocate. Increase if the script processes large arrays or strings."),
23519
+ function: exports_external.string().min(1, { message: "required" }).max(CUSTOM_SCRIPT_MAX_BYTES, {
23520
+ message: `must be ≤ ${CUSTOM_SCRIPT_MAX_BYTES} bytes`
23521
+ }).describe(CUSTOM_SCRIPT_FUNCTION_DESCRIPTION).refine(isParseableScoreFunction, {
23522
+ message: "must parse as JavaScript"
23523
+ }).refine(hasValidScoreSignature, {
23524
+ message: "must declare `function scoreScript(expected, actual) { ... }` (parameter names + order matter)"
23525
+ }).refine((fn) => {
23526
+ const { body, wrapperOk } = extractScoreFunctionBody(fn);
23527
+ return wrapperOk && /\b(return|throw)\b/.test(body);
23528
+ }, {
23529
+ message: "function body must `return` a number in [0, 1] (or `throw` to score 0)"
23530
+ }).superRefine((fn, ctx) => {
23531
+ const issue2 = getScoreFunctionRuntimeIssue(fn);
23532
+ if (issue2)
23533
+ ctx.addIssue({ code: exports_external.ZodIssueCode.custom, message: issue2 });
23534
+ }),
23535
+ timeoutMs: exports_external.number().int().positive().max(CUSTOM_SCRIPT_MAX_TIMEOUT_MS, {
23536
+ message: `must be ≤ ${CUSTOM_SCRIPT_MAX_TIMEOUT_MS} ms`
23537
+ }).default(5000).describe("Maximum wall-clock time the script can run before it's killed and the run is marked failed."),
23538
+ memoryLimitMb: exports_external.number().int().positive().max(CUSTOM_SCRIPT_MAX_MEMORY_MB, {
23539
+ message: `must be ≤ ${CUSTOM_SCRIPT_MAX_MEMORY_MB} MB`
23540
+ }).default(10).describe("Maximum memory the sandbox may allocate. Increase if the script processes large arrays or strings."),
23379
23541
  passThreshold: ScoreInUnitInterval.default(1).describe("Minimum script score this evaluator must reach for a run to pass.")
23380
23542
  });
23381
23543
  EvaluatorBaseEntrySchema = exports_external.object({
23382
23544
  name: exports_external.string().min(1).max(64).regex(/^[a-z0-9-]+$/, "name must be lowercase kebab-case").describe("Identifier used in eval results and the weighted-mean formula. Use a short kebab-case slug like header-exact or covenants-recall."),
23545
+ description: exports_external.string().trim().max(500).optional().describe("One-sentence explanation, in business language, of what this evaluator measures and why a stakeholder should care. Shown in the dashboard to non-technical reviewers (legal, ops, finance) who never see the YAML or judge prompt. Skip implementation detail (model names, thresholds, dot-paths); those live in `config`."),
23383
23546
  weight: exports_external.number().min(0).default(1).describe("Relative importance in the weighted mean. The overall score is Σ(weightᵢ × scoreᵢ) / Σweight. Set to 0 to score-only without affecting overall.")
23384
23547
  });
23385
23548
  EvaluatorConfigSchema = exports_external.discriminatedUnion("type", [
@@ -23569,6 +23732,7 @@ var init_eval = __esm(() => {
23569
23732
  init_dataset_archive();
23570
23733
  init_evaluator_config();
23571
23734
  init_judge();
23735
+ init_score_function();
23572
23736
  init_workflow_tool_metadata();
23573
23737
  });
23574
23738
 
@@ -23637,6 +23801,7 @@ var init_paths = __esm(() => {
23637
23801
  // ../types/src/index.ts
23638
23802
  var exports_src = {};
23639
23803
  __export(exports_src, {
23804
+ wrapBodyAsScoreFunction: () => wrapBodyAsScoreFunction,
23640
23805
  workflowTriggerConfigSchema: () => workflowTriggerConfigSchema,
23641
23806
  valuesMatch: () => valuesMatch,
23642
23807
  validateStepConfig: () => validateStepConfig,
@@ -23667,10 +23832,12 @@ __export(exports_src, {
23667
23832
  isValidJudgeModel: () => isValidJudgeModel,
23668
23833
  isTerminalExecutionStatus: () => isTerminalExecutionStatus,
23669
23834
  isStepCategory: () => isStepCategory,
23835
+ isParseableScoreFunction: () => isParseableScoreFunction,
23670
23836
  isOutputSchemaUnknown: () => isOutputSchemaUnknown,
23671
23837
  isNonSuccessTerminalExecutionStatus: () => isNonSuccessTerminalExecutionStatus,
23672
23838
  isFilePathDescriptor: () => isFilePathDescriptor,
23673
23839
  isFileOutput: () => isFileOutput,
23840
+ hasValidScoreSignature: () => hasValidScoreSignature,
23674
23841
  hasTriggerMethod: () => hasTriggerMethod,
23675
23842
  goldenFileSatisfiedInResult: () => goldenFileSatisfiedInResult,
23676
23843
  getTriggerMethods: () => getTriggerMethods,
@@ -23678,6 +23845,7 @@ __export(exports_src, {
23678
23845
  getStepSchema: () => getStepSchema,
23679
23846
  getStepOutputSchema: () => getStepOutputSchema,
23680
23847
  getStepConfigSchema: () => getStepConfigSchema,
23848
+ getScoreFunctionRuntimeIssue: () => getScoreFunctionRuntimeIssue,
23681
23849
  getRequiredOutputKeys: () => getRequiredOutputKeys,
23682
23850
  getProcessorSchemas: () => getProcessorSchemas,
23683
23851
  getParserCategory: () => getParserCategory,
@@ -23693,11 +23861,13 @@ __export(exports_src, {
23693
23861
  formatDiffValue: () => formatDiffValue,
23694
23862
  fileFieldNamesFromJsonSchema: () => fileFieldNamesFromJsonSchema,
23695
23863
  fieldStemFromGoldenName: () => fieldStemFromGoldenName,
23864
+ extractScoreFunctionBody: () => extractScoreFunctionBody,
23696
23865
  extractFieldMetadata: () => extractFieldMetadata,
23697
23866
  executionWallClockMs: () => executionWallClockMs,
23698
23867
  evalInputKey: () => evalInputKey,
23699
23868
  evalExpectedKey: () => evalExpectedKey,
23700
23869
  errorHintFor: () => errorHintFor,
23870
+ defaultScoreFunction: () => defaultScoreFunction,
23701
23871
  compareOutput: () => compareOutput,
23702
23872
  compareExpectedValues: () => compareExpectedValues,
23703
23873
  combineEvalScores: () => combineEvalScores,
@@ -23892,6 +24062,10 @@ __export(exports_src, {
23892
24062
  ControlApprovalOutputSchema: () => ControlApprovalOutputSchema,
23893
24063
  ControlApprovalConfigSchema: () => ControlApprovalConfigSchema,
23894
24064
  ConnectorSchema: () => ConnectorSchema,
24065
+ CUSTOM_SCRIPT_MAX_TIMEOUT_MS: () => CUSTOM_SCRIPT_MAX_TIMEOUT_MS,
24066
+ CUSTOM_SCRIPT_MAX_MEMORY_MB: () => CUSTOM_SCRIPT_MAX_MEMORY_MB,
24067
+ CUSTOM_SCRIPT_MAX_BYTES: () => CUSTOM_SCRIPT_MAX_BYTES,
24068
+ CUSTOM_SCRIPT_CONTRACT: () => CUSTOM_SCRIPT_CONTRACT,
23895
24069
  BoundingRegionSchema: () => BoundingRegionSchema,
23896
24070
  BlockStepSchema: () => BlockStepSchema,
23897
24071
  BaseEntitySchema: () => BaseEntitySchema,
@@ -143406,7 +143580,7 @@ import { fileURLToPath as fileURLToPath6 } from "url";
143406
143580
  // package.json
143407
143581
  var package_default = {
143408
143582
  name: "@eigenpal/cli",
143409
- version: "0.4.9",
143583
+ version: "0.4.10",
143410
143584
  description: "Eigenpal CLI — author and run document workflows from your terminal. Agent-ready.",
143411
143585
  keywords: [
143412
143586
  "eigenpal",
@@ -146149,26 +146323,23 @@ function getExampleNames(dir, exampleNamesFilter) {
146149
146323
  function readInput(exampleDir) {
146150
146324
  const argsPath = join6(exampleDir, "input", "arguments.json");
146151
146325
  const inputDir = join6(exampleDir, "input");
146152
- const merged = JSON.parse(readFileSync5(argsPath, "utf-8"));
146326
+ const scalars = JSON.parse(readFileSync5(argsPath, "utf-8"));
146327
+ const files = [];
146153
146328
  for (const entry of readdirSync3(inputDir)) {
146154
146329
  const full = join6(inputDir, entry);
146155
146330
  if (!statSync2(full).isDirectory())
146156
146331
  continue;
146157
- const files = readdirSync3(full).filter((f) => statSync2(join6(full, f)).isFile()).sort();
146158
- if (files.length === 0)
146159
- continue;
146160
- const inlined = files.map((filename) => {
146161
- const buf = readFileSync5(join6(full, filename));
146162
- const mime = guessMimeType(filename);
146163
- return {
146164
- _inline: buf.toString("base64"),
146332
+ const filenames = readdirSync3(full).filter((f) => statSync2(join6(full, f)).isFile()).sort();
146333
+ for (const filename of filenames) {
146334
+ files.push({
146335
+ argument: entry,
146165
146336
  filename,
146166
- ...mime ? { mimeType: mime } : {}
146167
- };
146168
- });
146169
- merged[entry] = files.length === 1 ? inlined[0] : inlined;
146337
+ content: readFileSync5(join6(full, filename)),
146338
+ mimeType: guessMimeType(filename)
146339
+ });
146340
+ }
146170
146341
  }
146171
- return merged;
146342
+ return { scalars, files };
146172
146343
  }
146173
146344
  function readOverrides(exampleDir) {
146174
146345
  const metaPath = join6(exampleDir, "meta.json");
@@ -146182,8 +146353,10 @@ function readOverrides(exampleDir) {
146182
146353
  return null;
146183
146354
  }
146184
146355
  function buildExamplePayload(exampleDir) {
146356
+ const { scalars, files } = readInput(exampleDir);
146185
146357
  return {
146186
- input: readInput(exampleDir),
146358
+ scalars,
146359
+ files,
146187
146360
  overrides: readOverrides(exampleDir)
146188
146361
  };
146189
146362
  }
@@ -146665,11 +146838,19 @@ async function runExec(client, dir, workflowIdOrSlug, exampleNames, options8 = {
146665
146838
  progress.setRunning(index);
146666
146839
  else
146667
146840
  console.log(`${ui.dim("→")} ${name}`);
146668
- const res = await client.post(`/api/v1/workflows/${workflowId}/run`, {
146669
- input: example.input,
146670
- overrides: example.overrides ?? undefined,
146671
- trigger: "cli"
146672
- });
146841
+ const form = new FormData;
146842
+ const sidecar = { trigger: "cli" };
146843
+ if (Object.keys(example.scalars).length > 0)
146844
+ sidecar.input = example.scalars;
146845
+ if (example.overrides)
146846
+ sidecar.overrides = example.overrides;
146847
+ form.append("_json", JSON.stringify(sidecar));
146848
+ for (const file2 of example.files) {
146849
+ form.append(file2.argument, new Blob([file2.content], {
146850
+ type: file2.mimeType ?? "application/octet-stream"
146851
+ }), file2.filename);
146852
+ }
146853
+ const res = await client.postFormData(`/api/v1/workflows/${workflowId}/run`, form);
146673
146854
  if (typeof res?.executionId !== "string") {
146674
146855
  throw new Error("Run API did not return executionId");
146675
146856
  }
@@ -147464,7 +147645,7 @@ var EVALUATOR_REGISTRY = {
147464
147645
  "custom-script": {
147465
147646
  type: "custom-script",
147466
147647
  name: "Custom Script",
147467
- description: "Run sandboxed JavaScript that returns { score, label? }. Receives `actual`, `expected`, `input`.",
147648
+ description: "Score the workflow output with sandboxed JavaScript. Authors a `function scoreScript(expected, actual) { ... }` declaration that returns a number in [0, 1].",
147468
147649
  configSchema: CustomScriptConfigSchema
147469
147650
  }
147470
147651
  };
@@ -147506,6 +147687,7 @@ function registerEvaluatorTypeCommands(parent) {
147506
147687
  type: def.type,
147507
147688
  name: def.name,
147508
147689
  description: def.description,
147690
+ entrySchema: z12.toJSONSchema(EvaluatorBaseEntrySchema, { target: "draft-7" }),
147509
147691
  configSchema: z12.toJSONSchema(def.configSchema, { target: "draft-7" })
147510
147692
  };
147511
147693
  const text = JSON.stringify(result, null, 2);
@@ -148057,7 +148239,7 @@ flag would otherwise shadow it. Validation failures come back as
148057
148239
  error51(`Cannot update workflow ${ui.bold(workflowId)} — pass --bump <patch|minor|major> or --set-version <X.Y.Z>, or add a top-level \`version: X.Y.Z\` to ${filePath}.`);
148058
148240
  process.exit(1);
148059
148241
  }
148060
- const yamlToSend = cliWantsRewrite ? spliceWorkflowVersion(yamlOnDisk, nextVersion) : yamlOnDisk;
148242
+ const yamlToSend = cliWantsRewrite ? spliceWorkflowVersion2(yamlOnDisk, nextVersion) : yamlOnDisk;
148061
148243
  const result = await client.post(`/api/v1/workflows/${workflowId}/versions`, {
148062
148244
  yaml: yamlToSend,
148063
148245
  version: nextVersion
@@ -148071,7 +148253,7 @@ flag would otherwise shadow it. Validation failures come back as
148071
148253
  if (opts.bump) {
148072
148254
  throw new Error("--bump requires --workflow-id (need a current version to bump from). For a new workflow, use --set-version <X.Y.Z> or omit both.");
148073
148255
  }
148074
- const yamlToSend = opts.setVersion && !extractWorkflowVersion(yamlOnDisk) ? spliceWorkflowVersion(yamlOnDisk, opts.setVersion) : yamlOnDisk;
148256
+ const yamlToSend = opts.setVersion && !extractWorkflowVersion(yamlOnDisk) ? spliceWorkflowVersion2(yamlOnDisk, opts.setVersion) : yamlOnDisk;
148075
148257
  const result = await client.post("/api/v1/workflows", { yaml: yamlToSend });
148076
148258
  if (opts.json) {
148077
148259
  printJson(result);
@@ -148098,7 +148280,7 @@ function bumpSemver(current, level) {
148098
148280
  return `${major}.${minor + 1}.0`;
148099
148281
  return `${major}.${minor}.${patch + 1}`;
148100
148282
  }
148101
- function spliceWorkflowVersion(yaml, next) {
148283
+ function spliceWorkflowVersion2(yaml, next) {
148102
148284
  if (/^version:\s*['"]?(\d+\.\d+\.\d+)['"]?\s*$/m.test(yaml)) {
148103
148285
  return yaml.replace(/^version:.*$/m, `version: ${next}`);
148104
148286
  }
@@ -174,6 +174,12 @@ eigenpal workflow evaluators validate ./evaluators.yaml
174
174
  eigenpal workflow evaluators push <workflow-id> --file evaluators.yaml
175
175
  ```
176
176
 
177
+ Always set `description:` on every evaluator entry. It's a short, plain-language
178
+ sentence (≤500 chars; aim for one line) explaining what the evaluator measures
179
+ and why it matters, written for a non-technical reviewer in the dashboard.
180
+ Skip model names, thresholds, and dot-paths; those belong in `config`. See
181
+ [`reference/evaluators.md`](reference/evaluators.md#writing-descriptions-for-stakeholders).
182
+
177
183
  ### Build + validate + upload a dataset
178
184
 
179
185
  ```bash
@@ -11,16 +11,41 @@ this is the agent-facing summary.
11
11
  ```yaml
12
12
  evaluators: # array of evaluator definitions, ≥1 entry
13
13
  - name: ... # required, unique within this file, [a-z0-9][a-z0-9-_]*
14
+ description: ... # optional, ≤500 chars; one-sentence business explanation (see below)
14
15
  type: ... # required, one of: exact-diff | llm-judge | custom-script
15
16
  config: # required, shape depends on `type`
16
17
  passThreshold: 0.95 # 0..1, optional (default 1.0)
17
18
  ...
18
19
 
19
- # OPTIONAL global config
20
- concurrency: 4 # max examples in parallel during `experiment run` (default 1)
21
- judge: # optional shared model defaults for llm-judge evaluators
22
- model: gpt-4o
23
- temperature: 0
20
+ # OPTIONAL workflow-level pass threshold. A run passes when its weighted-mean
21
+ # score across evaluators clears this number. Defaults to 0.7.
22
+ passThreshold: 0.7
23
+ ```
24
+
25
+ ## Writing descriptions for stakeholders
26
+
27
+ Every evaluator entry takes an optional `description`. Always set it. The
28
+ dashboard surfaces this string to non-technical reviewers (legal, ops,
29
+ finance) who never see the YAML or the judge prompt. Write it for them:
30
+
31
+ - One short sentence, plain language.
32
+ - Name what the evaluator measures and why a business reader should care.
33
+ - Skip implementation detail (model names, thresholds, dot-paths). Those
34
+ belong in `config`.
35
+ - Keep it under ~120 characters when you can; the schema caps it at 500.
36
+
37
+ ```yaml
38
+ # Good. A non-technical reviewer immediately grasps the intent.
39
+ - name: covenants-recall
40
+ description: Checks every covenant from the contract appears in the extraction.
41
+ type: llm-judge
42
+ config: ...
43
+
44
+ # Bad. Restates the config; teaches a stakeholder nothing.
45
+ - name: covenants-recall
46
+ description: llm-judge with passThreshold 0.85 over the covenants array.
47
+ type: llm-judge
48
+ config: ...
24
49
  ```
25
50
 
26
51
  ## Evaluator types
@@ -47,23 +72,34 @@ judge: # optional shared model defaults for llm-judge evaluators
47
72
  - name: covenants-recall
48
73
  type: llm-judge
49
74
  config:
50
- model: gpt-4o # string — the OpenAI model id
51
- temperature: 0 # number — for reproducibility
75
+ model: gpt-4o # OPTIONAL — falls back to the workspace default LLM provider
76
+ mode: continuous # continuous | discrete (default: continuous)
52
77
  passThreshold: 0.85
53
- prompt: |
78
+ docLabel: extraction # what the judge calls the workflow output (default: "output")
79
+ promptExtension: |
54
80
  Score how well the actual output captures every covenant in the
55
- expected output, on a scale of 0..1. Penalise missing covenants;
56
- do not penalise over-extraction. Return JSON { score, rationale }.
57
- ```
81
+ expected output. Penalise missing covenants; do not penalise
82
+ over-extraction. Be strict on covenants that change loan economics.
58
83
 
59
- The harness wraps your `prompt` and feeds:
60
-
61
- - `actual` — the workflow's output
62
- - `expected` — the example's `expected/output.json`
63
- - `meta` — example metadata
84
+ # discrete mode — the judge picks a label and the score table maps to a number
85
+ - name: tone-check
86
+ type: llm-judge
87
+ config:
88
+ mode: discrete
89
+ passThreshold: 0.7
90
+ labels:
91
+ excellent: 1.0
92
+ acceptable: 0.7
93
+ off-tone: 0.0
94
+ promptExtension: |
95
+ Pick the label that best matches the tone of the response.
96
+ ```
64
97
 
65
- It expects the model to return JSON with at least `score` (0..1) and
66
- `rationale` (string). The fixed harness handles parse failures + retries.
98
+ `promptExtension` is the only required field — it's appended to the harness
99
+ prompt and tells the judge what "good" looks like for this workflow. The
100
+ harness handles JSON parsing, retries, and feeds the judge `actual`,
101
+ `expected`, and example metadata. In `discrete` mode the judge MUST return
102
+ one of the keys in `labels`; the harness then looks up the score.
67
103
 
68
104
  ### `custom-script` — JavaScript in the sandbox
69
105
 
@@ -72,16 +108,38 @@ It expects the model to return JSON with at least `score` (0..1) and
72
108
  type: custom-script
73
109
  config:
74
110
  passThreshold: 1.0
75
- code: |
76
- const a = actual.totalAmount ?? 0;
77
- const e = expected.totalAmount ?? 0;
78
- const score = Math.abs(a - e) <= 0.01 ? 1 : 0;
79
- return { score, rationale: `actual=${a}, expected=${e}` };
111
+ function: |
112
+ /**
113
+ * @param {Expected} expected the example's expected output
114
+ * @param {Actual} actual the workflow's actual output
115
+ * @returns {number} a score in [0, 1]; throws are caught and scored as 0
116
+ */
117
+ function scoreScript(expected, actual) {
118
+ const a = actual.totalAmount ?? 0;
119
+ const e = expected.totalAmount ?? 0;
120
+ return Math.abs(a - e) <= 0.01 ? 1 : 0;
121
+ }
80
122
  ```
81
123
 
82
124
  Same WASM sandbox as `transform.script` (5s wall clock, 10 MB heap, no
83
- network). Receives `actual`, `expected`, `meta` in scope. Returns
84
- `{ score: number, label?: string, rationale?: string }`.
125
+ network). The YAML stores the **whole** `function scoreScript(expected,
126
+ actual) { ... }` declaration verbatim — same text the dashboard editor
127
+ shows. The JSDoc preamble is optional but recommended: it pulls the
128
+ workflow's output schema into IntelliSense for `expected.…` /
129
+ `actual.…` in the dashboard.
130
+
131
+ The signature shape is **locked**:
132
+
133
+ - Function name MUST be `scoreScript`.
134
+ - Parameters MUST be `(expected, actual)` in that order. Swapping them
135
+ would silently invert every score.
136
+ - Body MUST `return` a number in `[0, 1]`, or `throw` (which is caught
137
+ and scored as 0).
138
+
139
+ `workflow validate` and `workflow evaluators validate` flag YAML where
140
+ the function fails to parse, the signature doesn't match, or the body
141
+ contains neither `return` nor `throw` — so a hand-edited config errors
142
+ before it reaches the worker.
85
143
 
86
144
  ## Pass / fail vs score
87
145
 
@@ -160,9 +218,11 @@ process — `--out` writes the file directly from the server's storage.
160
218
  LLM-judge scores have variance. To reduce flake:
161
219
 
162
220
  - Pin a specific model id (`gpt-4o-2024-08-06` rather than `gpt-4o`).
163
- - Set `temperature: 0`.
164
- - Keep the prompt narrow — score one dimension per evaluator. Use
221
+ - Keep `promptExtension` narrow — score one dimension per evaluator. Use
165
222
  multiple judges instead of one omnibus prompt.
223
+ - Prefer `mode: discrete` with a small label set when the judgement is
224
+ categorical (good / acceptable / bad) — it eliminates the model's
225
+ free-form numeric drift.
166
226
  - For high-stakes thresholds, run the experiment ≥3 times and average.
167
227
 
168
228
  <!-- GENERATED:EVALUATOR_CATALOG START -->
@@ -177,6 +237,14 @@ _Generated from `EvalConfigYamlSchema` in `@eigenpal/types/src/eval/evaluator-co
177
237
  | `evaluators` | array<object> | no | `[]` | |
178
238
  | `passThreshold` | number | no | `0.7` | |
179
239
 
240
+ ### Common entry fields (every evaluator type)
241
+
242
+ | Field | Type | Required | Default | Description |
243
+ | --- | --- | --- | --- | --- |
244
+ | `name` | string | yes | | Identifier used in eval results and the weighted-mean formula. Use a short kebab-case slug like header-exact or covenants-recall. |
245
+ | `description` | string | no | | One-sentence explanation, in business language, of what this evaluator measures and why a stakeholder should care. Shown in the dashboard to non-technical reviewers (legal, ops, finance) who never see the YAML or judge prompt. Skip implementation detail (model names, thresholds, dot-paths); those live in `config`. |
246
+ | `weight` | number | no | `1` | Relative importance in the weighted mean. The overall score is Σ(weightᵢ × scoreᵢ) / Σweight. Set to 0 to score-only without affecting overall. |
247
+
180
248
  ### `exact-diff` — JSON deep-diff against expected output
181
249
 
182
250
  | Field | Type | Required | Default | Description |
@@ -203,7 +271,7 @@ _Generated from `EvalConfigYamlSchema` in `@eigenpal/types/src/eval/evaluator-co
203
271
 
204
272
  | Field | Type | Required | Default | Description |
205
273
  | --- | --- | --- | --- | --- |
206
- | `code` | string | yes | | Sandboxed JavaScript. Receives `actual`, `expected`, `input` as variables and must return { score, label? } where score is in [0, 1]. |
274
+ | `function` | string | yes | | Full JavaScript declaration of `function scoreScript(expected, actual) { ... }`. Receives `expected` (the example's expected output) and `actual` (the workflow's actual output), returns a number in [0, 1]. Throws are caught and scored as 0. |
207
275
  | `timeoutMs` | integer | no | `5000` | Maximum wall-clock time the script can run before it's killed and the run is marked failed. |
208
276
  | `memoryLimitMb` | integer | no | `10` | Maximum memory the sandbox may allocate. Increase if the script processes large arrays or strings. |
209
277
  | `passThreshold` | number | no | `1` | Minimum script score this evaluator must reach for a run to pass. |
@@ -1,5 +1,6 @@
1
1
  evaluators:
2
2
  - name: exact-match
3
+ description: Checks the extracted fields match the expected output exactly.
3
4
  type: exact-diff
4
5
  config:
5
6
  passThreshold: 1.0
@@ -1,5 +1,6 @@
1
1
  evaluators:
2
2
  - name: label-match
3
+ description: Checks the predicted label matches the expected label.
3
4
  type: exact-diff
4
5
  config:
5
6
  passThreshold: 1.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eigenpal/cli",
3
- "version": "0.4.9",
3
+ "version": "0.4.10",
4
4
  "description": "Eigenpal CLI — author and run document workflows from your terminal. Agent-ready.",
5
5
  "keywords": [
6
6
  "eigenpal",