oh-my-promptfoo 1.26.0-next.145 → 1.26.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.
@@ -6,6 +6,7 @@ let node_fs = require("node:fs");
6
6
  let node_fs_promises = require("node:fs/promises");
7
7
  let node_os = require("node:os");
8
8
  let node_module = require("node:module");
9
+ let node_util = require("node:util");
9
10
  // packages/oh-my-promptfoo/src/rubric-contract.ts
10
11
  const PROMPTFOO_TEMPLATE_SLOT = /\{\{-?\s*(output|rubric)\b/;
11
12
  let templateEngine;
@@ -1342,6 +1343,152 @@ function skillNotLoaded(_output, context) {
1342
1343
  return error(cause instanceof Error ? cause.message : String(cause));
1343
1344
  }
1344
1345
  }
1346
+ // packages/oh-my-promptfoo/src/workspace-file.ts
1347
+ function criterion(input) {
1348
+ const value = require_fs.closed(input, [
1349
+ "path",
1350
+ "equals",
1351
+ "contains",
1352
+ "regex",
1353
+ "isJson",
1354
+ "jsonPath",
1355
+ "absent",
1356
+ "byteLength",
1357
+ "encoding"
1358
+ ], "workspace-file criterion");
1359
+ workspacePath(value.path);
1360
+ for (const key of ["equals", "contains"]) if (value[key] !== void 0 && typeof value[key] !== "string") throw new Error(`${key} must be a string`);
1361
+ for (const key of ["isJson", "absent"]) if (value[key] !== void 0 && typeof value[key] !== "boolean") throw new Error(`${key} must be boolean`);
1362
+ if (value.byteLength !== void 0 && (!Number.isSafeInteger(value.byteLength) || value.byteLength < 0)) throw new Error("byteLength must be a nonnegative integer");
1363
+ if (value.encoding !== void 0 && ![
1364
+ "utf8",
1365
+ "utf16le",
1366
+ "ascii",
1367
+ "latin1"
1368
+ ].includes(String(value.encoding))) throw new Error("Unsupported workspace-file encoding");
1369
+ if (value.absent === true && Object.keys(value).some((key) => key !== "path" && key !== "absent")) throw new Error("absent: true cannot be combined with content clauses");
1370
+ if (value.jsonPath !== void 0) segments((typeof value.jsonPath === "string" ? { path: value.jsonPath } : require_fs.closed(value.jsonPath, ["path", "equals"], "jsonPath")).path);
1371
+ if (value.regex !== void 0) pattern(value.regex);
1372
+ return value;
1373
+ }
1374
+ function segments(path) {
1375
+ if (typeof path !== "string") throw new Error("jsonPath requires a string path");
1376
+ if (path === "$" || path === "") return [];
1377
+ if (path.startsWith("/")) {
1378
+ if (/~(?![01])/.test(path)) throw new Error("Invalid JSON Pointer escape");
1379
+ return path.slice(1).split("/").map((part) => part.replaceAll("~1", "/").replaceAll("~0", "~"));
1380
+ }
1381
+ const parts = path.replace(/^\$\./, "").split(".");
1382
+ if (parts.some((part) => !part || /[[\]*]/.test(part))) throw new Error("jsonPath accepts JSON Pointer or dotted properties, not expressions");
1383
+ return parts;
1384
+ }
1385
+ function pattern(value) {
1386
+ if (typeof value === "string") return new RegExp(value);
1387
+ const spec = require_fs.closed(value, ["regex", "flags"], "regex");
1388
+ if (typeof spec.regex !== "string" || spec.flags !== void 0 && typeof spec.flags !== "string") throw new Error("regex requires a pattern and optional string flags");
1389
+ return new RegExp(spec.regex, spec.flags);
1390
+ }
1391
+ function check(file, bytes) {
1392
+ if (file.absent === true) return bytes === void 0 ? [] : ["File exists"];
1393
+ if (bytes === void 0) throw new Error(`Workspace file ${file.path} is absent`);
1394
+ const failures = [];
1395
+ if (file.byteLength !== void 0 && bytes.length !== file.byteLength) failures.push(`Expected ${file.byteLength} bytes, got ${bytes.length}`);
1396
+ if (file.encoding === void 0 && file.equals === void 0 && file.contains === void 0 && file.regex === void 0 && file.isJson === void 0 && file.jsonPath === void 0) return failures;
1397
+ const encoding = file.encoding ?? "utf8";
1398
+ let text;
1399
+ try {
1400
+ if (encoding === "ascii" && bytes.some((byte) => byte > 127)) throw new Error("non-ASCII bytes");
1401
+ text = encoding === "latin1" || encoding === "ascii" ? bytes.toString(encoding) : new TextDecoder(encoding === "utf16le" ? "utf-16le" : "utf-8", {
1402
+ fatal: true,
1403
+ ignoreBOM: true
1404
+ }).decode(bytes);
1405
+ } catch {
1406
+ return [...failures, `File is not valid ${encoding}`];
1407
+ }
1408
+ let selected = text;
1409
+ if (file.isJson !== void 0 || file.jsonPath !== void 0) {
1410
+ let parsed;
1411
+ let validJson = true;
1412
+ try {
1413
+ parsed = JSON.parse(text);
1414
+ } catch {
1415
+ validJson = false;
1416
+ if (file.isJson !== false || file.jsonPath !== void 0) return [...failures, "File is not valid JSON"];
1417
+ }
1418
+ if (file.isJson === false && validJson) failures.push("File is valid JSON");
1419
+ if (file.jsonPath !== void 0) {
1420
+ const spec = typeof file.jsonPath === "string" ? { path: file.jsonPath } : file.jsonPath;
1421
+ selected = parsed;
1422
+ for (const part of segments(spec.path)) {
1423
+ if (selected === null || typeof selected !== "object" || !Object.hasOwn(selected, part)) return [...failures, `JSON field ${spec.path} is absent`];
1424
+ selected = selected[part];
1425
+ }
1426
+ if ("equals" in spec && !(0, node_util.isDeepStrictEqual)(selected, spec.equals)) failures.push(`JSON field ${spec.path} does not equal the expected value`);
1427
+ }
1428
+ }
1429
+ if (file.equals !== void 0 && selected !== file.equals) failures.push("Content does not equal the expected string");
1430
+ if (file.contains !== void 0 && (typeof selected !== "string" || !selected.includes(file.contains))) failures.push("Content does not contain the expected string");
1431
+ if (file.regex !== void 0 && (typeof selected !== "string" || !pattern(file.regex).test(selected))) failures.push("Content does not match the regex");
1432
+ return failures;
1433
+ }
1434
+ /** Deterministic file criteria, all ANDed; evidence errors never certify a pass. */
1435
+ async function workspaceFile(_output, context) {
1436
+ const componentResults = [];
1437
+ try {
1438
+ const config = context?.config;
1439
+ let files;
1440
+ if (config && typeof config === "object" && "files" in config) {
1441
+ const spec = require_fs.closed(config, ["files"], "workspace-file config");
1442
+ if (!Array.isArray(spec.files) || !spec.files.length) throw new Error("files must be a nonempty list");
1443
+ files = spec.files.map(criterion);
1444
+ } else files = [criterion(config)];
1445
+ if (new Set(files.map((file) => file.path)).size !== files.length) throw new Error("files must have unique paths");
1446
+ const workspace = new CapturedWorkspace(context);
1447
+ for (const file of files) try {
1448
+ const failures = check(file, await workspace.read(file.path));
1449
+ const pass = failures.length === 0;
1450
+ componentResults.push({
1451
+ pass,
1452
+ score: pass ? 1 : 0,
1453
+ reason: failures.join("; ") || "File criteria passed",
1454
+ assertion: {
1455
+ type: "javascript",
1456
+ metric: file.path
1457
+ },
1458
+ metadata: {}
1459
+ });
1460
+ } catch (cause) {
1461
+ componentResults.push({
1462
+ pass: false,
1463
+ score: 0,
1464
+ reason: cause instanceof Error ? cause.message : String(cause),
1465
+ assertion: {
1466
+ type: "javascript",
1467
+ metric: file.path
1468
+ },
1469
+ metadata: { graderError: true }
1470
+ });
1471
+ }
1472
+ const pass = componentResults.every((result) => result.pass);
1473
+ return {
1474
+ pass,
1475
+ score: pass ? 1 : 0,
1476
+ reason: pass ? "All workspace file criteria passed" : componentResults.filter((result) => !result.pass).map((result) => `${result.assertion.metric}: ${result.reason}`).join("; "),
1477
+ namedScores: Object.fromEntries(componentResults.map((result) => [result.assertion.metric, result.score])),
1478
+ componentResults,
1479
+ metadata: componentResults.some((result) => result.metadata.graderError) ? { graderError: true } : {}
1480
+ };
1481
+ } catch (cause) {
1482
+ return {
1483
+ pass: false,
1484
+ score: 0,
1485
+ reason: cause instanceof Error ? cause.message : String(cause),
1486
+ namedScores: {},
1487
+ componentResults,
1488
+ metadata: { graderError: true }
1489
+ };
1490
+ }
1491
+ }
1345
1492
  // packages/oh-my-promptfoo/src/assertions.ts
1346
1493
  const SELF = /^package:oh-my-promptfoo\/assertions:llmAssert$/;
1347
1494
  const MISPLACED_PROVIDER_ERROR = "llmAssert does not see provider: beside config: (Promptfoo does not pass it to package assertions): move it into this assertion's config.provider, or set the judge for the suite in defaultTest.options.provider, for a test in its options.provider, or for the run with --grader";
@@ -1428,5 +1575,6 @@ exports["skill-not-loaded"] = skillNotLoaded;
1428
1575
  exports["tool-called"] = toolCalled;
1429
1576
  exports["tool-not-called"] = toolNotCalled;
1430
1577
  exports.withAssertions = withAssertions;
1578
+ exports["workspace-file"] = workspaceFile;
1431
1579
 
1432
1580
  //# sourceMappingURL=assertions.cjs.map