humanish 0.88.1 → 0.89.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.
Files changed (76) hide show
  1. package/README.md +8 -0
  2. package/dist/actor-contract.d.ts +1 -1
  3. package/dist/actor-stop-cause.js +1 -0
  4. package/dist/actor-stop-cause.js.map +1 -1
  5. package/dist/computer-use-actor.d.ts +2 -0
  6. package/dist/computer-use-actor.js +6 -3
  7. package/dist/computer-use-actor.js.map +1 -1
  8. package/dist/computer-use.d.ts +5 -2
  9. package/dist/computer-use.js +38 -3
  10. package/dist/computer-use.js.map +1 -1
  11. package/dist/cua-actor-lab.js +3 -1
  12. package/dist/cua-actor-lab.js.map +1 -1
  13. package/dist/cua-diagnostics.d.ts +1 -1
  14. package/dist/cua-diagnostics.js +1 -1
  15. package/dist/cua-diagnostics.js.map +1 -1
  16. package/dist/export-bundle.js +6 -0
  17. package/dist/export-bundle.js.map +1 -1
  18. package/dist/export.js +67 -10
  19. package/dist/export.js.map +1 -1
  20. package/dist/feedback.d.ts +10 -0
  21. package/dist/feedback.js +58 -1
  22. package/dist/feedback.js.map +1 -1
  23. package/dist/index.d.ts +5 -1
  24. package/dist/index.js +2 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/lab-config.d.ts +1 -1
  27. package/dist/observer-app.html +10 -9
  28. package/dist/observer.d.ts +2 -0
  29. package/dist/observer.js +52 -5
  30. package/dist/observer.js.map +1 -1
  31. package/dist/openai-responses-cu.d.ts +3 -0
  32. package/dist/openai-responses-cu.js +4 -1
  33. package/dist/openai-responses-cu.js.map +1 -1
  34. package/dist/pricing.js +2 -2
  35. package/dist/pricing.js.map +1 -1
  36. package/dist/program.js +146 -3
  37. package/dist/program.js.map +1 -1
  38. package/dist/run.d.ts +19 -1
  39. package/dist/run.js +151 -21
  40. package/dist/run.js.map +1 -1
  41. package/dist/shared-world-lab.d.ts +2 -0
  42. package/dist/shared-world-lab.js +90 -5
  43. package/dist/shared-world-lab.js.map +1 -1
  44. package/dist/study-analysis-engine.d.ts +34 -0
  45. package/dist/study-analysis-engine.js +217 -0
  46. package/dist/study-analysis-engine.js.map +1 -0
  47. package/dist/study-analysis-evidence.d.ts +27 -0
  48. package/dist/study-analysis-evidence.js +420 -0
  49. package/dist/study-analysis-evidence.js.map +1 -0
  50. package/dist/study-analysis-provider.d.ts +38 -0
  51. package/dist/study-analysis-provider.js +140 -0
  52. package/dist/study-analysis-provider.js.map +1 -0
  53. package/dist/study-analysis-service.d.ts +45 -0
  54. package/dist/study-analysis-service.js +207 -0
  55. package/dist/study-analysis-service.js.map +1 -0
  56. package/dist/study-analysis-sharing.d.ts +11 -0
  57. package/dist/study-analysis-sharing.js +27 -0
  58. package/dist/study-analysis-sharing.js.map +1 -0
  59. package/dist/study-analysis-store.d.ts +30 -0
  60. package/dist/study-analysis-store.js +343 -0
  61. package/dist/study-analysis-store.js.map +1 -0
  62. package/dist/study-analysis-validation.d.ts +448 -0
  63. package/dist/study-analysis-validation.js +331 -0
  64. package/dist/study-analysis-validation.js.map +1 -0
  65. package/dist/study-analysis.d.ts +167 -0
  66. package/dist/study-analysis.js +4 -0
  67. package/dist/study-analysis.js.map +1 -0
  68. package/docs/architecture/observer.md +10 -1
  69. package/docs/contracts/feedback.md +9 -3
  70. package/docs/contracts/schemas.md +52 -3
  71. package/docs/contracts/study-analysis.md +150 -0
  72. package/docs/goals/current.md +4 -3
  73. package/docs/ramp/README.md +13 -2
  74. package/docs/release/0.88.2-sequential-study-budgets.md +57 -0
  75. package/docs/release/0.89.0-study-findings.md +79 -0
  76. package/package.json +3 -2
