terminalhire 0.42.35 → 0.42.37

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.
@@ -26207,12 +26207,22 @@ function reportedCounts(classified, reparsed) {
26207
26207
  function readCounts(stdout, stderr = "") {
26208
26208
  const out = `${stdout}
26209
26209
  ${stderr}`;
26210
+ const matched = [];
26210
26211
  for (const reader of READERS) {
26211
26212
  const counts = reader.read(out);
26212
26213
  if (counts)
26213
- return { ...counts, runner: reader.runner };
26214
+ matched.push({ reader, counts });
26214
26215
  }
26215
- return null;
26216
+ const first = matched[0];
26217
+ if (first === void 0)
26218
+ return null;
26219
+ const legs = matched.filter(({ reader }) => !matched.some((m) => reader.shadowedBy?.includes(m.reader.runner)));
26220
+ const partial = legs.length > 1 || (first.reader.summaries?.(out) ?? 1) > 1;
26221
+ return {
26222
+ ...first.counts,
26223
+ runner: first.reader.runner,
26224
+ ...partial ? { coverage: "partial" } : {}
26225
+ };
26216
26226
  }
26217
26227
  function coverageThresholdUnmet(facts, counts) {
26218
26228
  if (counts === null || counts.runner !== "node-tap")
@@ -26371,7 +26381,7 @@ function isGreen(outcome) {
26371
26381
  function isOurFault(outcome) {
26372
26382
  return LIMIT_OWNER[outcome] === "ours";
26373
26383
  }
26374
- var VERIFICATION_OUTCOMES, int, withSuiteFailures, READERS, SUPPORTED_RUNNERS, COVERAGE_TABLE, EXEC_FAILURE, SUITE_REPORTED_FAILURE, MISSING_SYSTEM_DEPENDENCY, OFFLINE_BUILD_GAP, LIMIT_OWNER;
26384
+ var VERIFICATION_OUTCOMES, int, withSuiteFailures, countLines, READERS, SUPPORTED_RUNNERS, COVERAGE_TABLE, EXEC_FAILURE, SUITE_REPORTED_FAILURE, MISSING_SYSTEM_DEPENDENCY, OFFLINE_BUILD_GAP, LIMIT_OWNER;
26375
26385
  var init_classify2 = __esm({
26376
26386
  "../../packages/envrun/dist/classify.js"() {
26377
26387
  "use strict";
@@ -26390,6 +26400,7 @@ var init_classify2 = __esm({
26390
26400
  return counts;
26391
26401
  return { ...counts, tests_failed: int(/(\d+) failed/.exec(suiteLine[1])) };
26392
26402
  };
26403
+ countLines = (out, re) => (out.match(re) ?? []).length;
26393
26404
  READERS = [
26394
26405
  {
26395
26406
  // `node --test` TAP. Anchored to line start, which is what makes it a
@@ -26401,7 +26412,10 @@ var init_classify2 = __esm({
26401
26412
  if (!pass || !fail2)
26402
26413
  return null;
26403
26414
  return { tests_passed: int(pass), tests_failed: int(fail2) };
26404
- }
26415
+ },
26416
+ // One `# pass N` per invocation, so a script that runs `node --test` once per file
26417
+ // prints many. envrun's own offline leg printed 67 and was read as the first one's 4.
26418
+ summaries: (out) => countLines(out, /^# pass \d+$/gm)
26405
26419
  },
26406
26420
  {
26407
26421
  // node-tap's runner summary, which is NOT the `# pass N` / `# fail N` pair
@@ -26428,7 +26442,8 @@ var init_classify2 = __esm({
26428
26442
  tests_passed: int(/\bpass: (\d+)/.exec(line[1])),
26429
26443
  tests_failed: int(/\bfail: (\d+)/.exec(line[1]))
26430
26444
  };
26431
- }
26445
+ },
26446
+ summaries: (out) => countLines(out, /^# \{ total: \d+[^}\n]* \}$/gm)
26432
26447
  },
26433
26448
  {
26434
26449
  // jest: `Tests: 1 failed, 2 passed, 3 total`
@@ -26441,7 +26456,8 @@ var init_classify2 = __esm({
26441
26456
  tests_passed: int(/(\d+) passed/.exec(line[1])),
26442
26457
  tests_failed: int(/(\d+) failed/.exec(line[1]))
26443
26458
  }, /^Test Suites:\s+(.+?)\s*$/m.exec(out));
26444
- }
26459
+ },
26460
+ summaries: (out) => countLines(out, /^Tests:\s+[^\n]*\btotal\b/gm)
26445
26461
  },
26446
26462
  {
26447
26463
  // vitest: `Tests 3 passed (3)` / `Tests 1 failed | 2 passed (3)`
@@ -26458,7 +26474,12 @@ var init_classify2 = __esm({
26458
26474
  if (!passed && !failed)
26459
26475
  return null;
26460
26476
  return withSuiteFailures({ tests_passed: int(passed), tests_failed: int(failed) }, files);
26461
- }
26477
+ },
26478
+ // One invocation prints one `Test Files` line, and a numeric `Tests` line only when a
26479
+ // spec loaded (the every-spec-failed shape above has none). The larger of the two
26480
+ // counts invocations without counting a paired block twice; reading `Tests` alone
26481
+ // missed an all-failed run that followed a passing one (Codex review).
26482
+ summaries: (out) => Math.max(countLines(out, /^\s*Tests\s+[^\n]*\(\d+\)\s*$/gm), countLines(out, /^\s*Test Files\s+[^\n]*\(\d+\)\s*$/gm))
26462
26483
  },
26463
26484
  {
26464
26485
  // pytest summary rule: `===== 297 passed in 0.54s =====`,
@@ -26481,7 +26502,10 @@ var init_classify2 = __esm({
26481
26502
  if (!passed && !failed && !errors)
26482
26503
  return null;
26483
26504
  return { tests_passed: int(passed), tests_failed: int(failed) + int(errors) };
26484
- }
26505
+ },
26506
+ // Only FINAL tallies, which carry pytest's `in 0.54s`. The interim banners the reader
26507
+ // skips above carry no duration, and counting them would mark every red run partial.
26508
+ summaries: (out) => Math.max(1, countLines(out, /^=+ [^\n]*?(?:passed|failed|error|no tests ran)[^\n]*? in [\d.]+s[^\n]*? =+$/gm))
26485
26509
  },
26486
26510
  {
26487
26511
  // Maven surefire's per-module summary (TERM-1122), measured against surefire
@@ -26543,7 +26567,8 @@ var init_classify2 = __esm({
26543
26567
  return null;
26544
26568
  const failed = Number(m[1]);
26545
26569
  return { tests_passed: Number(m[2]) - failed, tests_failed: failed };
26546
- }
26570
+ },
26571
+ summaries: (out) => Math.max(1, countLines(out, /^\d+% tests passed, \d+ tests? failed out of \d+$/gm))
26547
26572
  },
26548
26573
  {
26549
26574
  // mocha: ` 440 passing (1s)` and ` 2 failing`
@@ -26554,7 +26579,8 @@ var init_classify2 = __esm({
26554
26579
  if (!passing && !failing)
26555
26580
  return null;
26556
26581
  return { tests_passed: int(passing), tests_failed: int(failing) };
26557
- }
26582
+ },
26583
+ summaries: (out) => Math.max(countLines(out, /^\s*\d+ passing\b/gm), countLines(out, /^\s*\d+ failing\b/gm))
26558
26584
  },
26559
26585
  {
26560
26586
  // ava: ` 3 tests passed` / ` 1 test failed`
@@ -26565,7 +26591,8 @@ var init_classify2 = __esm({
26565
26591
  if (!passed && !failed)
26566
26592
  return null;
26567
26593
  return { tests_passed: int(passed), tests_failed: int(failed) };
26568
- }
26594
+ },
26595
+ summaries: (out) => Math.max(countLines(out, /^\s*\d+ tests? passed\b/gm), countLines(out, /^\s*\d+ tests? failed\b/gm))
26569
26596
  },
26570
26597
  {
26571
26598
  // uvu: a `Total:` / `Passed:` / `Skipped:` block.
@@ -26578,7 +26605,8 @@ var init_classify2 = __esm({
26578
26605
  const t = int(total);
26579
26606
  const p = int(passed);
26580
26607
  return { tests_passed: p, tests_failed: Math.max(0, t - p) };
26581
- }
26608
+ },
26609
+ summaries: (out) => countLines(out, /^\s*Total:\s+\d+/gm)
26582
26610
  },
26583
26611
  {
26584
26612
  /**
@@ -26613,6 +26641,7 @@ var init_classify2 = __esm({
26613
26641
  * those would double them against their parent.
26614
26642
  */
26615
26643
  runner: "bare TAP",
26644
+ shadowedBy: ["node --test (TAP)", "node-tap"],
26616
26645
  read: (out) => {
26617
26646
  let n = 0;
26618
26647
  let failed = 0;
@@ -30343,6 +30372,10 @@ var init_venueDescriptor = __esm({
30343
30372
  function fmtMs(ms) {
30344
30373
  return ms < 1e3 ? `${String(ms)}ms` : `${(ms / 1e3).toFixed(1)}s`;
30345
30374
  }
30375
+ function fmtMinutes(ms) {
30376
+ const min = ms / 6e4;
30377
+ return Number.isInteger(min) ? String(min) : min.toFixed(1);
30378
+ }
30346
30379
  function renderVerdictLine(r) {
30347
30380
  if (r.status === "refused") {
30348
30381
  const first = r.boundaryRefusals[0];
@@ -30354,9 +30387,9 @@ function renderVerdictLine(r) {
30354
30387
  const t = fmtMs(r.wallMs);
30355
30388
  switch (r.outcome) {
30356
30389
  case "completed":
30357
- return `GREEN ${String(r.counts?.tests_passed ?? 0)} test(s) passed, none failed \u2014 ${t}${r.preview ? ` \u2014 ${r.preview.url}` : ""}`;
30390
+ return `GREEN ${String(r.counts?.tests_passed ?? 0)} test(s) passed, none failed ` + (r.counts?.coverage === "partial" ? `(${PARTIAL_COUNTS_NOTE}) ` : "") + `\u2014 ${t}${r.preview ? ` \u2014 ${r.preview.url}` : ""}`;
30358
30391
  case "tests-failed":
30359
- return `RED ${r.counts ? `${String(r.counts.tests_failed)} test(s) failed` : `exit ${String(r.exitCode)}`} \u2014 ${t}${r.preview ? ` \u2014 ${r.preview.url}` : ""}`;
30392
+ return `RED ${r.counts ? `${String(r.counts.tests_failed)} test(s) failed` : `exit ${String(r.exitCode)}`}` + (r.counts?.coverage === "partial" ? ` (${PARTIAL_COUNTS_NOTE})` : "") + ` \u2014 ${t}${r.preview ? ` \u2014 ${r.preview.url}` : ""}`;
30360
30393
  case "no-tests-observed":
30361
30394
  return `NOTHING RAN the suite reported zero tests passed and zero failed \u2014 ${t}`;
30362
30395
  case "counts-unparsed":
@@ -30366,7 +30399,7 @@ function renderVerdictLine(r) {
30366
30399
  case "environment-exhausted":
30367
30400
  return `OUR FAULT the run ran out of a resource we cap; your work has not been judged \u2014 ${t}`;
30368
30401
  case "budget-exceeded":
30369
- return `OUR LIMIT we stopped the run at our time limit; this is not a result about the work \u2014 ${t}`;
30402
+ return r.testTimeoutMs === void 0 ? `OUR LIMIT we stopped the run at our time limit; this is not a result about the work \u2014 ${t}` : `OUR LIMIT the ${fmtMinutes(r.testTimeoutMs)}-min test time limit set for this posting stopped the run; this is not a result about the work \u2014 ${t}`;
30370
30403
  case null:
30371
30404
  throw new Error("a verified run has no outcome \u2014 the result was assembled wrong");
30372
30405
  }
@@ -30388,7 +30421,7 @@ function renderRunReport(r) {
30388
30421
  if (r.status === "verified" && r.outcome === "counts-unparsed") {
30389
30422
  lines.push("", "Your suite exited 0, but we could not read how many tests ran, so this is not a verdict either way.");
30390
30423
  } else if (r.status === "verified" && r.outcome === "budget-exceeded") {
30391
- lines.push("", "Our time limit ended the run; that says nothing about your work.");
30424
+ lines.push("", r.testTimeoutMs === void 0 ? "Our time limit ended the run; that says nothing about your work." : `The ${fmtMinutes(r.testTimeoutMs)}-min test time limit set for this posting ended the run; that says nothing about your work.`);
30392
30425
  } else if (r.status === "verified" && r.outcome !== null && isOurFault(r.outcome)) {
30393
30426
  lines.push("", "This is an environment failure on our side, not a statement about your work.");
30394
30427
  }
@@ -30410,7 +30443,7 @@ function exitCodeFor(r) {
30410
30443
  return 2;
30411
30444
  return exitCodeForOutcome(r.outcome);
30412
30445
  }
30413
- var RUN_TEST_COMMAND_SOURCES, RUN_IMAGE_SOURCES, RUN_RESULT_SCHEMA, RUN_RESULT_FIELDS, RENDER_NONE, FIELD_VIEWS;
30446
+ var RUN_TEST_COMMAND_SOURCES, RUN_IMAGE_SOURCES, RUN_RESULT_SCHEMA, PARTIAL_COUNTS_NOTE, RUN_RESULT_FIELDS, RENDER_NONE, FIELD_VIEWS;
30414
30447
  var init_result = __esm({
30415
30448
  "../../packages/envrun/dist/result.js"() {
30416
30449
  "use strict";
@@ -30433,6 +30466,7 @@ var init_result = __esm({
30433
30466
  "founder-declared"
30434
30467
  ];
30435
30468
  RUN_RESULT_SCHEMA = "terminalhire.verification-run/1";
30469
+ PARTIAL_COUNTS_NOTE = "from one of several test summaries in the output, so not a total for the whole run";
30436
30470
  RUN_RESULT_FIELDS = [
30437
30471
  "schema",
30438
30472
  "runId",
@@ -30445,6 +30479,9 @@ var init_result = __esm({
30445
30479
  "testOutputTail",
30446
30480
  "counts",
30447
30481
  "wallMs",
30482
+ "installMs",
30483
+ "testMs",
30484
+ "testTimeoutMs",
30448
30485
  "targetRepo",
30449
30486
  "targetSha",
30450
30487
  "patchSha256",
@@ -30475,8 +30512,11 @@ var init_result = __esm({
30475
30512
  testCommand: (r) => r.testCommand === null ? null : `test command ${r.testCommand}`,
30476
30513
  testOutputTail: RENDER_NONE,
30477
30514
  // printed as a block below the fields, when red
30478
- counts: (r) => r.counts === null ? null : `tests ${String(r.counts.tests_passed)} passed, ${String(r.counts.tests_failed)} failed (${r.counts.runner})`,
30515
+ counts: (r) => r.counts === null ? null : `tests ${String(r.counts.tests_passed)} passed, ${String(r.counts.tests_failed)} failed (${r.counts.runner})` + (r.counts.coverage === "partial" ? ` \u2014 ${PARTIAL_COUNTS_NOTE}` : ""),
30479
30516
  wallMs: (r) => `round trip ${fmtMs(r.wallMs)}`,
30517
+ installMs: (r) => r.installMs === void 0 ? null : `install step ${fmtMs(r.installMs)}`,
30518
+ testMs: (r) => r.testMs === void 0 ? null : `test step ${fmtMs(r.testMs)}`,
30519
+ testTimeoutMs: (r) => r.testTimeoutMs === void 0 ? null : `time limit ${fmtMinutes(r.testTimeoutMs)} min`,
30480
30520
  targetRepo: (r) => `target ${r.targetRepo}`,
30481
30521
  targetSha: (r) => `commit ${r.targetSha.slice(0, 12)}`,
30482
30522
  patchSha256: (r) => r.patchSha256 === null ? null : `patch ${r.patchSha256.slice(0, 12)}`,
@@ -30571,7 +30611,11 @@ function toTestRunResult(result, outputSha256) {
30571
30611
  return {
30572
30612
  exit_code: result.exitCode ?? -1,
30573
30613
  ...outputSha256 === void 0 ? {} : { output_sha256: outputSha256 },
30574
- ...result.counts === null ? {} : { tests_passed: result.counts.tests_passed, tests_failed: result.counts.tests_failed }
30614
+ ...result.counts === null ? {} : {
30615
+ tests_passed: result.counts.tests_passed,
30616
+ tests_failed: result.counts.tests_failed,
30617
+ ...result.counts.coverage === "partial" ? { tests_coverage: "partial" } : {}
30618
+ }
30575
30619
  };
30576
30620
  }
30577
30621
  function toAcceptancePredicate(pair, opts = {}) {
@@ -30726,7 +30770,8 @@ var init_attestation2 = __esm({
30726
30770
  "test-command-unavailable": null,
30727
30771
  // null, with the same reasoning as `test-command-unavailable` and NOT
30728
30772
  // `budget-exceeded` (TERM-644). A signed `budget-exceeded` records one fact:
30729
- // the run hit OUR time budget. It blames no one (TERM-1289; `LIMIT_OWNER` in
30773
+ // the run hit the time budget we applied — our default, or the one set for the
30774
+ // posting (TERM-1308), which is still ours to enforce. It blames no one (TERM-1289; `LIMIT_OWNER` in
30730
30775
  // `classify.ts` owns that question), and it is not an acceptable run:
30731
30776
  // `acceptRun` (`packages/attest/src/verify.ts`) refuses every `budget_outcome`
30732
30777
  // except `completed`. Exhausting the tmpfs we sized, or an OOM kill from memory we
@@ -31425,7 +31470,7 @@ var init_gcpPlacement = __esm({
31425
31470
  this.name = "GcpPlacementError";
31426
31471
  }
31427
31472
  };
31428
- GCP_MAX_RUN_DURATION_SECONDS = 3600;
31473
+ GCP_MAX_RUN_DURATION_SECONDS = 10800;
31429
31474
  GCP_MANAGED_LABEL_KEY = "th-managed";
31430
31475
  GCP_RUN_LABEL_KEY = "th-run";
31431
31476
  CONFIDENTIAL_SPACE_MACHINE_TYPE = "n2d-standard-2";
@@ -33321,9 +33366,9 @@ var init_placement = __esm({
33321
33366
  });
33322
33367
 
33323
33368
  // ../../packages/envspec/dist/yaml.js
33324
- function parseYaml(source) {
33325
- const lines = splitLines(source);
33326
- const cursor = { lines, index: 0 };
33369
+ function parseYaml(source, limits = {}) {
33370
+ const lines = splitLines(source, limits.maxLineLength ?? Infinity);
33371
+ const cursor = { lines, index: 0, depth: 0, maxDepth: limits.maxDepth ?? Infinity };
33327
33372
  skipIgnorable(cursor);
33328
33373
  if (cursor.index >= lines.length)
33329
33374
  return null;
@@ -33335,13 +33380,16 @@ function parseYaml(source) {
33335
33380
  }
33336
33381
  return value;
33337
33382
  }
33338
- function splitLines(source) {
33383
+ function splitLines(source, maxLineLength) {
33339
33384
  const raw = source.replace(/\r\n/g, "\n").split("\n");
33340
33385
  const out = [];
33341
33386
  let sawDocumentStart = false;
33342
33387
  for (let i = 0; i < raw.length; i += 1) {
33343
33388
  const line = raw[i];
33344
33389
  const number = i + 1;
33390
+ if (line.length > maxLineLength) {
33391
+ throw new YamlUnsupportedError("line-length", number, `line longer than ${maxLineLength} characters`);
33392
+ }
33345
33393
  const withoutIndent = line.replace(/^[ ]+/, "");
33346
33394
  const indent = line.length - withoutIndent.length;
33347
33395
  if (/^[ ]*\t/.test(line)) {
@@ -33378,6 +33426,18 @@ function peek(cursor) {
33378
33426
  return cursor.index < cursor.lines.length ? cursor.lines[cursor.index] : null;
33379
33427
  }
33380
33428
  function parseNode(cursor, indent) {
33429
+ if (cursor.depth >= cursor.maxDepth) {
33430
+ const at = peek(cursor);
33431
+ throw new YamlUnsupportedError("depth", at?.number ?? 0, `nesting deeper than ${cursor.maxDepth}`);
33432
+ }
33433
+ cursor.depth += 1;
33434
+ try {
33435
+ return parseNodeAt(cursor, indent);
33436
+ } finally {
33437
+ cursor.depth -= 1;
33438
+ }
33439
+ }
33440
+ function parseNodeAt(cursor, indent) {
33381
33441
  const line = peek(cursor);
33382
33442
  if (line === null)
33383
33443
  return null;
@@ -33797,6 +33857,178 @@ var init_database = __esm({
33797
33857
  }
33798
33858
  });
33799
33859
 
33860
+ // ../../packages/envspec/dist/semverRange.js
33861
+ function rangeAdmits(version, range) {
33862
+ const locked = parseVersion(version);
33863
+ if (locked === null || range.length > MAX_RANGE_LENGTH)
33864
+ return null;
33865
+ let unknown = false;
33866
+ for (const alternative of range.split("||")) {
33867
+ const interval = parseSet(alternative);
33868
+ if (interval === null) {
33869
+ unknown = true;
33870
+ continue;
33871
+ }
33872
+ if (within(locked, interval))
33873
+ return true;
33874
+ }
33875
+ return unknown ? null : false;
33876
+ }
33877
+ function parseVersion(text) {
33878
+ const m = /^v?(\d+)\.(\d+)\.(\d+)(?:\+[0-9A-Za-z.-]+)?$/.exec(text.trim());
33879
+ if (m === null || !NUMBER.test(m[1]) || !NUMBER.test(m[2]) || !NUMBER.test(m[3]))
33880
+ return null;
33881
+ return [Number(m[1]), Number(m[2]), Number(m[3])];
33882
+ }
33883
+ function parseSet(text) {
33884
+ const set = text.trim();
33885
+ if (set === "")
33886
+ return { low: null, high: null };
33887
+ const hyphen = /^(\S+)\s+-\s+(\S+)$/.exec(set);
33888
+ if (hyphen !== null) {
33889
+ const from = parsePartial(hyphen[1]);
33890
+ const to = parsePartial(hyphen[2]);
33891
+ if (from === null || to === null)
33892
+ return null;
33893
+ return intersect(atLeast2(from), atMost(to));
33894
+ }
33895
+ const comparators = set.replace(/(<=|>=|<|>|=|~>|~|\^)\s+/g, "$1").split(/\s+/);
33896
+ let interval = { low: null, high: null };
33897
+ for (const comparator of comparators) {
33898
+ const next = parseComparator(comparator);
33899
+ if (next === null)
33900
+ return null;
33901
+ interval = intersect(interval, next);
33902
+ }
33903
+ return interval;
33904
+ }
33905
+ function parsePartial(text) {
33906
+ const m = /^v?([^.]+)(?:\.([^.]+))?(?:\.([^.+]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(text);
33907
+ if (m === null)
33908
+ return null;
33909
+ const parts = [];
33910
+ let wild = false;
33911
+ for (const part of [m[1], m[2], m[3]]) {
33912
+ if (part === void 0 || /^[xX*]$/.test(part)) {
33913
+ wild = true;
33914
+ parts.push(null);
33915
+ continue;
33916
+ }
33917
+ if (wild || !NUMBER.test(part))
33918
+ return null;
33919
+ parts.push(Number(part));
33920
+ }
33921
+ return { major: parts[0] ?? null, minor: parts[1] ?? null, patch: parts[2] ?? null };
33922
+ }
33923
+ function parseComparator(text) {
33924
+ const m = /^(<=|>=|<|>|=|~>|~|\^)?(.*)$/.exec(text);
33925
+ if (m === null)
33926
+ return null;
33927
+ const operator = m[1] ?? "";
33928
+ const partial = parsePartial(m[2]);
33929
+ if (partial === null)
33930
+ return null;
33931
+ const { major, minor, patch } = partial;
33932
+ if (major === null) {
33933
+ return operator === "" || operator === "=" || operator === ">=" ? { low: null, high: null } : null;
33934
+ }
33935
+ switch (operator) {
33936
+ case "":
33937
+ case "=":
33938
+ return intersect(atLeast2(partial), atMost(partial));
33939
+ case ">=":
33940
+ return atLeast2(partial);
33941
+ case "<=":
33942
+ return atMost(partial);
33943
+ case ">":
33944
+ return { low: { at: bumpPartial(partial), inclusive: true }, high: null };
33945
+ case "<":
33946
+ return { low: null, high: { at: floor(partial), inclusive: false } };
33947
+ case "~":
33948
+ case "~>":
33949
+ return {
33950
+ low: { at: floor(partial), inclusive: true },
33951
+ high: { at: minor === null ? [major + 1, 0, 0] : [major, minor + 1, 0], inclusive: false }
33952
+ };
33953
+ case "^": {
33954
+ let high;
33955
+ if (major > 0 || minor === null)
33956
+ high = [major + 1, 0, 0];
33957
+ else if (minor > 0 || patch === null)
33958
+ high = [0, minor + 1, 0];
33959
+ else
33960
+ high = [0, 0, patch + 1];
33961
+ return { low: { at: floor(partial), inclusive: true }, high: { at: high, inclusive: false } };
33962
+ }
33963
+ default:
33964
+ return null;
33965
+ }
33966
+ }
33967
+ function floor(p) {
33968
+ return [p.major ?? 0, p.minor ?? 0, p.patch ?? 0];
33969
+ }
33970
+ function bumpPartial(p) {
33971
+ const major = p.major ?? 0;
33972
+ if (p.minor === null)
33973
+ return [major + 1, 0, 0];
33974
+ if (p.patch === null)
33975
+ return [major, p.minor + 1, 0];
33976
+ return [major, p.minor, p.patch + 1];
33977
+ }
33978
+ function atLeast2(p) {
33979
+ return p.major === null ? { low: null, high: null } : { low: { at: floor(p), inclusive: true }, high: null };
33980
+ }
33981
+ function atMost(p) {
33982
+ if (p.major === null)
33983
+ return { low: null, high: null };
33984
+ if (p.patch !== null && p.minor !== null) {
33985
+ return { low: null, high: { at: [p.major, p.minor, p.patch], inclusive: true } };
33986
+ }
33987
+ return { low: null, high: { at: bumpPartial(p), inclusive: false } };
33988
+ }
33989
+ function compare(a, b) {
33990
+ return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
33991
+ }
33992
+ function intersect(a, b) {
33993
+ const low = a.low === null ? b.low : b.low === null ? a.low : tighterLow(a.low, b.low);
33994
+ const high = a.high === null ? b.high : b.high === null ? a.high : tighterHigh(a.high, b.high);
33995
+ return { low, high };
33996
+ }
33997
+ function tighterLow(a, b) {
33998
+ const c = compare(a.at, b.at);
33999
+ if (c !== 0)
34000
+ return c > 0 ? a : b;
34001
+ return a.inclusive ? b : a;
34002
+ }
34003
+ function tighterHigh(a, b) {
34004
+ const c = compare(a.at, b.at);
34005
+ if (c !== 0)
34006
+ return c < 0 ? a : b;
34007
+ return a.inclusive ? b : a;
34008
+ }
34009
+ function within(v, interval) {
34010
+ const { low, high } = interval;
34011
+ if (low !== null) {
34012
+ const c = compare(v, low.at);
34013
+ if (c < 0 || c === 0 && !low.inclusive)
34014
+ return false;
34015
+ }
34016
+ if (high !== null) {
34017
+ const c = compare(v, high.at);
34018
+ if (c > 0 || c === 0 && !high.inclusive)
34019
+ return false;
34020
+ }
34021
+ return true;
34022
+ }
34023
+ var MAX_RANGE_LENGTH, NUMBER;
34024
+ var init_semverRange = __esm({
34025
+ "../../packages/envspec/dist/semverRange.js"() {
34026
+ "use strict";
34027
+ MAX_RANGE_LENGTH = 1024;
34028
+ NUMBER = /^(?:0|[1-9]\d{0,15})$/;
34029
+ }
34030
+ });
34031
+
33800
34032
  // ../../packages/envspec/dist/manifest.js
33801
34033
  function detectRuntime(repo) {
33802
34034
  const candidates = [];
@@ -33878,47 +34110,155 @@ function lockfileGaps(repo) {
33878
34110
  const pkg = readJsonObject(repo, "package.json");
33879
34111
  if (pkg === null)
33880
34112
  return null;
34113
+ const lockfile = ["package-lock.json", "npm-shrinkwrap.json", "yarn.lock", "pnpm-lock.yaml"].find((name) => repo.exists(name));
34114
+ if (lockfile === void 0)
34115
+ return null;
34116
+ const listingFor = lockfileListings(repo, pkg, lockfile);
34117
+ if (listingFor === null)
34118
+ return null;
34119
+ const missing = [];
34120
+ const rootListing = listingFor("");
34121
+ if (rootListing !== null) {
34122
+ for (const [n, r, sec] of wantedDependencies(pkg))
34123
+ if (!rootListing(n, r, sec))
34124
+ missing.push(`${n}@${r}`);
34125
+ }
34126
+ for (const dir of workspaceDirs(repo, pkg, lockfile)) {
34127
+ const listing = listingFor(dir);
34128
+ const manifest = readJsonObject(repo, `${dir}/package.json`);
34129
+ if (listing === null || manifest === null)
34130
+ continue;
34131
+ for (const [n, r, sec] of wantedDependencies(manifest)) {
34132
+ if (!listing(n, r, sec))
34133
+ missing.push(`${n}@${r} (${dir})`);
34134
+ }
34135
+ }
34136
+ return missing.length === 0 ? null : { lockfile, missing };
34137
+ }
34138
+ function wantedDependencies(pkg) {
33881
34139
  const wanted = [];
34140
+ const optional = readObjectField(pkg, "optionalDependencies");
33882
34141
  for (const section of DEPENDENCY_SECTIONS) {
33883
34142
  const deps = readObjectField(pkg, section);
33884
34143
  if (deps === null)
33885
34144
  continue;
33886
34145
  for (const [name, range] of Object.entries(deps)) {
34146
+ if (section === "dependencies" && readStringField(optional, name) !== null)
34147
+ continue;
33887
34148
  if (typeof range === "string")
33888
- wanted.push([name, range]);
34149
+ wanted.push([name, range, section]);
33889
34150
  }
33890
34151
  }
33891
- if (wanted.length === 0)
33892
- return null;
33893
- const check = (lockfile, listed) => {
33894
- if (listed === null)
33895
- return null;
33896
- const missing = wanted.filter(([n, r]) => !listed(n, r)).map(([n, r]) => `${n}@${r}`);
33897
- return missing.length === 0 ? null : { lockfile, missing };
33898
- };
33899
- for (const lockfile of ["package-lock.json", "npm-shrinkwrap.json"]) {
33900
- if (repo.exists(lockfile))
33901
- return check(lockfile, npmLockListing(repo, lockfile));
34152
+ return wanted;
34153
+ }
34154
+ function lockfileListings(repo, pkg, lockfile) {
34155
+ if (lockfile === "yarn.lock") {
34156
+ const root = yarnLockListing(repo.readText(lockfile));
34157
+ return root === null ? null : (dir) => dir === "" ? root : null;
33902
34158
  }
33903
- if (repo.exists("yarn.lock"))
33904
- return check("yarn.lock", yarnLockListing(repo.readText("yarn.lock")));
33905
- if (repo.exists("pnpm-lock.yaml")) {
33906
- return check("pnpm-lock.yaml", pnpmLockListing(repo.readText("pnpm-lock.yaml")));
34159
+ if (lockfile === "pnpm-lock.yaml") {
34160
+ const text = repo.readText(lockfile);
34161
+ const importers = pnpmImporters(text);
34162
+ const settings = pnpmSettings(text);
34163
+ const overrides = pnpmOverrides(text);
34164
+ if (importers === null || settings === null || overrides === null)
34165
+ return null;
34166
+ const honoured = sameOverrides(overrides, declaredPnpmOverrides(repo, pkg)) ? overrides : /* @__PURE__ */ new Map();
34167
+ const withOverrides = (listing) => (name, range, section) => {
34168
+ const override = honoured.get(name);
34169
+ return listing(name, range, section) || override !== void 0 && listing(name, override, section);
34170
+ };
34171
+ const withSettings = (listing) => (name, range, section) => !settings.autoInstallPeers && section === "peerDependencies" || settings.excludeLinksFromLockfile && range.startsWith("link:") || listing(name, range, section);
34172
+ const workspaceAware = text !== null && /^importers:/m.test(text);
34173
+ return (dir) => {
34174
+ const importer = importers.get(dir === "" ? "." : dir);
34175
+ if (importer !== void 0)
34176
+ return withSettings(withOverrides(pnpmImporterListing(importer)));
34177
+ return dir === "" || !workspaceAware ? null : () => false;
34178
+ };
33907
34179
  }
33908
- return null;
34180
+ return npmLockListings(repo, lockfile);
33909
34181
  }
33910
- function npmLockListing(repo, lockfile) {
34182
+ function npmLockListings(repo, lockfile) {
33911
34183
  const lock = readJsonObject(repo, lockfile);
33912
34184
  if (lock === null)
33913
34185
  return null;
33914
- const root = readObjectField(readObjectField(lock, "packages"), "");
33915
- if (root !== null) {
33916
- return (name, range) => DEPENDENCY_SECTIONS.some((s) => readStringField(readObjectField(root, s), name) === range);
34186
+ const packages = readObjectField(lock, "packages");
34187
+ if (readObjectField(packages, "") !== null) {
34188
+ return (dir) => {
34189
+ const entry = readObjectField(packages, dir);
34190
+ if (entry === null)
34191
+ return () => false;
34192
+ return (name, range) => DEPENDENCY_SECTIONS.some((s) => readStringField(readObjectField(entry, s), name) === range);
34193
+ };
33917
34194
  }
33918
34195
  const v1 = readObjectField(lock, "dependencies");
33919
34196
  if (v1 === null)
33920
34197
  return null;
33921
- return (name) => readObjectField(v1, name) !== null;
34198
+ const root = (name, range) => {
34199
+ const entry = readObjectField(v1, name);
34200
+ if (entry === null)
34201
+ return false;
34202
+ const version = readStringField(entry, "version");
34203
+ return version === null ? true : rangeAdmits(version, range) ?? true;
34204
+ };
34205
+ return (dir) => dir === "" ? root : null;
34206
+ }
34207
+ function workspaceDirs(repo, pkg, lockfile) {
34208
+ let patterns;
34209
+ if (lockfile === "pnpm-lock.yaml") {
34210
+ const text = repo.readText("pnpm-workspace.yaml");
34211
+ try {
34212
+ patterns = text === null ? [] : asStringList(mapGet(parseYaml(text, PNPM_LIMITS), "packages"));
34213
+ } catch (err) {
34214
+ if (!(err instanceof YamlUnsupportedError))
34215
+ throw err;
34216
+ patterns = [];
34217
+ }
34218
+ } else {
34219
+ const field = pkg["workspaces"];
34220
+ patterns = Array.isArray(field) ? field : readObjectField(pkg, "workspaces")?.["packages"];
34221
+ }
34222
+ if (!Array.isArray(patterns))
34223
+ return [];
34224
+ const budget = { visits: 0 };
34225
+ const included = /* @__PURE__ */ new Set();
34226
+ const excluded = /* @__PURE__ */ new Set();
34227
+ for (const raw of patterns) {
34228
+ if (typeof raw !== "string")
34229
+ continue;
34230
+ const negated = raw.startsWith("!");
34231
+ const pattern = (negated ? raw.slice(1) : raw).replace(/^\.\//, "").replace(/\/+$/, "");
34232
+ const segments = pattern.split("/");
34233
+ if (pattern === "" || /[{}[\]\\]/.test(pattern) || segments.some((s) => s === ".." || s === "." || s === "")) {
34234
+ continue;
34235
+ }
34236
+ for (const dir of matchSegments(repo, "", segments, 0, budget))
34237
+ (negated ? excluded : included).add(dir);
34238
+ }
34239
+ const manifestOf = (dir) => `${dir}/package.json`;
34240
+ return [...included].filter((dir) => !excluded.has(dir) && dir !== "" && readJsonObject(repo, manifestOf(dir)) !== null).sort().slice(0, WORKSPACE_LIMITS.maxWorkspaces);
34241
+ }
34242
+ function matchSegments(repo, base, segments, depth, budget) {
34243
+ if (segments.length === 0)
34244
+ return [base];
34245
+ if (depth > WORKSPACE_LIMITS.maxDepth || budget.visits >= WORKSPACE_LIMITS.maxVisits)
34246
+ return [];
34247
+ budget.visits += 1;
34248
+ const [head, ...rest] = segments;
34249
+ const at = (name) => base === "" ? name : `${base}/${name}`;
34250
+ const children = () => repo.listNames(base).filter((n) => n !== "node_modules" && !n.startsWith(".") && repo.isDirectory(at(n)));
34251
+ if (head === "**") {
34252
+ return [
34253
+ ...matchSegments(repo, base, rest, depth + 1, budget),
34254
+ ...children().flatMap((n) => matchSegments(repo, at(n), segments, depth + 1, budget))
34255
+ ];
34256
+ }
34257
+ if (!/[*?]/.test(head)) {
34258
+ return head !== "node_modules" && repo.isDirectory(at(head)) ? matchSegments(repo, at(head), rest, depth + 1, budget) : [];
34259
+ }
34260
+ const re = new RegExp(`^${head.replace(/[.+^$()|]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]")}$`);
34261
+ return children().filter((n) => re.test(n)).flatMap((n) => matchSegments(repo, at(n), rest, depth + 1, budget));
33922
34262
  }
33923
34263
  function yarnLockListing(text) {
33924
34264
  if (text === null || !/^(# yarn lockfile v1|__metadata:)/m.test(text))
@@ -33937,40 +34277,120 @@ function yarnLockListing(text) {
33937
34277
  }
33938
34278
  return (name, range) => specs.has(`${name}@${range}`);
33939
34279
  }
33940
- function pnpmLockListing(text) {
33941
- if (text === null || !/^lockfileVersion:/m.test(text))
34280
+ function pnpmImporterListing(importer) {
34281
+ return (name, range) => asString(mapGet(mapGet(importer, "specifiers"), name)) === range || PNPM_SECTIONS.some((section) => asString(mapGet(mapGet(mapGet(importer, section), name), "specifier")) === range);
34282
+ }
34283
+ function pnpmSettings(text) {
34284
+ if (text === null)
33942
34285
  return null;
33943
- const lines = pnpmRootLines(text.split(/\r?\n/));
33944
- if (lines === null)
34286
+ const block = topLevelBlock(text.replace(/\r\n/g, "\n").split("\n"), "settings");
34287
+ let node = null;
34288
+ try {
34289
+ node = block === null ? null : mapGet(parseYaml(block, PNPM_LIMITS), "settings");
34290
+ } catch (err) {
34291
+ if (err instanceof YamlUnsupportedError)
34292
+ return null;
34293
+ throw err;
34294
+ }
34295
+ return {
34296
+ autoInstallPeers: asString(mapGet(node, "autoInstallPeers")) !== "false",
34297
+ excludeLinksFromLockfile: asString(mapGet(node, "excludeLinksFromLockfile")) === "true"
34298
+ };
34299
+ }
34300
+ function pnpmOverrides(text) {
34301
+ if (text === null)
33945
34302
  return null;
33946
- const unquote = (s) => s.replace(/^['"]|['"]$/g, "");
33947
- return (name, range) => lines.some((line, i) => {
33948
- if (unquote(line) === `${name}:` || line === `'${name}':` || line === `"${name}":`) {
33949
- const next = lines[i + 1] ?? "";
33950
- return next.startsWith("specifier:") && unquote(next.slice(10).trim()) === range;
33951
- }
33952
- const flat = /^(['"]?)(.+)\1:\s*(.+)$/.exec(line);
33953
- return flat !== null && flat[2] === name && unquote(flat[3] ?? "") === range;
33954
- });
34303
+ const block = topLevelBlock(text.replace(/\r\n/g, "\n").split("\n"), "overrides");
34304
+ if (block === null)
34305
+ return /* @__PURE__ */ new Map();
34306
+ let node;
34307
+ try {
34308
+ node = mapGet(parseYaml(block, PNPM_LIMITS), "overrides");
34309
+ } catch (err) {
34310
+ if (err instanceof YamlUnsupportedError)
34311
+ return null;
34312
+ throw err;
34313
+ }
34314
+ if (node === null)
34315
+ return /* @__PURE__ */ new Map();
34316
+ if (!isMap(node))
34317
+ return null;
34318
+ const overrides = /* @__PURE__ */ new Map();
34319
+ for (const { key, value } of node.entries) {
34320
+ const spec = asString(value);
34321
+ if (spec === null)
34322
+ return null;
34323
+ overrides.set(key, spec);
34324
+ }
34325
+ return overrides;
33955
34326
  }
33956
- function pnpmRootLines(raw) {
33957
- const importers = raw.indexOf("importers:");
33958
- if (importers === -1) {
33959
- const end = raw.indexOf("packages:");
33960
- return (end === -1 ? raw : raw.slice(0, end)).map((l) => l.trim());
34327
+ function declaredPnpmOverrides(repo, pkg) {
34328
+ const sources = [
34329
+ readObjectField(readObjectField(pkg, "pnpm"), "overrides"),
34330
+ readObjectField(pkg, "resolutions")
34331
+ ];
34332
+ const workspace = repo.readText("pnpm-workspace.yaml");
34333
+ if (workspace !== null) {
34334
+ let node;
34335
+ try {
34336
+ node = mapGet(parseYaml(workspace, PNPM_LIMITS), "overrides");
34337
+ } catch (err) {
34338
+ if (err instanceof YamlUnsupportedError)
34339
+ return null;
34340
+ throw err;
34341
+ }
34342
+ if (node !== null && !isMap(node))
34343
+ return null;
34344
+ if (isMap(node))
34345
+ sources.push(Object.fromEntries(node.entries.map((e) => [e.key, e.value])));
33961
34346
  }
33962
- const start = raw.findIndex((l, i) => i > importers && /^ {2}(['"]?)\.\1:\s*$/.test(l));
33963
- if (start === -1)
34347
+ const declared = /* @__PURE__ */ new Map();
34348
+ for (const source of sources) {
34349
+ for (const [key, value] of Object.entries(source ?? {})) {
34350
+ if (typeof value !== "string")
34351
+ return null;
34352
+ const seen = declared.get(key);
34353
+ if (seen !== void 0 && seen !== value)
34354
+ return null;
34355
+ declared.set(key, value);
34356
+ }
34357
+ }
34358
+ return declared;
34359
+ }
34360
+ function sameOverrides(a, b) {
34361
+ return b !== null && a.size === b.size && [...a].every(([key, value]) => b.get(key) === value);
34362
+ }
34363
+ function pnpmImporters(text) {
34364
+ if (text === null || !/^lockfileVersion:/m.test(text))
33964
34365
  return null;
33965
- const out = [];
33966
- for (const line of raw.slice(start + 1)) {
33967
- if (line.trim() === "")
33968
- continue;
33969
- if (!line.startsWith(" "))
33970
- break;
33971
- out.push(line.trim());
34366
+ const lines = text.replace(/\r\n/g, "\n").split("\n");
34367
+ try {
34368
+ const importers = topLevelBlock(lines, "importers");
34369
+ if (importers !== null) {
34370
+ const node = mapGet(parseYaml(importers, PNPM_LIMITS), "importers");
34371
+ return isMap(node) ? new Map(node.entries.map((e) => [e.key, e.value])) : null;
34372
+ }
34373
+ const entries = [];
34374
+ for (const key of [...PNPM_SECTIONS, "specifiers"]) {
34375
+ const block = topLevelBlock(lines, key);
34376
+ if (block !== null)
34377
+ entries.push({ key, value: mapGet(parseYaml(block, PNPM_LIMITS), key) });
34378
+ }
34379
+ return /* @__PURE__ */ new Map([[".", { kind: "map", entries }]]);
34380
+ } catch (err) {
34381
+ if (err instanceof YamlUnsupportedError)
34382
+ return null;
34383
+ throw err;
33972
34384
  }
33973
- return out;
34385
+ }
34386
+ function topLevelBlock(lines, key) {
34387
+ const start = lines.findIndex((l) => l === `${key}:` || l.startsWith(`${key}: `));
34388
+ if (start === -1)
34389
+ return null;
34390
+ let end = start + 1;
34391
+ while (end < lines.length && (lines[end] === "" || /^[ \t#]/.test(lines[end])))
34392
+ end += 1;
34393
+ return lines.slice(start, end).join("\n");
33974
34394
  }
33975
34395
  function unfrozenInstall(command) {
33976
34396
  const lead = /^(?:(?:sudo|time)\s+|env(?:\s+[A-Za-z_]\w*=\S*)*\s+)*/.exec(command)?.[0] ?? "";
@@ -34259,10 +34679,12 @@ function matchFirst(text, pattern) {
34259
34679
  const match2 = pattern.exec(text);
34260
34680
  return match2 === null ? null : match2[1];
34261
34681
  }
34262
- var RUNTIME_MANIFESTS, MAKEFILE_NAMES, C_FAMILY_SOURCE, MANIFEST_FILENAMES, JVM_BUILD_FILES, NPM_PLACEHOLDER_TEST, DEPENDENCY_SECTIONS, YARN_MUTABLE, REQUIREMENT_SPECIFIER, CPP_BUILD_FILES, CMAKE_DECLARES_TESTS, EXACT_VERSION;
34682
+ var RUNTIME_MANIFESTS, MAKEFILE_NAMES, C_FAMILY_SOURCE, MANIFEST_FILENAMES, JVM_BUILD_FILES, NPM_PLACEHOLDER_TEST, DEPENDENCY_SECTIONS, WORKSPACE_LIMITS, PNPM_LIMITS, PNPM_SECTIONS, YARN_MUTABLE, REQUIREMENT_SPECIFIER, CPP_BUILD_FILES, CMAKE_DECLARES_TESTS, EXACT_VERSION;
34263
34683
  var init_manifest2 = __esm({
34264
34684
  "../../packages/envspec/dist/manifest.js"() {
34265
34685
  "use strict";
34686
+ init_semverRange();
34687
+ init_yaml();
34266
34688
  RUNTIME_MANIFESTS = [
34267
34689
  { runtime: "node", files: ["package.json"] },
34268
34690
  { runtime: "python", files: ["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt"] },
@@ -34286,6 +34708,9 @@ var init_manifest2 = __esm({
34286
34708
  "optionalDependencies",
34287
34709
  "peerDependencies"
34288
34710
  ];
34711
+ WORKSPACE_LIMITS = { maxDepth: 8, maxVisits: 2e3, maxWorkspaces: 200 };
34712
+ PNPM_LIMITS = { maxDepth: 8, maxLineLength: 4096 };
34713
+ PNPM_SECTIONS = ["dependencies", "devDependencies", "optionalDependencies"];
34289
34714
  YARN_MUTABLE = "YARN_ENABLE_IMMUTABLE_INSTALLS=false";
34290
34715
  REQUIREMENT_SPECIFIER = /^[A-Za-z0-9._-]+(\[[A-Za-z0-9._,-]+\])?([<>=!~]=?[A-Za-z0-9._*+-]+(,[<>=!~]=?[A-Za-z0-9._*+-]+)*)?$/;
34291
34716
  CPP_BUILD_FILES = [
@@ -34366,21 +34791,50 @@ var init_references = __esm({
34366
34791
  });
34367
34792
 
34368
34793
  // ../../packages/envspec/dist/repo.js
34369
- import { readdirSync as readdirSync4, readFileSync as readFileSync13, statSync as statSync5 } from "fs";
34794
+ import { readdirSync as readdirSync4, readFileSync as readFileSync13, realpathSync as realpathSync2, statSync as statSync5 } from "fs";
34370
34795
  import { join as join26, relative as relative2, sep as sep7 } from "path";
34371
34796
  function createRepoReader(repoPath) {
34372
34797
  const resolveIn = (relativePath) => relativePath === "" ? repoPath : join26(repoPath, relativePath);
34373
34798
  const toPosix = (absolute) => relative2(repoPath, absolute).split(sep7).join("/");
34799
+ let realRoot;
34800
+ const rootPrefix = () => {
34801
+ if (realRoot === void 0) {
34802
+ try {
34803
+ const real = realpathSync2(repoPath);
34804
+ realRoot = real.endsWith(sep7) ? real : real + sep7;
34805
+ } catch {
34806
+ realRoot = null;
34807
+ }
34808
+ }
34809
+ return realRoot;
34810
+ };
34811
+ const containedPath = (relativePath) => {
34812
+ const prefix = rootPrefix();
34813
+ if (prefix === null)
34814
+ return null;
34815
+ try {
34816
+ const real = realpathSync2(resolveIn(relativePath));
34817
+ return real + sep7 === prefix || real.startsWith(prefix) ? real : null;
34818
+ } catch {
34819
+ return null;
34820
+ }
34821
+ };
34374
34822
  const readText = (relativePath) => {
34823
+ const real = containedPath(relativePath);
34824
+ if (real === null)
34825
+ return null;
34375
34826
  try {
34376
- return readFileSync13(resolveIn(relativePath), "utf8");
34827
+ return readFileSync13(real, "utf8");
34377
34828
  } catch {
34378
34829
  return null;
34379
34830
  }
34380
34831
  };
34381
34832
  const statOf = (relativePath) => {
34833
+ const real = containedPath(relativePath);
34834
+ if (real === null)
34835
+ return null;
34382
34836
  try {
34383
- return statSync5(resolveIn(relativePath));
34837
+ return statSync5(real);
34384
34838
  } catch {
34385
34839
  return null;
34386
34840
  }
@@ -34426,11 +34880,14 @@ function createRepoReader(repoPath) {
34426
34880
  const st = statOf(relativeDir);
34427
34881
  if (st === null || !st.isDirectory())
34428
34882
  return [];
34883
+ let names;
34429
34884
  try {
34430
- return readdirSync4(resolveIn(relativeDir)).slice().sort();
34885
+ names = readdirSync4(resolveIn(relativeDir));
34431
34886
  } catch {
34432
34887
  return [];
34433
34888
  }
34889
+ const at = (name) => relativeDir === "" ? name : `${relativeDir}/${name}`;
34890
+ return names.filter((name) => containedPath(at(name)) !== null).sort();
34434
34891
  }
34435
34892
  };
34436
34893
  }
@@ -36467,6 +36924,12 @@ async function runVerification(req, ctx) {
36467
36924
  testOutputTail: outputTail,
36468
36925
  counts: verdict.counts,
36469
36926
  wallMs: Date.now() - startedAt,
36927
+ // TERM-1308: the steps' own durations, which `runEnvironmentSpec` measures, and the
36928
+ // limit applied to the test step. Absent rather than 0 or a default when there is
36929
+ // nothing to report, so a baseline never sizes a budget from a step that did not run.
36930
+ ...verdict.install === null ? {} : { installMs: verdict.install.wallMs },
36931
+ ...verdict.test === null ? {} : { testMs: verdict.test.wallMs },
36932
+ ...req.testTimeoutMs === void 0 ? {} : { testTimeoutMs: req.testTimeoutMs },
36470
36933
  targetRepo: publishableTarget(req.targetRepo),
36471
36934
  targetSha: req.targetSha,
36472
36935
  patchSha256,
@@ -37604,6 +38067,7 @@ __export(dist_exports, {
37604
38067
  LabelWatch: () => LabelWatch,
37605
38068
  MIN_GIT_VERSION_FOR_END_OF_OPTIONS: () => MIN_GIT_VERSION_FOR_END_OF_OPTIONS,
37606
38069
  OUTCOME_TO_BUDGET: () => OUTCOME_TO_BUDGET,
38070
+ PARTIAL_COUNTS_NOTE: () => PARTIAL_COUNTS_NOTE,
37607
38071
  PATH_REFUSAL_CODES: () => PATH_REFUSAL_CODES,
37608
38072
  PLACEMENTS: () => PLACEMENTS,
37609
38073
  PLACEMENT_ALIASES: () => PLACEMENT_ALIASES,
@@ -38045,7 +38509,7 @@ import {
38045
38509
  renameSync as renameSync6,
38046
38510
  existsSync as existsSync14,
38047
38511
  lstatSync as lstatSync5,
38048
- realpathSync as realpathSync2,
38512
+ realpathSync as realpathSync3,
38049
38513
  rmSync as rmSync10,
38050
38514
  readdirSync as readdirSync5,
38051
38515
  statSync as statSync6,
@@ -38810,7 +39274,7 @@ function canonicalPath2(p) {
38810
39274
  if (typeof p !== "string" || p === "") return p;
38811
39275
  const resolved = pathResolve(p);
38812
39276
  try {
38813
- return realpathSync2.native(resolved);
39277
+ return realpathSync3.native(resolved);
38814
39278
  } catch {
38815
39279
  return resolved;
38816
39280
  }
@@ -38891,6 +39355,66 @@ async function fetchFreshClaimablePool() {
38891
39355
  const contributions = (index?.contribute ?? []).filter((j) => j.source === "contribute");
38892
39356
  return [...bounties, ...contributions];
38893
39357
  }
39358
+ var BASELINE_STATUSES = /* @__PURE__ */ new Set([
39359
+ "pending",
39360
+ "not-scheduled",
39361
+ "passed",
39362
+ "failed",
39363
+ "unreadable",
39364
+ "no-tests",
39365
+ "over-ceiling",
39366
+ "not-runnable",
39367
+ "not-runnable-policy",
39368
+ "over-cap"
39369
+ ]);
39370
+ var MIN_TEST_BUDGET_MINUTES = 5;
39371
+ var MAX_TEST_BUDGET_MINUTES = 120;
39372
+ function baselineFields(b) {
39373
+ const out = {};
39374
+ const m = b?.testBudgetMinutes;
39375
+ if (Number.isInteger(m) && m >= MIN_TEST_BUDGET_MINUTES && m <= MAX_TEST_BUDGET_MINUTES) {
39376
+ out.testBudgetMinutes = m;
39377
+ }
39378
+ if (typeof b?.baselineStatus === "string" && BASELINE_STATUSES.has(b.baselineStatus)) {
39379
+ out.baselineStatus = b.baselineStatus;
39380
+ }
39381
+ if (typeof b?.baselineSha === "string" && /^[0-9a-f]{7,40}$/.test(b.baselineSha)) {
39382
+ out.baselineSha = b.baselineSha;
39383
+ }
39384
+ return out;
39385
+ }
39386
+ function baselineLine({ baselineStatus, baselineSha, testBudgetMinutes } = {}) {
39387
+ const at = typeof baselineSha === "string" ? ` at ${baselineSha.slice(0, 12)}` : "";
39388
+ const limit2 = typeof testBudgetMinutes === "number" ? ` Time limit ${String(testBudgetMinutes)} min.` : "";
39389
+ switch (baselineStatus) {
39390
+ case "passed":
39391
+ return `Tests ran on Terminal Hire's machines${at} and passed.${limit2}`;
39392
+ case "failed":
39393
+ return `Tests ran on Terminal Hire's machines${at} and did not pass.${limit2}`;
39394
+ case "unreadable":
39395
+ return `Tests ran; Terminal Hire could not read the result.${limit2}`;
39396
+ case "no-tests":
39397
+ return `Tests ran and none were observed.${limit2}`;
39398
+ case "over-ceiling":
39399
+ return "Terminal Hire stopped this repository's tests at the 120-min limit.";
39400
+ case "not-runnable":
39401
+ return "Terminal Hire could not run this repository's tests.";
39402
+ case "not-runnable-policy":
39403
+ return "Terminal Hire does not run tests for postings shared at this level.";
39404
+ case "pending":
39405
+ return "Test time being measured.";
39406
+ case "over-cap":
39407
+ return "Test time not measured yet.";
39408
+ default:
39409
+ return null;
39410
+ }
39411
+ }
39412
+ function postingTestBudgetMinutes(bountyId) {
39413
+ if (typeof bountyId !== "string") return null;
39414
+ const job = findClaimableInCache(bountyId);
39415
+ if (!job || job.source !== "bounty") return null;
39416
+ return baselineFields(job.bounty).testBudgetMinutes ?? null;
39417
+ }
38894
39418
  function extractClaimableFields(job) {
38895
39419
  if (job.source === "contribute") {
38896
39420
  const c = job.contribution ?? {};
@@ -38934,7 +39458,9 @@ function extractClaimableFields(job) {
38934
39458
  // (`publicRunRequirements`). Carried, never derived here: the CLI has no copy of the
38935
39459
  // repository before a claim, and a second derivation could disagree with the one the
38936
39460
  // poster saw. Null when the index carries none.
38937
- runRequirements: b.runRequirements && typeof b.runRequirements === "object" ? b.runRequirements : null
39461
+ runRequirements: b.runRequirements && typeof b.runRequirements === "object" ? b.runRequirements : null,
39462
+ // TERM-1308. Present only when the index row carries them; no key otherwise.
39463
+ ...b.bountySource === "founder" ? baselineFields(b) : {}
38938
39464
  };
38939
39465
  }
38940
39466
  function parseGitHubUrl(url) {
@@ -39545,6 +40071,7 @@ async function resolveBounty(arg) {
39545
40071
  let founderPosting = false;
39546
40072
  let claimMode = "open";
39547
40073
  let runRequirements = null;
40074
+ let baseline = {};
39548
40075
  let job = findClaimableInCache(arg) ?? (looksLikeShortRef(arg) ? findClaimableByShortRef(arg) : null);
39549
40076
  let freshPool;
39550
40077
  if (!job && looksLikeShortRef(arg)) {
@@ -39552,6 +40079,7 @@ async function resolveBounty(arg) {
39552
40079
  if (freshPool) job = findByShortRefInPool(freshPool, arg);
39553
40080
  }
39554
40081
  if (job) {
40082
+ let testBudgetMinutes, baselineStatus, baselineSha;
39555
40083
  ({
39556
40084
  bountyId,
39557
40085
  title,
@@ -39562,8 +40090,16 @@ async function resolveBounty(arg) {
39562
40090
  openPRsAtDiscovery,
39563
40091
  founderPosting,
39564
40092
  claimMode,
39565
- runRequirements
40093
+ runRequirements,
40094
+ testBudgetMinutes,
40095
+ baselineStatus,
40096
+ baselineSha
39566
40097
  } = extractClaimableFields(job));
40098
+ baseline = {
40099
+ ...testBudgetMinutes === void 0 ? {} : { testBudgetMinutes },
40100
+ ...baselineStatus === void 0 ? {} : { baselineStatus },
40101
+ ...baselineSha === void 0 ? {} : { baselineSha }
40102
+ };
39567
40103
  indexNativeId = bountyId;
39568
40104
  } else {
39569
40105
  const parsed = parseGitHubUrl(arg);
@@ -39635,7 +40171,8 @@ async function resolveBounty(arg) {
39635
40171
  founderPosting,
39636
40172
  claimMode,
39637
40173
  // TERM-1259. Only a founder index hit carries these; every other path is null.
39638
- runRequirements: runRequirements ?? null
40174
+ runRequirements: runRequirements ?? null,
40175
+ ...baseline
39639
40176
  };
39640
40177
  }
39641
40178
  function fmtOpenPRsLine(b) {
@@ -40344,6 +40881,8 @@ async function cmdPreview(arg, { json } = {}) {
40344
40881
  openPRs: b.openPRs,
40345
40882
  assignees: b.assignees,
40346
40883
  contested: isContested(b),
40884
+ // TERM-1308. The posting's test budget and baseline, only when the index carried them.
40885
+ ...baselineFields(b),
40347
40886
  // Absent on a founder posting — see the scan above. A consumer must read
40348
40887
  // "no policy key" as "no policy step exists here", never as "clean".
40349
40888
  ...policy ? {
@@ -40374,6 +40913,8 @@ async function cmdPreview(arg, { json } = {}) {
40374
40913
  console.log(fmtOpenPRsLine(b));
40375
40914
  const previewContested = fmtContestedWarning(b);
40376
40915
  if (previewContested) console.log(previewContested);
40916
+ const previewBaseline = baselineLine(b);
40917
+ if (previewBaseline) console.log(` tests: ${previewBaseline}`);
40377
40918
  if (policy) printPolicySection(policy);
40378
40919
  if (process.stdout.isTTY) {
40379
40920
  try {
@@ -44042,6 +44583,8 @@ export {
44042
44583
  assertNoBooleanPath,
44043
44584
  attemptSliceDelivery,
44044
44585
  backgroundEnableFailed,
44586
+ baselineFields,
44587
+ baselineLine,
44045
44588
  beatFounderPresence,
44046
44589
  buildAssignmentComment,
44047
44590
  buildPatchSubmission,
@@ -44094,6 +44637,7 @@ export {
44094
44637
  pickBodySource,
44095
44638
  pickExistingPr,
44096
44639
  pickStartableClaim,
44640
+ postingTestBudgetMinutes,
44097
44641
  printNextSteps,
44098
44642
  printReleaseIsLocalOnly,
44099
44643
  printResolveHint,