bitfab 0.29.1 → 0.30.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -749,6 +749,15 @@ interface ReplayOptions {
749
749
  * reconstructed. Validated server-side against the org.
750
750
  */
751
751
  datasetId?: string;
752
+ /**
753
+ * Graders to attach directly to this experiment (test run), independent of any
754
+ * graders already on the dataset. The resulting experiment is graded by the
755
+ * union of these and the dataset's runnable graders at completion, so use this
756
+ * to grade a single run with a check you don't want to add to the dataset
757
+ * permanently. Each id must be an active/live grader belonging to the same
758
+ * organization and trace function, otherwise the server rejects the replay.
759
+ */
760
+ graderIds?: string[];
752
761
  /**
753
762
  * Reshape recorded inputs before they are spread into `fn`.
754
763
  *
@@ -1799,7 +1808,7 @@ declare class BitfabFunction {
1799
1808
  /**
1800
1809
  * SDK version from package.json (injected at build time)
1801
1810
  */
1802
- declare const __version__ = "0.29.1";
1811
+ declare const __version__ = "0.30.1";
1803
1812
 
1804
1813
  /**
1805
1814
  * Constants for the Bitfab SDK.
package/dist/index.d.ts CHANGED
@@ -749,6 +749,15 @@ interface ReplayOptions {
749
749
  * reconstructed. Validated server-side against the org.
750
750
  */
751
751
  datasetId?: string;
752
+ /**
753
+ * Graders to attach directly to this experiment (test run), independent of any
754
+ * graders already on the dataset. The resulting experiment is graded by the
755
+ * union of these and the dataset's runnable graders at completion, so use this
756
+ * to grade a single run with a check you don't want to add to the dataset
757
+ * permanently. Each id must be an active/live grader belonging to the same
758
+ * organization and trace function, otherwise the server rejects the replay.
759
+ */
760
+ graderIds?: string[];
752
761
  /**
753
762
  * Reshape recorded inputs before they are spread into `fn`.
754
763
  *
@@ -1799,7 +1808,7 @@ declare class BitfabFunction {
1799
1808
  /**
1800
1809
  * SDK version from package.json (injected at build time)
1801
1810
  */
1802
- declare const __version__ = "0.29.1";
1811
+ declare const __version__ = "0.30.1";
1803
1812
 
1804
1813
  /**
1805
1814
  * Constants for the Bitfab SDK.
package/dist/index.js CHANGED
@@ -23,12 +23,12 @@ import {
23
23
  flushTraces,
24
24
  getCurrentSpan,
25
25
  getCurrentTrace
26
- } from "./chunk-RTPEBWVO.js";
26
+ } from "./chunk-ERHYLE4S.js";
27
27
  import {
28
28
  BITFAB_PROGRESS_PREFIX,
29
29
  BitfabError,
30
30
  reportReplayProgress
31
- } from "./chunk-YZU6WFG2.js";
31
+ } from "./chunk-MYDYCNW4.js";
32
32
  export {
33
33
  BITFAB_PROGRESS_PREFIX,
34
34
  Bitfab,
package/dist/node.cjs CHANGED
@@ -336,6 +336,203 @@ var init_replayContext = __esm({
336
336
  }
337
337
  });
338
338
 
339
+ // src/codeChange.ts
340
+ async function resolveAutoCodeChange(label) {
341
+ if (typeof process === "undefined") {
342
+ return null;
343
+ }
344
+ if (process.env?.BITFAB_DISABLE_CODE_CHANGE_CAPTURE) {
345
+ return null;
346
+ }
347
+ const fromEnv = await readCodeChangeFile();
348
+ if (fromEnv) {
349
+ return fromEnv;
350
+ }
351
+ return captureCodeChangeFromGit(process.cwd?.() ?? ".", label);
352
+ }
353
+ async function readCodeChangeFile() {
354
+ const path = process.env?.BITFAB_CODE_CHANGE_PATH;
355
+ if (!path) {
356
+ return null;
357
+ }
358
+ try {
359
+ const { readFile } = await import("fs/promises");
360
+ const parsed = JSON.parse(await readFile(path, "utf8"));
361
+ const files = Array.isArray(parsed?.files) && parsed.files.every(
362
+ (f) => typeof f === "object" && f !== null && !Array.isArray(f)
363
+ ) ? parsed.files : void 0;
364
+ const description = typeof parsed?.description === "string" ? parsed.description : void 0;
365
+ if (!files && description === void 0) {
366
+ return null;
367
+ }
368
+ return { description, files };
369
+ } catch {
370
+ return null;
371
+ }
372
+ }
373
+ async function captureCodeChangeFromGit(cwd, label) {
374
+ let execFile;
375
+ let readFile;
376
+ try {
377
+ ;
378
+ ({ execFile } = await import("child_process"));
379
+ ({ readFile } = await import("fs/promises"));
380
+ } catch {
381
+ return null;
382
+ }
383
+ const git = (dir, args) => new Promise((resolve) => {
384
+ execFile(
385
+ "git",
386
+ args,
387
+ // 30s timeout so a hung git (e.g. a network-touching ref op) can't
388
+ // block the whole replay indefinitely.
389
+ { cwd: dir, maxBuffer: 64 * 1024 * 1024, timeout: 3e4 },
390
+ (err, stdout) => resolve(err ? null : stdout)
391
+ );
392
+ });
393
+ try {
394
+ const root = (await git(cwd, ["rev-parse", "--show-toplevel"]))?.trim();
395
+ if (!root) {
396
+ return null;
397
+ }
398
+ const resolved = await resolveBase(git, root);
399
+ if (!resolved) {
400
+ return null;
401
+ }
402
+ const { base, fromTrunk } = resolved;
403
+ const blobBytes = async (ref, path) => {
404
+ const out = await git(root, ["cat-file", "-s", `${ref}:${path}`]);
405
+ const n = out ? Number.parseInt(out.trim(), 10) : Number.NaN;
406
+ return Number.isFinite(n) ? n : 0;
407
+ };
408
+ const workingBytes = async (path) => {
409
+ try {
410
+ const { stat } = await import("fs/promises");
411
+ const { join } = await import("path");
412
+ return (await stat(join(root, path))).size;
413
+ } catch {
414
+ return 0;
415
+ }
416
+ };
417
+ const tracked = await git(root, [
418
+ "diff",
419
+ "--name-status",
420
+ "--no-renames",
421
+ "-z",
422
+ base,
423
+ "--",
424
+ ":!.bitfab"
425
+ ]);
426
+ const untracked = await git(root, [
427
+ "ls-files",
428
+ "--others",
429
+ "--exclude-standard",
430
+ "-z",
431
+ "--",
432
+ ":!.bitfab"
433
+ ]);
434
+ const entries = [
435
+ ...parseNameStatusZ(tracked ?? ""),
436
+ ...(untracked ?? "").split(NUL).filter((p) => p.length > 0).map((path) => ({ status: "A", path }))
437
+ ];
438
+ if (entries.length === 0) {
439
+ return null;
440
+ }
441
+ const files = [];
442
+ let totalBytes = 0;
443
+ for (const { status, path } of entries) {
444
+ if (files.length >= MAX_FILES) {
445
+ break;
446
+ }
447
+ const beforeBytes = status === "A" ? 0 : await blobBytes(base, path);
448
+ const afterBytes = status === "D" ? 0 : await workingBytes(path);
449
+ if (beforeBytes > MAX_FILE_BYTES || afterBytes > MAX_FILE_BYTES) {
450
+ continue;
451
+ }
452
+ const before = (status === "A" ? "" : await git(root, ["show", `${base}:${path}`]) ?? "").replace(/\r\n/g, "\n");
453
+ const after = (status === "D" ? "" : await readWorkingFile(readFile, root, path)).replace(/\r\n/g, "\n");
454
+ if (before === after) {
455
+ continue;
456
+ }
457
+ const size = Buffer.byteLength(before, "utf8") + Buffer.byteLength(after, "utf8");
458
+ if (totalBytes + size > MAX_TOTAL_BYTES || looksBinary(before) || looksBinary(after)) {
459
+ continue;
460
+ }
461
+ totalBytes += size;
462
+ files.push({ path, before, after });
463
+ }
464
+ if (files.length === 0) {
465
+ return null;
466
+ }
467
+ const subject = (await git(root, ["log", "-1", "--format=%s", "HEAD"]))?.trim();
468
+ const fileWord = files.length === 1 ? "file" : "files";
469
+ const head = label?.trim() || subject || "Working-tree change";
470
+ const against = fromTrunk ? "vs trunk" : "uncommitted (vs HEAD)";
471
+ return {
472
+ description: `${head} (${files.length} ${fileWord} changed ${against})`,
473
+ files
474
+ };
475
+ } catch {
476
+ return null;
477
+ }
478
+ }
479
+ async function resolveBase(git, root) {
480
+ const forced = process.env?.BITFAB_CODE_CHANGE_BASE;
481
+ if (forced && await refExists(git, root, forced)) {
482
+ const base = (await git(root, ["merge-base", "HEAD", forced]))?.trim() || (await git(root, ["rev-parse", "--verify", forced]))?.trim() || null;
483
+ return base ? { base, fromTrunk: true } : null;
484
+ }
485
+ for (const candidate of TRUNK_CANDIDATES) {
486
+ if (!await refExists(git, root, candidate)) {
487
+ continue;
488
+ }
489
+ const mb = (await git(root, ["merge-base", "HEAD", candidate]))?.trim();
490
+ if (mb) {
491
+ return { base: mb, fromTrunk: true };
492
+ }
493
+ }
494
+ return await refExists(git, root, "HEAD") ? { base: "HEAD", fromTrunk: false } : null;
495
+ }
496
+ async function refExists(git, root, ref) {
497
+ return await git(root, ["rev-parse", "--verify", `${ref}^{object}`]) !== null;
498
+ }
499
+ async function readWorkingFile(readFile, root, path) {
500
+ try {
501
+ const { join } = await import("path");
502
+ return await readFile(join(root, path), "utf8");
503
+ } catch {
504
+ return "";
505
+ }
506
+ }
507
+ function parseNameStatusZ(raw) {
508
+ const parts = raw.split(NUL).filter((p) => p.length > 0);
509
+ const out = [];
510
+ for (let i = 0; i + 1 < parts.length; i += 2) {
511
+ out.push({ status: parts[i].charAt(0), path: parts[i + 1] });
512
+ }
513
+ return out;
514
+ }
515
+ function looksBinary(s) {
516
+ return s.slice(0, 8e3).includes(NUL);
517
+ }
518
+ var MAX_FILES, MAX_FILE_BYTES, MAX_TOTAL_BYTES, TRUNK_CANDIDATES, NUL;
519
+ var init_codeChange = __esm({
520
+ "src/codeChange.ts"() {
521
+ "use strict";
522
+ MAX_FILES = 60;
523
+ MAX_FILE_BYTES = 5e5;
524
+ MAX_TOTAL_BYTES = 2e6;
525
+ TRUNK_CANDIDATES = [
526
+ "origin/HEAD",
527
+ "origin/main",
528
+ "origin/master",
529
+ "main",
530
+ "master"
531
+ ];
532
+ NUL = String.fromCharCode(0);
533
+ }
534
+ });
535
+
339
536
  // src/replay.ts
340
537
  var replay_exports = {};
341
538
  __export(replay_exports, {
@@ -560,6 +757,15 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
560
757
  }
561
758
  }
562
759
  await replayContextReady;
760
+ let codeChangeDescription = options?.codeChangeDescription;
761
+ let codeChangeFiles = options?.codeChangeFiles;
762
+ if (codeChangeDescription === void 0 && codeChangeFiles === void 0) {
763
+ const captured = await resolveAutoCodeChange(options?.name);
764
+ if (captured) {
765
+ codeChangeDescription = captured.description;
766
+ codeChangeFiles = captured.files;
767
+ }
768
+ }
563
769
  const {
564
770
  testRunId,
565
771
  testRunUrl,
@@ -571,12 +777,13 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
571
777
  options?.traceIds ? void 0 : options?.limit ?? 5,
572
778
  options?.traceIds,
573
779
  options?.name,
574
- options?.codeChangeDescription,
575
- options?.codeChangeFiles,
780
+ codeChangeDescription,
781
+ codeChangeFiles,
576
782
  options?.environment !== void 0,
577
783
  // includeDbBranchLease
578
784
  options?.experimentGroupId,
579
- options?.datasetId
785
+ options?.datasetId,
786
+ options?.graderIds
580
787
  );
581
788
  const mockStrategy = options?.mock ?? "marked";
582
789
  const maxConcurrency = options?.maxConcurrency ?? 10;
@@ -727,6 +934,7 @@ var BITFAB_PROGRESS_PREFIX;
727
934
  var init_replay = __esm({
728
935
  "src/replay.ts"() {
729
936
  "use strict";
937
+ init_codeChange();
730
938
  init_errors();
731
939
  init_mockOverride();
732
940
  init_randomUuid();
@@ -769,7 +977,7 @@ registerAsyncLocalStorageClass(
769
977
  );
770
978
 
771
979
  // src/version.generated.ts
772
- var __version__ = "0.29.1";
980
+ var __version__ = "0.30.1";
773
981
 
774
982
  // src/constants.ts
775
983
  var DEFAULT_SERVICE_URL = "https://bitfab.ai";
@@ -1128,7 +1336,7 @@ var HttpClient = class {
1128
1336
  * Start a replay session by fetching historical traces.
1129
1337
  * Blocking call - creates a test run and returns lightweight item references.
1130
1338
  */
1131
- async startReplay(traceFunctionKey, limit, traceIds, name, codeChangeDescription, codeChangeFiles, includeDbBranchLease, experimentGroupId, datasetId) {
1339
+ async startReplay(traceFunctionKey, limit, traceIds, name, codeChangeDescription, codeChangeFiles, includeDbBranchLease, experimentGroupId, datasetId, graderIds) {
1132
1340
  const payload = { traceFunctionKey };
1133
1341
  if (limit !== void 0) {
1134
1342
  payload.limit = limit;
@@ -1154,6 +1362,9 @@ var HttpClient = class {
1154
1362
  if (datasetId !== void 0) {
1155
1363
  payload.datasetId = datasetId;
1156
1364
  }
1365
+ if (graderIds !== void 0) {
1366
+ payload.graderIds = graderIds;
1367
+ }
1157
1368
  const timeout = includeDbBranchLease ? 18e4 : 3e4;
1158
1369
  return this.request("/api/sdk/replay/start", payload, {
1159
1370
  timeout