package/dist/run.d.ts CHANGED
@@ -646,6 +646,20 @@ export interface SharedWorldTurn {
646
646
  seedDigest: string;
647
647
  }
648
648
  export type SharedWorldTimelineEntry = SharedWorldCheckpoint | SharedWorldTurn;
649
+ /** Declared seats that were never started after one executed sequential role stopped the run.
650
+ * The executed timeline plus this ordered tail must account for every declared sim/stream. */
651
+ export interface SharedWorldSkippedTail {
652
+ afterRoleId: string;
653
+ roles: Array<{
654
+ roleId: string;
655
+ simId: string;
656
+ streamId: string;
657
+ }>;
658
+ cause: "harness_error" | "session_error" | "usage_unreported" | "study_spend_limit";
659
+ /** Present only for a measured aggregate threshold; estimates, not provider billing. */
660
+ maxTotalUsd?: number;
661
+ estimatedTotalUsd?: number;
662
+ }
649
663
  /**
650
664
  * The shared-world evidence block (`humanish.shared-world.v1`). TWO variants discriminated by
651
665
  * `topologyMode` (FIX-8 — renamed off `RunBundle.mode` to avoid the dry-run|live collision):
@@ -690,6 +704,8 @@ export interface SharedWorldEvidence {
690
704
  /** The role ids that actually took a turn, in declared order. */
691
705
  sequence?: string[];
692
706
  timeline?: SharedWorldTimelineEntry[];
707
+ /** Explicit unstarted suffix; absent on historical/full-execution bundles. */
708
+ skippedTail?: SharedWorldSkippedTail;
693
709
  /** Per-actor harness-clocked windows (overlap proves simultaneity). */
694
710
  laneWindows?: SharedWorldLaneWindow[];
695
711
  /** Cadence digests of the shared world under load (baseline + periodic + final). */
