humanish 0.128.0 → 0.130.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.
- package/CHANGELOG.md +24 -0
- package/dist/actors/stop-cause.d.ts +1 -1
- package/dist/analysis/automatic.js +0 -1
- package/dist/analysis/evidence-sources.d.ts +44 -7
- package/dist/analysis/evidence-sources.js +40 -6
- package/dist/analysis/evidence.js +96 -125
- package/dist/analysis/service.js +12 -6
- package/dist/analysis/source.d.ts +28 -0
- package/dist/analysis/source.js +118 -0
- package/dist/analysis/validation.js +1 -1
- package/dist/cli/commands/study-format.js +2 -1
- package/dist/feedback/export.js +0 -1
- package/dist/observer/data.d.ts +2 -2
- package/dist/observer/feed-cursors.d.ts +3 -1
- package/dist/observer/feed-cursors.js +5 -2
- package/dist/observer/run-routes.js +40 -7
- package/dist/observer-app.html +1 -1
- package/dist/routes/computer-use/fanout-bundle.js +4 -7
- package/dist/routes/shared-world/bundle-records.js +6 -1
- package/dist/routes/shared-world/bundle.d.ts +8 -0
- package/dist/routes/shared-world/bundle.js +33 -13
- package/dist/routes/shared-world/participant-specs.d.ts +2 -1
- package/dist/routes/shared-world/participant-specs.js +5 -2
- package/dist/routes/shared-world/result.js +13 -7
- package/dist/routes/shared-world/types.d.ts +2 -0
- package/dist/run/bundle-shape.d.ts +2 -2
- package/dist/run/bundle.d.ts +8 -1
- package/dist/run/detail.js +4 -1
- package/dist/run/outcomes.d.ts +7 -0
- package/dist/run/outcomes.js +14 -0
- package/dist/run/participant-records.d.ts +1 -1
- package/dist/run/stream-shape.d.ts +2 -2
- package/dist/run/stream-trace.d.ts +46 -15
- package/dist/run/stream-trace.js +101 -20
- package/dist/run/streams.d.ts +18 -0
- package/dist/study/bundle-estimate.d.ts +4 -1
- package/dist/study/bundle-estimate.js +40 -15
- package/dist/verify/verify.js +28 -21
- package/docs/architecture/observer.md +13 -0
- package/docs/contracts/run-bundle.md +11 -5
- package/docs/contracts/study-analysis.md +13 -1
- package/package.json +1 -1
|
@@ -4,7 +4,7 @@ import { actorEnding } from "../../actors/stop-cause.js";
|
|
|
4
4
|
import { cuaGoalSource } from "../../actors/goal-source.js";
|
|
5
5
|
import { redactText } from "../../evidence/redaction.js";
|
|
6
6
|
import { REVIEW_SCHEMA, bundleArtifacts, bundleHead, } from "../../run/bundle.js";
|
|
7
|
-
import { aggregateTaskFunnels, formatParticipantOutcomes, formatRunTaskFunnel, gapsListClause, withCuaReviewProvenance, } from "../../run/outcomes.js";
|
|
7
|
+
import { aggregateTaskFunnels, formatParticipantOutcomes, formatRunTaskFunnel, gapsListClause, skippedSentence, withCuaReviewProvenance, } from "../../run/outcomes.js";
|
|
8
8
|
import { providerResourcesForOutcome, publicSafeAppUrlLabel } from "./bundle-parts.js";
|
|
9
9
|
import { subjectName } from "../../run/subject-name.js";
|
|
10
10
|
import { participantFeedbackCandidates } from "./participant-feedback.js";
|
|
@@ -63,12 +63,9 @@ function fanoutSummary(args, facts) {
|
|
|
63
63
|
// A run where every participant was skipped has no one to describe in this sentence.
|
|
64
64
|
if (tookPart > 0)
|
|
65
65
|
sentences.push(`${plural(tookPart, "participant")} took part, ${tookPart === 1 ? "in its own copy" : "each in their own copy"} of the app: ${outcomes}${gaps}.`);
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
sentences.push(`1 participant was skipped before its session started${why}.`);
|
|
70
|
-
if (facts.skipped > 1)
|
|
71
|
-
sentences.push(`${facts.skipped} participants were skipped before their sessions started${why}.`);
|
|
66
|
+
const skipped = skippedSentence(facts.skipped, gaps === "");
|
|
67
|
+
if (skipped !== "")
|
|
68
|
+
sentences.push(skipped);
|
|
72
69
|
return `${host}${rerun}${sentences.join(" ")}${tasks}`;
|
|
73
70
|
}
|
|
74
71
|
/**
|
|
@@ -67,7 +67,12 @@ function sharedWorldSimulation(ctx, spec, index, view) {
|
|
|
67
67
|
mode: "browser-sim",
|
|
68
68
|
progress: inProgress ? 35 : 100,
|
|
69
69
|
currentStep: view.reason,
|
|
70
|
-
summary: participantSummary(name, { name: "the shared app" }, {
|
|
70
|
+
summary: participantSummary(name, { name: "the shared app" }, {
|
|
71
|
+
trace: session?.trace,
|
|
72
|
+
skippedReason: outcome?.skippedReason,
|
|
73
|
+
sessionError: outcome?.sessionError,
|
|
74
|
+
inProgress,
|
|
75
|
+
}),
|
|
71
76
|
startedAt: args.run.createdAt,
|
|
72
77
|
updatedAt: args.run.createdAt,
|
|
73
78
|
...(args.plan.arrivals.declared
|
|
@@ -7,6 +7,14 @@ export declare function maxSimultaneousWindows(windows: Array<{
|
|
|
7
7
|
startedAt: number;
|
|
8
8
|
endedAt: number;
|
|
9
9
|
}>): number;
|
|
10
|
+
/**
|
|
11
|
+
* The windows of the participants that used the app, for overlap and the live-at-once count. A
|
|
12
|
+
* participant the run skipped before its session started never used it, though its window sits
|
|
13
|
+
* inside another participant's session. run.json's laneWindows keep that window: verify expects
|
|
14
|
+
* one per declared participant, and reads overlap from them only on a pass, which a run with a
|
|
15
|
+
* skipped participant cannot reach.
|
|
16
|
+
*/
|
|
17
|
+
export declare function usedAppWindows(results: readonly ActorRunResult[]): ActorRunResult[];
|
|
10
18
|
/**
|
|
11
19
|
* The judgment for a shared-world bundle's inputs: how each participant ended, plus what the run
|
|
12
20
|
* observed about its world: overlap, a state change under overlap on the provisioned plane, and
|
|
@@ -16,7 +16,7 @@ import { hostOriginDigest } from "./provenance.js";
|
|
|
16
16
|
import { participantEvent, participantIds } from "../../run/participant-records.js";
|
|
17
17
|
import { sharedWorldParticipantRecords } from "./bundle-records.js";
|
|
18
18
|
import { CONCURRENT_ATTRIBUTION_LIMITS, EXTERNAL_PUBLIC_ATTRIBUTION_LIMITS, } from "./types.js";
|
|
19
|
-
import { gapsListClause } from "../../run/outcomes.js";
|
|
19
|
+
import { gapsListClause, skippedSentence } from "../../run/outcomes.js";
|
|
20
20
|
import { plural } from "../../run/text.js";
|
|
21
21
|
import { describeArrivals } from "../../study/arrivals.js";
|
|
22
22
|
/** Max windows live at the same instant (sweep over start/end points). The simultaneity
|
|
@@ -38,6 +38,16 @@ export function maxSimultaneousWindows(windows) {
|
|
|
38
38
|
}
|
|
39
39
|
return max;
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* The windows of the participants that used the app, for overlap and the live-at-once count. A
|
|
43
|
+
* participant the run skipped before its session started never used it, though its window sits
|
|
44
|
+
* inside another participant's session. run.json's laneWindows keep that window: verify expects
|
|
45
|
+
* one per declared participant, and reads overlap from them only on a pass, which a run with a
|
|
46
|
+
* skipped participant cannot reach.
|
|
47
|
+
*/
|
|
48
|
+
export function usedAppWindows(results) {
|
|
49
|
+
return results.filter((result) => result.outcome.skippedReason === undefined);
|
|
50
|
+
}
|
|
41
51
|
/**
|
|
42
52
|
* The judgment for a shared-world bundle's inputs: how each participant ended, plus what the run
|
|
43
53
|
* observed about its world: overlap, a state change under overlap on the provisioned plane, and
|
|
@@ -52,11 +62,7 @@ export function judgeSharedWorldRun(args) {
|
|
|
52
62
|
expected: args.actorSpecs.length,
|
|
53
63
|
participants: args.actorResults.map((result) => participantFactsOf(result.outcome)),
|
|
54
64
|
world: {
|
|
55
|
-
|
|
56
|
-
...concurrencyFacts(args.actorSpecs.map((_spec, index) => ({
|
|
57
|
-
startedAt: args.actorResults[index]?.startedAt ?? 0,
|
|
58
|
-
endedAt: args.actorResults[index]?.endedAt ?? 0,
|
|
59
|
-
})), external ? undefined : args.stateSnapshots),
|
|
65
|
+
...concurrencyFacts(usedAppWindows(args.actorResults), external ? undefined : args.stateSnapshots),
|
|
60
66
|
...(external ? { lobbyConvergence: args.lobbyConvergenceDigest !== undefined } : {}),
|
|
61
67
|
},
|
|
62
68
|
});
|
|
@@ -68,8 +74,10 @@ export function actorRunPassed(result) {
|
|
|
68
74
|
* The run's summary in plain sentences: who took part, how many reached the goal, and what the
|
|
69
75
|
* world showed. A participant reached the goal when its session passed the judge's checks and
|
|
70
76
|
* ended at the goal, so a productive `budget_reached` session does not count even when the lobby
|
|
71
|
-
* converged.
|
|
72
|
-
*
|
|
77
|
+
* converged. A participant the run skipped, such as a follower stopped at the handoff, never used
|
|
78
|
+
* the app, so it is counted in a sentence of its own. Each participant's ending stays in
|
|
79
|
+
* `sharedWorld.outcomes`, and the plane, topology and attribution limits stay in the bundle's
|
|
80
|
+
* events and `sharedWorld` block.
|
|
73
81
|
*/
|
|
74
82
|
function sharedWorldSummary(args, facts) {
|
|
75
83
|
const count = args.actorSpecs.length;
|
|
@@ -79,16 +87,20 @@ function sharedWorldSummary(args, facts) {
|
|
|
79
87
|
const served = facts.external ? ", as it is deployed" : ", served from a sandbox";
|
|
80
88
|
return `Dry run: ${people} would use ${app} at the same time${served}. No sandboxes were launched and $0 was spent.`;
|
|
81
89
|
}
|
|
90
|
+
// The live summary is written once, before any participant starts, so it names the plan.
|
|
82
91
|
if (facts.inProgress) {
|
|
83
|
-
return `${people} ${count === 1 ? "is" : "are"}
|
|
92
|
+
return `${people} ${count === 1 ? "is planned, in" : "are planned, all in"} ${app}. The run is still going, so nothing here is final.`;
|
|
84
93
|
}
|
|
85
94
|
const passed = facts.outcomes.filter((outcome) => outcome.ok);
|
|
86
95
|
const reached = passed.filter((outcome) => outcome.completionReason === "goal_satisfied").length;
|
|
87
|
-
const
|
|
96
|
+
const skipped = args.actorResults.filter((result) => result.outcome.skippedReason !== undefined).length;
|
|
97
|
+
const tookPart = count - skipped;
|
|
98
|
+
const together = tookPart === 1 ? "" : facts.overlaps ? " at the same time" : ", but never at the same time";
|
|
88
99
|
// A participant who did not pass has a gap line; one who passed but stopped short has none.
|
|
100
|
+
const gaps = gapsListClause(tookPart - passed.length, tookPart);
|
|
89
101
|
const rest = [
|
|
90
102
|
passed.length > reached ? `; ${passed.length - reached} ended without reaching it` : "",
|
|
91
|
-
|
|
103
|
+
gaps,
|
|
92
104
|
].join("");
|
|
93
105
|
const world = facts.external
|
|
94
106
|
? args.lobbyConvergenceDigest
|
|
@@ -97,7 +109,15 @@ function sharedWorldSummary(args, facts) {
|
|
|
97
109
|
: facts.deltas === 0
|
|
98
110
|
? "The app's shared state did not change during the run."
|
|
99
111
|
: `The app's shared state changed ${plural(facts.deltas, "time")} during the run.`;
|
|
100
|
-
return
|
|
112
|
+
return [
|
|
113
|
+
tookPart > 0
|
|
114
|
+
? `${plural(tookPart, "participant")} used ${app}${together}. ${reached} of ${tookPart} reached the goal${rest}.`
|
|
115
|
+
: "",
|
|
116
|
+
skippedSentence(skipped, gaps === ""),
|
|
117
|
+
world,
|
|
118
|
+
]
|
|
119
|
+
.filter((sentence) => sentence !== "")
|
|
120
|
+
.join(" ");
|
|
101
121
|
}
|
|
102
122
|
/** The run's first two events: its creation and the shared plane's provenance. */
|
|
103
123
|
function planeEvents(args, external) {
|
|
@@ -261,7 +281,7 @@ function concurrencyReview(args, external, inProgress, evidence, events, nextEve
|
|
|
261
281
|
// The count that matters is how many participants were live at once, which can be fewer than exist:
|
|
262
282
|
// a 6-participant run capped at 3 must never read as 6-wide concurrency.
|
|
263
283
|
const capForReport = plan.concurrency;
|
|
264
|
-
const maxLive = maxSimultaneousWindows(
|
|
284
|
+
const maxLive = maxSimultaneousWindows(usedAppWindows(args.actorResults));
|
|
265
285
|
events.push({
|
|
266
286
|
id: nextEventId("concurrency"),
|
|
267
287
|
at: createdAt,
|
|
@@ -12,7 +12,8 @@ export declare function resolveActorEntryUrl(baseUrl: string, entry: string | un
|
|
|
12
12
|
* follower joins through the real UI (a direct /lobby/CODE visit does not auto-join a non-member). */
|
|
13
13
|
export declare function withLobbyCodeMission(spec: DesktopParticipantRun, code: string): DesktopParticipantRun;
|
|
14
14
|
/** A follower blocked by an expired deadline or an ended host. It never opened a browser;
|
|
15
|
-
* keep the actual reason rather than turning every upstream failure into a timeout.
|
|
15
|
+
* keep the actual reason rather than turning every upstream failure into a timeout. The skip
|
|
16
|
+
* reason is a sentence in the form the computer-use skips use, since run lines and gaps print it. */
|
|
16
17
|
export declare function makeBlockedFollowerOutcome(spec: DesktopParticipantRun, reason: string, timedOut: boolean): ParticipantRunOutcome;
|
|
17
18
|
/**
|
|
18
19
|
* A live run publishes an in-progress bundle before its participants start, whether or not an
|
|
@@ -63,7 +63,8 @@ export function withLobbyCodeMission(spec, code) {
|
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
65
|
/** A follower blocked by an expired deadline or an ended host. It never opened a browser;
|
|
66
|
-
* keep the actual reason rather than turning every upstream failure into a timeout.
|
|
66
|
+
* keep the actual reason rather than turning every upstream failure into a timeout. The skip
|
|
67
|
+
* reason is a sentence in the form the computer-use skips use, since run lines and gaps print it. */
|
|
67
68
|
export function makeBlockedFollowerOutcome(spec, reason, timedOut) {
|
|
68
69
|
return {
|
|
69
70
|
spec,
|
|
@@ -77,7 +78,9 @@ export function makeBlockedFollowerOutcome(spec, reason, timedOut) {
|
|
|
77
78
|
noEngagement: true,
|
|
78
79
|
selfReportedBlocker: false,
|
|
79
80
|
harnessError: false,
|
|
80
|
-
skippedReason: timedOut
|
|
81
|
+
skippedReason: timedOut
|
|
82
|
+
? "skipped: the host did not reach a lobby within the handoff deadline; no desktop was created"
|
|
83
|
+
: "skipped: the host ended before it reached a lobby; no desktop was created",
|
|
81
84
|
};
|
|
82
85
|
}
|
|
83
86
|
/**
|
|
@@ -8,7 +8,7 @@ import { validatePreparedRunArtifactPaths } from "../../run/paths.js";
|
|
|
8
8
|
import { participantExecutionFailures, participantFactsOf, } from "../computer-use/participant-facts.js";
|
|
9
9
|
import { participantCapWarning } from "../computer-use/participant-model.js";
|
|
10
10
|
import { resolveSubjectState } from "../computer-use/subject-projection.js";
|
|
11
|
-
import { actorRunPassed, buildConcurrentSharedWorldBundle, judgeSharedWorldRun, maxSimultaneousWindows, } from "./bundle.js";
|
|
11
|
+
import { actorRunPassed, buildConcurrentSharedWorldBundle, judgeSharedWorldRun, maxSimultaneousWindows, usedAppWindows, } from "./bundle.js";
|
|
12
12
|
import { planeStateOf } from "./plan.js";
|
|
13
13
|
import { buildSubjectProvenance, hostOriginDigest } from "./provenance.js";
|
|
14
14
|
import {} from "./types.js";
|
|
@@ -47,9 +47,16 @@ function concurrentParticipantResults(actorSpecs, actorResults, dryRun) {
|
|
|
47
47
|
}
|
|
48
48
|
const session = result.outcome.session;
|
|
49
49
|
const thisOk = participantOk(result);
|
|
50
|
+
const { skippedReason } = result.outcome;
|
|
51
|
+
// The same judged record run.json writes, so the run line and the bundle name one status.
|
|
52
|
+
const record = judgeParticipantRecords([participantFactsOf(result.outcome)], {
|
|
53
|
+
sessionLabel: "Participant",
|
|
54
|
+
missingSessionMessage: "Actor did not produce a terminal session.",
|
|
55
|
+
}).participants[0];
|
|
50
56
|
return {
|
|
51
57
|
...base,
|
|
52
|
-
status:
|
|
58
|
+
status: record.status,
|
|
59
|
+
...(skippedReason === undefined ? {} : { skippedReason }),
|
|
53
60
|
ok: thisOk,
|
|
54
61
|
window: { startedAt: result.startedAt, endedAt: result.endedAt },
|
|
55
62
|
...(session
|
|
@@ -70,10 +77,7 @@ function concurrentParticipantResults(actorSpecs, actorResults, dryRun) {
|
|
|
70
77
|
: {
|
|
71
78
|
error: {
|
|
72
79
|
code: "HUMANISH_SHARED_WORLD_FAILED",
|
|
73
|
-
message:
|
|
74
|
-
sessionLabel: "Participant",
|
|
75
|
-
missingSessionMessage: "Actor did not produce a terminal session.",
|
|
76
|
-
}).participants[0].notPassedMessage,
|
|
80
|
+
message: record.notPassedMessage,
|
|
77
81
|
},
|
|
78
82
|
}),
|
|
79
83
|
};
|
|
@@ -306,7 +310,9 @@ export async function finishConcurrentRun(ctx, live, results, plane) {
|
|
|
306
310
|
? {}
|
|
307
311
|
: { subjectSandbox: { sandboxId: subjectSandboxId, killed: subjectKilled } }),
|
|
308
312
|
...(dryRun ? {} : { overlapProven }),
|
|
309
|
-
...(dryRun
|
|
313
|
+
...(dryRun
|
|
314
|
+
? {}
|
|
315
|
+
: { maxSimultaneousLanes: maxSimultaneousWindows(usedAppWindows(actorResults)) }),
|
|
310
316
|
subject,
|
|
311
317
|
roles: participantResults,
|
|
312
318
|
observer,
|
|
@@ -94,6 +94,8 @@ export interface ConcurrentSharedWorldParticipantResult {
|
|
|
94
94
|
sandboxIdDigest?: string;
|
|
95
95
|
killed: boolean;
|
|
96
96
|
};
|
|
97
|
+
/** Set when the run skipped the participant before its session started (status `blocked`). */
|
|
98
|
+
skippedReason?: string;
|
|
97
99
|
error?: {
|
|
98
100
|
code: ConcurrentSharedWorldStudyErrorCode;
|
|
99
101
|
message: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type ReviewSummary, type RunAdapterScore, type
|
|
1
|
+
import { type ReviewSummary, type RunAdapterScore, type StoredRunBundle, type RunOutcome } from "./bundle.js";
|
|
2
2
|
import { type StoredCleanupResult, type RunPointer } from "./results.js";
|
|
3
|
-
export declare function isRunBundle(value: unknown): value is
|
|
3
|
+
export declare function isRunBundle(value: unknown): value is StoredRunBundle;
|
|
4
4
|
/** run.json's `outcome`: a finished run's ok and execution outcome, or the signal that stopped it. */
|
|
5
5
|
export declare function isRunOutcome(value: unknown): value is RunOutcome;
|
|
6
6
|
export declare function isCleanupResult(value: unknown): value is StoredCleanupResult;
|
package/dist/run/bundle.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { ExecutionOutcome } from "./judge.js";
|
|
|
4
4
|
import type { SharedWorldEvidence } from "./shared-world-evidence.js";
|
|
5
5
|
import { type RunStudyProvenance } from "./study-provenance.js";
|
|
6
6
|
import type { RunInterruptSignal } from "./status.js";
|
|
7
|
-
import type { RunSimulationStatus, RunStream, RunStreamKind } from "./streams.js";
|
|
7
|
+
import type { RunSimulationStatus, RunStream, RunStreamKind, StoredRunStream } from "./streams.js";
|
|
8
8
|
/**
|
|
9
9
|
* The run bundle version this release writes: each stream's trace, and each running participant's
|
|
10
10
|
* items so far, are files run.json indexes, and each participant's per-request lists are only in
|
|
@@ -388,6 +388,13 @@ export interface RunBundle {
|
|
|
388
388
|
*/
|
|
389
389
|
cost?: RunCostSummary;
|
|
390
390
|
}
|
|
391
|
+
/**
|
|
392
|
+
* run.json as a reader parses it, in any version: each stream a StoredRunStream, without the
|
|
393
|
+
* per-request lists that from v4 only its trace file holds. loadStreamTraces gives the RunBundle.
|
|
394
|
+
*/
|
|
395
|
+
export type StoredRunBundle = Omit<RunBundle, "streams"> & {
|
|
396
|
+
streams: StoredRunStream[];
|
|
397
|
+
};
|
|
391
398
|
/**
|
|
392
399
|
* How a run ended. `finished`: Run.finish published the final bundle with the route result's `ok`
|
|
393
400
|
* and its execution outcome (`judgeExecution` in src/run/judge.ts). `interrupted`: the CLI's signal
|
package/dist/run/detail.js
CHANGED
|
@@ -119,7 +119,10 @@ function costOf(trace, rowsLoaded) {
|
|
|
119
119
|
* items run.json or a file supplied for it.
|
|
120
120
|
*/
|
|
121
121
|
async function displayedStreams(runPaths, streams, bundle) {
|
|
122
|
-
const loaded = await loadStreamTraces(runPaths, {
|
|
122
|
+
const loaded = await loadStreamTraces(runPaths, {
|
|
123
|
+
...bundle,
|
|
124
|
+
streams,
|
|
125
|
+
});
|
|
123
126
|
return displayedTraces(loaded).streams;
|
|
124
127
|
}
|
|
125
128
|
/**
|
package/dist/run/outcomes.d.ts
CHANGED
|
@@ -53,6 +53,13 @@ interface SummarySession {
|
|
|
53
53
|
export declare function participantSummary(name: string, app: SummaryApp, session: SummarySession): string;
|
|
54
54
|
/** "; the gaps list the other 2", or "all 3" when nobody passed. Empty when the gaps list nobody. */
|
|
55
55
|
export declare function gapsListClause(listed: number, total: number): string;
|
|
56
|
+
/**
|
|
57
|
+
* The sentence that counts the participants a run skipped before their sessions started, or ""
|
|
58
|
+
* when it skipped none. A skipped participant never used the app, so a summary counts it here and
|
|
59
|
+
* leaves it out of the sentence about those that took part. `pointAtGaps` is false when that
|
|
60
|
+
* sentence already points at the gaps.
|
|
61
|
+
*/
|
|
62
|
+
export declare function skippedSentence(skipped: number, pointAtGaps: boolean): string;
|
|
56
63
|
/** One line a stakeholder can read, with the denominator attached to every number. */
|
|
57
64
|
export declare function formatParticipantOutcomes(outcomes: ParticipantOutcomes, terminalCauses?: readonly ParticipantOutcomeDetail[]): string;
|
|
58
65
|
/** Presentation details tolerate optional legacy actor payloads without changing their tallies. */
|
package/dist/run/outcomes.js
CHANGED
|
@@ -206,6 +206,20 @@ export function gapsListClause(listed, total) {
|
|
|
206
206
|
return `; the gaps list the other ${listed}`;
|
|
207
207
|
return total === 1 ? "; the gaps say why" : `; the gaps list all ${total}`;
|
|
208
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* The sentence that counts the participants a run skipped before their sessions started, or ""
|
|
211
|
+
* when it skipped none. A skipped participant never used the app, so a summary counts it here and
|
|
212
|
+
* leaves it out of the sentence about those that took part. `pointAtGaps` is false when that
|
|
213
|
+
* sentence already points at the gaps.
|
|
214
|
+
*/
|
|
215
|
+
export function skippedSentence(skipped, pointAtGaps) {
|
|
216
|
+
const why = pointAtGaps ? "; the gaps say why" : "";
|
|
217
|
+
if (skipped === 1)
|
|
218
|
+
return `1 participant was skipped before its session started${why}.`;
|
|
219
|
+
if (skipped > 1)
|
|
220
|
+
return `${skipped} participants were skipped before their sessions started${why}.`;
|
|
221
|
+
return "";
|
|
222
|
+
}
|
|
209
223
|
/** One line a stakeholder can read, with the denominator attached to every number. */
|
|
210
224
|
export function formatParticipantOutcomes(outcomes, terminalCauses = []) {
|
|
211
225
|
if (outcomes.total === 0)
|
|
@@ -21,7 +21,7 @@ export declare function participantEvent(ids: ParticipantIds, event: Omit<RunEve
|
|
|
21
21
|
/** The ids a provider resource saves for the participant whose sandbox it is. */
|
|
22
22
|
export declare function participantResourceIds(ids: ParticipantIds, participantId: string): Required<Pick<RunProviderResource, "simId" | "streamId" | "laneId">>;
|
|
23
23
|
/** The record id a saved stream belongs to. */
|
|
24
|
-
export declare function recordIdOf(stream: RunStream): string;
|
|
24
|
+
export declare function recordIdOf(stream: Pick<RunStream, "simId">): string;
|
|
25
25
|
/** The record id a saved event names, when it names one. */
|
|
26
26
|
export declare function eventRecordIdOf(event: RunEvent): string | undefined;
|
|
27
27
|
/** The participant id an adapter recorded on a saved stream (a fan-out `lane-NN` id), when it has one. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type RunSimulationStatus, type RunStream, type RunStreamKind, type StreamTraceIndex } from "./streams.js";
|
|
2
|
-
export declare function isRunStream(value: unknown): value is
|
|
1
|
+
import { type RunSimulationStatus, type RunStream, type RunStreamKind, type StoredRunStream, type StreamTraceIndex } from "./streams.js";
|
|
2
|
+
export declare function isRunStream(value: unknown): value is StoredRunStream;
|
|
3
3
|
export declare function isRunStreamArtifact(value: unknown): value is RunStream["artifacts"][number];
|
|
4
4
|
/**
|
|
5
5
|
* The trace file names the run writer produces: actor.json for one participant,
|
|
@@ -1,14 +1,23 @@
|
|
|
1
|
-
import { type RunBundle } from "./bundle.js";
|
|
1
|
+
import { type RunBundle, type StoredRunBundle } from "./bundle.js";
|
|
2
2
|
import { type PreparedOutputRoot } from "./contained-output.js";
|
|
3
3
|
import { type LiveItemsCursor } from "./live-items.js";
|
|
4
|
-
import type { LiveItemsIndex, RunStream } from "./streams.js";
|
|
5
|
-
/**
|
|
4
|
+
import type { LiveItemsIndex, RunStream, StoredRunStream, WithoutRequestLists } from "./streams.js";
|
|
5
|
+
/**
|
|
6
|
+
* A trace that does not match its index, or cannot be read. The message names the stream, and ends
|
|
7
|
+
* with `advice`: by default, restore the run folder from a copy.
|
|
8
|
+
*/
|
|
6
9
|
export declare class StreamTraceRefusedError extends Error {
|
|
7
10
|
readonly streamId: string;
|
|
8
|
-
constructor(streamId: string, detail: string);
|
|
11
|
+
constructor(streamId: string, detail: string, advice?: string);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A trace or live file whose index records more than the loader reads of one file. The loader reads
|
|
15
|
+
* no file past the limit, so a copy of the file is refused too.
|
|
16
|
+
*/
|
|
17
|
+
export declare class StreamTraceTooLargeError extends StreamTraceRefusedError {
|
|
9
18
|
}
|
|
10
19
|
/** `trace` without the per-request lists; every other field as it is. */
|
|
11
|
-
export declare function withoutRequestLists<T extends object>(trace: T): T
|
|
20
|
+
export declare function withoutRequestLists<T extends object>(trace: T): WithoutRequestLists<T>;
|
|
12
21
|
/**
|
|
13
22
|
* The run.json the run writes, and the trace files it writes first. Each stream with a finished
|
|
14
23
|
* actor keeps that actor without `items` and the per-request lists, its artifacts other than
|
|
@@ -18,7 +27,7 @@ export declare function withoutRequestLists<T extends object>(trace: T): T;
|
|
|
18
27
|
* run.json held beside it. A running participant's live actor is written by liveSummary.
|
|
19
28
|
*/
|
|
20
29
|
export declare function splitStreamTraces(bundle: RunBundle): {
|
|
21
|
-
bundle:
|
|
30
|
+
bundle: StoredRunBundle;
|
|
22
31
|
files: {
|
|
23
32
|
path: string;
|
|
24
33
|
bytes: Buffer;
|
|
@@ -39,13 +48,22 @@ export type TraceFileReader = (relativePath: string, maxBytes: number, length?:
|
|
|
39
48
|
}>;
|
|
40
49
|
/**
|
|
41
50
|
* A live file the loader joined into stream `streamId`: the index it read the items by, and the
|
|
42
|
-
* cursor it read past when the stream holds only the items after one.
|
|
51
|
+
* cursor it read past when the stream holds only the items after one. A read with `stopped` has
|
|
52
|
+
* an index that records more than the loader reads of one file, so it read nothing and joined no
|
|
53
|
+
* items past `after`, which it always has; `stopped` is the warning a reader that shows the run
|
|
54
|
+
* gives.
|
|
43
55
|
*/
|
|
44
|
-
export
|
|
56
|
+
export type LiveItemsRead = {
|
|
45
57
|
streamId: string;
|
|
46
58
|
index: LiveItemsIndex;
|
|
47
59
|
after?: LiveItemsCursor;
|
|
48
|
-
|
|
60
|
+
stopped?: undefined;
|
|
61
|
+
} | {
|
|
62
|
+
streamId: string;
|
|
63
|
+
index: LiveItemsIndex;
|
|
64
|
+
after: LiveItemsCursor;
|
|
65
|
+
stopped: string;
|
|
66
|
+
};
|
|
49
67
|
/** For a live file's index, the prefix of that file the caller holds already, if any. */
|
|
50
68
|
type LiveAfter = (index: LiveItemsIndex) => LiveItemsCursor | undefined;
|
|
51
69
|
/**
|
|
@@ -53,10 +71,11 @@ type LiveAfter = (index: LiveItemsIndex) => LiveItemsCursor | undefined;
|
|
|
53
71
|
* stream without an index is returned as it is (a v1 stream, or one with no actor). An indexed
|
|
54
72
|
* stream's file must have a trace file name (isStreamTracePath); it is read under `maxBytes` and
|
|
55
73
|
* checked against the index's byte count, sha256, item count and screenshot count, and any
|
|
56
|
-
* difference throws StreamTraceRefusedError.
|
|
74
|
+
* difference throws StreamTraceRefusedError. An index that records more than `maxBytes` throws
|
|
75
|
+
* before the file is read. The rules that need the whole bundle (an unindexed v2
|
|
57
76
|
* actor, two participants indexing one file) are loadStreamTraces'.
|
|
58
77
|
*/
|
|
59
|
-
export declare function loadStreamTrace(runPaths: PreparedOutputRoot, stream:
|
|
78
|
+
export declare function loadStreamTrace(runPaths: PreparedOutputRoot, stream: StoredRunStream, maxBytes?: number): Promise<RunStream>;
|
|
60
79
|
/**
|
|
61
80
|
* Whether a run.json of `schema` holds the per-request lists (REQUEST_LISTS) the writer kept in
|
|
62
81
|
* it: v1 to v3, and a schema WRITER_RULES does not name, which is read as v1. From v4 they are
|
|
@@ -80,14 +99,16 @@ interface LoadedTraces {
|
|
|
80
99
|
* `liveAfter` names, for a live file's index, the prefix the caller holds already (the Observer
|
|
81
100
|
* feed's cursor). The loader then reads and joins only the items past it, checked by liveItemsAfter
|
|
82
101
|
* without the indexed sha256, and `live` records the cursor; a cursor past the index or off the
|
|
83
|
-
* file's lines gets the whole prefix, checked as always.
|
|
102
|
+
* file's lines gets the whole prefix, checked as always. A live file whose index records more than
|
|
103
|
+
* `maxBytes` is not read: for a cursor that fits the index the stream gets no items past it and
|
|
104
|
+
* `live` records `stopped`, and without one the stream is refused.
|
|
84
105
|
*
|
|
85
106
|
* A live file that cannot be read may have been removed by the writer after `bundle` was read: it
|
|
86
107
|
* removes a superseded generation once run.json names the next one, and every live file once the
|
|
87
108
|
* run finished. The loader then reads run.json again, once, through the same reader, and loads
|
|
88
109
|
* that bundle instead when it is a run bundle of the same run.
|
|
89
110
|
*/
|
|
90
|
-
export declare function loadStreamTraces(source: PreparedOutputRoot | TraceFileReader, bundle:
|
|
111
|
+
export declare function loadStreamTraces(source: PreparedOutputRoot | TraceFileReader, bundle: StoredRunBundle, options?: {
|
|
91
112
|
maxBytes?: number;
|
|
92
113
|
onTrace?: (path: string, bytes: Buffer) => void;
|
|
93
114
|
liveAfter?: LiveAfter;
|
|
@@ -97,12 +118,22 @@ export declare function loadStreamTraces(source: PreparedOutputRoot | TraceFileR
|
|
|
97
118
|
* summary, shows none of the items or per-request lists run.json or a file supplied for it (its
|
|
98
119
|
* actor's items and lists, its in-progress items, its screenshot entries), and carries
|
|
99
120
|
* `traceUnavailable` with the reason; a warn event on its timeline says the same. A reader that
|
|
100
|
-
* shows the run then never prices or checks rows the loader refused.
|
|
121
|
+
* shows the run then never prices or checks rows the loader refused. A live file the loader
|
|
122
|
+
* stopped reading (LiveItemsRead's `stopped`) gives its stream a `live-items.not-read` warn event.
|
|
101
123
|
*/
|
|
102
124
|
export declare function displayedTraces(loaded: {
|
|
103
125
|
bundle: RunBundle;
|
|
104
126
|
refused: readonly StreamTraceRefusedError[];
|
|
127
|
+
live?: readonly LiveItemsRead[];
|
|
105
128
|
}): RunBundle;
|
|
129
|
+
/**
|
|
130
|
+
* What `reduce` returns for each stream of the bundle, loaded as loadRunTraces loads it, in stream
|
|
131
|
+
* order; the first refusal throws. Streams are loaded one at a time and nothing of a stream's files
|
|
132
|
+
* is kept past its `reduce` call, so a caller that keeps part of each trace holds one trace at a
|
|
133
|
+
* time where loadRunTraces holds them all. Unlike loadStreamTraces it does not read run.json again
|
|
134
|
+
* when a live file cannot be read, so it suits a run whose participants have finished.
|
|
135
|
+
*/
|
|
136
|
+
export declare function reduceRunTraces<T>(runPaths: PreparedOutputRoot, bundle: StoredRunBundle, reduce: (stream: RunStream, index: number) => T): Promise<T[]>;
|
|
106
137
|
/** The bundle with every stream loaded; the first refusal throws. */
|
|
107
|
-
export declare function loadRunTraces(source: PreparedOutputRoot | TraceFileReader, bundle:
|
|
138
|
+
export declare function loadRunTraces(source: PreparedOutputRoot | TraceFileReader, bundle: StoredRunBundle, maxBytes?: number): Promise<RunBundle>;
|
|
108
139
|
export {};
|