@@ -1055,6 +1071,8 @@ export interface RunResult {
1055
1071
  export interface VerifyResult {
1056
1072
  schema: typeof VERIFY_SCHEMA;
1057
1073
  ok: boolean;
1074
+ /** Only present when source evidence verifies but derived analysis failed the public-safety scan. */
1075
+ recordingOk?: boolean;
1058
1076
  cwd: string;
1059
1077
  run: string;
1060
1078
  bundlePath?: string;
@@ -1066,7 +1084,7 @@ export interface VerifyResult {
1066
1084
  shareSafety: {
1067
1085
  status: "share_ready" | "local_only" | "blocked";
1068
1086
  reasons: Array<{
1069
- code: "VERIFY_FAILED" | "PUBLIC_SAFETY_FINDINGS" | "RAW_SCREENSHOTS";
1087
+ code: "VERIFY_FAILED" | "PUBLIC_SAFETY_FINDINGS" | "ANALYSIS_UNVERIFIED" | "RAW_SCREENSHOTS";
1070
1088
  message: string;
1071
1089
  }>;
1072
1090
  };
package/dist/run.js CHANGED
@@ -22,6 +22,8 @@ import { screenshotEvidenceError } from "./image-evidence.js";
22
22
  import { buildObserverData } from "./observer-data.js";
23
23
  import { parseResolvedPersona, personaToDirectives, renderPersonaPromptSection } from "./persona.js";
24
24
  import { round6 } from "./pricing.js";
25
+ import { loadStudyAnalysis, listStudyAnalysisExecutions } from "./study-analysis-store.js";
26
+ import { isStudyAnalysisRecordPath, studyAnalysisSharingProblems } from "./study-analysis-sharing.js";
25
27
  import { containsSensitive, digestText, redactText, redactToSecretLabel, tailText } from "./redaction.js";
26
28
  import { bindExistingRunArtifactPaths, RUNS_RELATIVE_ROOT, isSafeRunIdSegment, prepareRunArtifactPaths, resolveExistingRunDirectory, resolveLatestRunDirectory, resolveRunsRoot, validatePreparedRunArtifactPaths } from "./run-paths.js";
27
29
  import { probeKeySources } from "./key-resolution.js";
@@ -2612,7 +2614,13 @@ async function verifyPreparedRun(cwd, runInput, runPaths) {
2612
2614
  ok: reviewJson !== null && reviewMarkdown !== null,
2613
2615
  message: "review.json and review.md must exist"
2614
2616
  });
2615
- const publicSafetyFindings = await scanRunPublicSafetyArtifacts(runPaths);
2617
+ const derivedPublicSafetyFindings = [];
2618
+ const publicSafetyFindings = await scanRunPublicSafetyArtifacts(runPaths, derivedPublicSafetyFindings);
2619
+ // JSON escapes can hide a sensitive value from the byte scan while the
2620
+ // decoded recording exposes it to Observer, feedback, or analysis input.
2621
+ if (publicSafetyFindings.length < 50 && bundle !== null && containsSensitivePattern(JSON.stringify(bundle))) {
2622
+ publicSafetyFindings.push("sensitive decoded run.json");
2623
+ }
2616
2624
  checks.push({
2617
2625
  name: "public-safety scan",
2618
2626
  ok: publicSafetyFindings.length === 0,
@@ -2716,6 +2724,10 @@ async function verifyPreparedRun(cwd, runInput, runPaths) {
2716
2724
  ? "cost figures are absent, or every claimed estimate carries its ratesAsOf date + source and the total matches its known lines (estimates never presented as exact)"
2717
2725
  : `cost labeling findings: ${costFindings.join(", ")}`
2718
2726
  });
2727
+ const recordingOk = checks.every((check) => check.ok);
2728
+ if (derivedPublicSafetyFindings.length > 0)
2729
+ checks.push({ name: "derived analysis public-safety scan", ok: false,
2730
+ message: "Derived analysis contains sensitive text or unsafe artifact paths; sharing is blocked, original recording remains independently verifiable." });
2719
2731
  const ok = checks.every((check) => check.ok);
2720
2732
  const warnings = isRunBundle(bundle)
2721
2733
  ? [...rawScreenshotPostureWarnings(bundle), ...undeclaredSubjectStateWarnings(bundle), ...desktopGeometryWarnings(bundle)]
@@ -2731,9 +2743,23 @@ async function verifyPreparedRun(cwd, runInput, runPaths) {
2731
2743
  }
2732
2744
  ]
2733
2745
  };
2746
+ // Interpretation validity is independent of run validity. Keep recordings usable,
2747
+ // while refusing to silently promote stale or malformed derived text into sharing.
2748
+ const analysis = await loadStudyAnalysis(runPaths);
2749
+ const analysisSharing = studyAnalysisSharingProblems(analysis);
2750
+ const executionHistory = await listStudyAnalysisExecutions(runPaths);
2751
+ if (analysisSharing.unverified || analysisSharing.sensitive || executionHistory.warnings.length > 0) {
2752
+ warnings.push("Some study analysis or correction records could not be validated against current evidence.");
2753
+ shareSafety.reasons.push({ code: "ANALYSIS_UNVERIFIED", message: "Derived analysis or corrections need review against the current evidence before sharing." });
2754
+ if (analysisSharing.sensitive)
2755
+ shareSafety.status = "blocked";
2756
+ else if (shareSafety.status === "share_ready")
2757
+ shareSafety.status = "local_only";
2758
+ }
2734
2759
  return {
2735
2760
  schema: VERIFY_SCHEMA,
2736
2761
  ok,
2762
+ ...(!ok && recordingOk ? { recordingOk: true } : {}),
2737
2763
  cwd,
2738
2764
  run: runInput,
2739
2765
  bundlePath: path.relative(cwd, bundlePath),
@@ -4414,7 +4440,7 @@ const SHARED_WORLD_STATESERIES_KEYS = new Set(["timestamp", "digest"]);
4414
4440
  */
4415
4441
  function sharedWorldEvidenceFindings(bundle) {
4416
4442
  if (bundle.mode !== "live") {
4417
- return [];
4443
+ return bundle.sharedWorld?.skippedTail === undefined ? [] : ["skippedTail requires a live executed interruption"];
4418
4444
  }
4419
4445
  const sw = bundle.sharedWorld;
4420
4446
  if (!sw) {
@@ -4426,6 +4452,9 @@ function sharedWorldEvidenceFindings(bundle) {
4426
4452
  }
4427
4453
  // FIX-8: dispatch on topologyMode FIRST; unknown/missing → fail closed.
4428
4454
  const topologyMode = sw.topologyMode;
4455
+ if (topologyMode !== "sequential" && sw.skippedTail !== undefined) {
4456
+ return ["skippedTail is only valid on sequential shared-world evidence"];
4457
+ }
4429
4458
  if (topologyMode === "sequential") {
4430
4459
  return sequentialSharedWorldFindings(bundle, sw);
4431
4460
  }
@@ -4457,6 +4486,95 @@ function sharedWorldCommonFindings(bundle, sw) {
4457
4486
  }
4458
4487
  return findings;
4459
4488
  }
4489
+ /** Validate the declared suffix against the executed prefix and existing participant evidence. */
4490
+ function sequentialSkippedTailFindings(bundle, sw, sequence, turns) {
4491
+ const failures = [];
4492
+ const reject = (message) => { failures.push(`skippedTail: ${message}`); };
4493
+ const tail = sw.skippedTail;
4494
+ if (!isRecord(tail) || !Array.isArray(tail.roles) || tail.roles.length === 0 || !tail.roles.every(isRecord)) {
4495
+ return ["skippedTail: a nonempty declared role suffix is required"];
4496
+ }
4497
+ const roster = [...turns, ...tail.roles];
4498
+ if (!Number.isSafeInteger(sw.roleCount) || sw.roleCount < 1
4499
+ || roster.length !== sw.roleCount || bundle.simCount !== sw.roleCount
4500
+ || bundle.simulations.length !== sw.roleCount || bundle.streams.length !== sw.roleCount
4501
+ || sequence.length !== turns.length || turns.length === 0) {
4502
+ reject("executed prefix and blocked suffix must account for every declared simulation and stream");
4503
+ }
4504
+ for (const key of ["roleId", "simId", "streamId"]) {
4505
+ const ids = roster.map(role => role[key]);
4506
+ if (ids.some(id => typeof id !== "string" || id.length === 0) || new Set(ids).size !== ids.length) {
4507
+ reject(`declared ${key} values must be nonempty and unique`);
4508
+ }
4509
+ }
4510
+ if (tail.afterRoleId !== sequence.at(-1) || tail.afterRoleId !== turns.at(-1)?.roleId) {
4511
+ reject("blocker must be the immediately preceding executed role");
4512
+ }
4513
+ if (bundle.review.verdict === "pass")
4514
+ reject("blocked participants cannot accompany a passed run review");
4515
+ roster.forEach((role, index) => {
4516
+ const sim = bundle.simulations[index], stream = bundle.streams[index];
4517
+ if (!sim || !stream || sim.index !== index + 1 || role.simId !== sim.id || role.streamId !== stream.id
4518
+ || stream.simId !== sim.id || sim.streamIds.length !== 1 || sim.streamIds[0] !== stream.id) {
4519
+ reject("ordered role, simulation and stream identities must agree");
4520
+ return;
4521
+ }
4522
+ const roleEvents = bundle.events.filter(event => event.simId === sim.id && event.streamId === stream.id);
4523
+ if (index < turns.length) {
4524
+ if (!stream.actor && !roleEvents.some(event => event.type === "shared-world.session.error")) {
4525
+ reject("an executed role needs an actor or an explicit attempted-session error");
4526
+ }
4527
+ return;
4528
+ }
4529
+ if (sim.status !== "blocked" || stream.status !== "blocked"
4530
+ || stream.actor !== undefined || stream.liveActor !== undefined || stream.embed?.kind !== "placeholder"
4531
+ || stream.ui?.actorStatus !== undefined || stream.ui?.screenshotUrl !== undefined
4532
+ || stream.artifacts.some(artifact => artifact.kind === "trace" || artifact.kind === "screenshot")
4533
+ || typeof sim.currentStep !== "string" || sim.currentStep.length === 0 || stream.ui?.state !== sim.currentStep) {
4534
+ reject("an unstarted role must be blocked with a reason and no actor, trace or screenshot");
4535
+ }
4536
+ const sessionEvents = roleEvents.filter(event => event.type.startsWith("shared-world.session."));
4537
+ if (sessionEvents.length !== 1 || sessionEvents[0]?.type !== "shared-world.session.blocked") {
4538
+ reject("each unstarted role needs exactly one blocked session event");
4539
+ }
4540
+ });
4541
+ const predecessor = bundle.streams[turns.length - 1];
4542
+ const actor = predecessor?.actor;
4543
+ if (tail.cause === "session_error") {
4544
+ if (!predecessor || !bundle.events.some(event => event.type === "shared-world.session.error"
4545
+ && event.simId === predecessor.simId && event.streamId === predecessor.id)) {
4546
+ reject("session_error requires the predecessor's explicit orchestration error");
4547
+ }
4548
+ }
4549
+ else if (tail.cause === "harness_error") {
4550
+ if (actor?.completionReason !== "harness_error")
4551
+ reject("harness_error must match the predecessor actor");
4552
+ }
4553
+ else if (tail.cause === "usage_unreported") {
4554
+ if (!actor || !(actor.interactionUsageIncomplete === true || actor.debrief?.usageReported === false
4555
+ || actor.estimatedCost?.estimatedCostUsd === null))
4556
+ reject("usage_unreported requires recorded unavailable usage");
4557
+ }
4558
+ else if (tail.cause === "study_spend_limit") {
4559
+ const estimates = bundle.streams.slice(0, turns.length).map(stream => stream.actor?.estimatedCost?.estimatedCostUsd);
4560
+ const allKnown = estimates.every(value => typeof value === "number" && Number.isFinite(value) && value >= 0)
4561
+ && bundle.streams.slice(0, turns.length).every(stream => stream.actor?.interactionUsageIncomplete !== true
4562
+ && stream.actor?.debrief?.usageReported !== false);
4563
+ const sum = estimates.reduce((total, value) => total + (value ?? 0), 0);
4564
+ if (!allKnown || typeof tail.maxTotalUsd !== "number" || !Number.isFinite(tail.maxTotalUsd) || tail.maxTotalUsd < 0
4565
+ || typeof tail.estimatedTotalUsd !== "number" || !Number.isFinite(tail.estimatedTotalUsd)
4566
+ || tail.estimatedTotalUsd !== sum || !(sum > tail.maxTotalUsd)) {
4567
+ reject("study_spend_limit requires known prefix estimates exceeding the recorded finite threshold");
4568
+ }
4569
+ }
4570
+ else {
4571
+ reject("a supported typed interruption cause is required");
4572
+ }
4573
+ if (tail.cause !== "study_spend_limit" && (tail.maxTotalUsd !== undefined || tail.estimatedTotalUsd !== undefined)) {
4574
+ reject("budget figures require a measured study_spend_limit cause");
4575
+ }
4576
+ return failures;
4577
+ }
4460
4578
  /**
4461
4579
  * SEQUENTIAL branch (the PoC #164): the alternating timeline must be well-formed, single-plane,
4462
4580
  * digest-only, and carry the sequential attributionLimits. FIX-8: a sequential bundle must NOT
@@ -4485,8 +4603,12 @@ function sequentialSharedWorldFindings(bundle, sw) {
4485
4603
  }
4486
4604
  const checkpoints = rawTimeline.filter((entry) => isRecord(entry) && entry.kind === "checkpoint");
4487
4605
  const turns = rawTimeline.filter((entry) => isRecord(entry) && entry.kind === "turn");
4488
- // Phantom/dropped role: sequence length == roleCount == executed-turn count.
4489
- if (!(sequence.length === sw.roleCount && turns.length === sw.roleCount)) {
4606
+ // Historical full-execution bundles keep the original equality rule. A shorter executed
4607
+ // prefix requires explicit blocked-tail evidence, never an inference from absent actors.
4608
+ if (sw.skippedTail !== undefined) {
4609
+ findings.push(...sequentialSkippedTailFindings(bundle, sw, sequence, turns));
4610
+ }
4611
+ else if (!(sequence.length === sw.roleCount && turns.length === sw.roleCount)) {
4490
4612
  findings.push(`phantom/dropped role: sequence length (${sequence.length}), roleCount (${sw.roleCount}), and timeline turn count (${turns.length}) must all match`);
4491
4613
  }
4492
4614
  // Timeline well-formed: starts with cp-baseline, strictly alternates checkpoint → turn →
@@ -4517,6 +4639,12 @@ function sequentialSharedWorldFindings(bundle, sw) {
4517
4639
  if (turn.roleId !== sequence[index]) {
4518
4640
  findings.push(`turn order does not match the declared sequence at position ${index} (turn "${String(turn.roleId)}" vs sequence "${String(sequence[index])}")`);
4519
4641
  }
4642
+ if (sw.skippedTail !== undefined) {
4643
+ const checkpoint = rawTimeline[index * 2 + 2];
4644
+ if (!isRecord(checkpoint) || checkpoint.name !== `cp-after-${String(turn.roleId)}`) {
4645
+ findings.push("skippedTail: each after-checkpoint must belong to its executed role, never an unstarted seat");
4646
+ }
4647
+ }
4520
4648
  });
4521
4649
  // Checkpoints: digest is sha256-16 and the record carries NO value-shaped field (digest-only).
4522
4650
  for (const checkpoint of checkpoints) {
@@ -4938,15 +5066,17 @@ const riskyPublicArtifactPathSegments = new Set([
4938
5066
  "Secure Preferences",
4939
5067
  "profiles"
4940
5068
  ]);
4941
- async function scanRunPublicSafetyArtifacts(runPaths) {
5069
+ async function scanRunPublicSafetyArtifacts(runPaths, derivedFindings) {
4942
5070
  const findings = [];
4943
5071
  await validatePreparedRunArtifactPaths(runPaths);
4944
- await scanRunPublicSafetyDirectory(runPaths, "", findings);
5072
+ await scanRunPublicSafetyDirectory(runPaths, "", findings, derivedFindings);
4945
5073
  await validatePreparedRunArtifactPaths(runPaths);
4946
5074
  return findings;
4947
5075
  }
4948
- async function scanRunPublicSafetyDirectory(runPaths, relativeDirectory, findings) {
4949
- if (findings.length >= 50) {
5076
+ async function scanRunPublicSafetyDirectory(runPaths, relativeDirectory, findings, derivedFindings) {
5077
+ // Each authority has its own finding budget. Derived files must never consume
5078
+ // the source scan's budget and make an unscanned recording appear verified.
5079
+ if (findings.length >= 50 && derivedFindings.length >= 50) {
4950
5080
  return;
4951
5081
  }
4952
5082
  const current = relativeDirectory
@@ -4955,33 +5085,33 @@ async function scanRunPublicSafetyDirectory(runPaths, relativeDirectory, finding
4955
5085
  const entries = await readdir(current).catch(() => []);
4956
5086
  for (const entryName of entries) {
4957
5087
  const relativePath = relativeDirectory ? `${relativeDirectory}/${entryName}` : entryName;
5088
+ const stats = await lstat(path.join(current, entryName), { bigint: true }).catch(() => null);
5089
+ const selectedFindings = !stats?.isDirectory()
5090
+ && (relativePath === "observer/study-analysis.json" || isStudyAnalysisRecordPath(relativePath)) ? derivedFindings : findings;
4958
5091
  if (isRiskyPublicArtifactPath(relativePath) || containsSensitivePattern(relativePath)) {
4959
- findings.push(`risky artifact path ${relativePath}`);
4960
- if (findings.length >= 50)
4961
- return;
5092
+ if (selectedFindings.length < 50)
5093
+ selectedFindings.push(`risky artifact path ${relativePath}`);
4962
5094
  }
4963
- const stats = await lstat(path.join(current, entryName), { bigint: true }).catch(() => null);
4964
5095
  if (!stats || stats.isSymbolicLink() || (!stats.isDirectory() && !stats.isFile()) || (stats.isFile() && stats.nlink > 1n)) {
4965
- findings.push(`unsafe artifact leaf ${relativePath}`);
4966
- if (findings.length >= 50)
4967
- return;
5096
+ if (selectedFindings.length < 50)
5097
+ selectedFindings.push(`unsafe artifact leaf ${relativePath}`);
4968
5098
  continue;
4969
5099
  }
4970
5100
  if (stats.isDirectory()) {
4971
- await scanRunPublicSafetyDirectory(runPaths, relativePath, findings);
4972
- if (findings.length >= 50)
4973
- return;
5101
+ // A directory named analysis.json is not an owned record. Its children
5102
+ // can contain source evidence even after derived findings are saturated.
5103
+ await scanRunPublicSafetyDirectory(runPaths, relativePath, findings, derivedFindings);
4974
5104
  continue;
4975
5105
  }
5106
+ if (selectedFindings.length >= 50)
5107
+ continue;
4976
5108
  if (!shouldScanTextArtifact(relativePath)) {
4977
5109
  continue;
4978
5110
  }
4979
5111
  const bytes = await readSafeRunArtifactBytes(runPaths, relativePath);
4980
5112
  const text = bytes?.toString("utf8") ?? null;
4981
5113
  if (text !== null && containsSensitivePattern(text)) {
4982
- findings.push(`sensitive text ${relativePath}`);
4983
- if (findings.length >= 50)
4984
- return;
5114
+ selectedFindings.push(`sensitive text ${relativePath}`);
4985
5115
  }
4986
5116
  }
4987
5117
  }