humanish 0.129.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.
@@ -0,0 +1,28 @@
1
+ import { type StoredRunBundle } from "../run/bundle.js";
2
+ import { type PreparedRunArtifactPaths } from "../run/paths.js";
3
+ import type { RunStream } from "../run/streams.js";
4
+ /**
5
+ * A refusal of the run's recorded source. Its message is the code, as every analysis error's is,
6
+ * and `reason` says which file analysis did not read and why, for the person who asked.
7
+ */
8
+ export declare class AnalysisSourceRefusedError extends Error {
9
+ readonly reason: string;
10
+ constructor(code: "ANALYSIS_SOURCE_TOO_LARGE" | "ANALYSIS_SOURCE_INVALID", reason: string);
11
+ }
12
+ /** The refusal of a run.json of `size` bytes, more than the `max` analysis reads of it. */
13
+ export declare function runFileTooLarge(size: number | bigint, max: number): AnalysisSourceRefusedError;
14
+ /**
15
+ * run.json's bundle, when it is a run bundle of the prepared run that analysis reads: at most
16
+ * EVIDENCE_LIMITS.sourceBytes, at most EVIDENCE_LIMITS.participants participants, and each
17
+ * participant and event with the fields analysis reads.
18
+ */
19
+ export declare function parseAnalysisSource(prepared: PreparedRunArtifactPaths, bytes: Buffer): StoredRunBundle;
20
+ /**
21
+ * What `keep` takes from each stream of run.json's bundle with the stream's trace loaded, in stream
22
+ * order. Each trace file is read up to RUN_ARTIFACT_MAX_BYTES, as the loader, verify and the
23
+ * Observer read it, and is dropped once `keep` returns, so analysis holds one trace at a time
24
+ * whatever the number of participants. run.json's digest, which the analysis record binds, binds
25
+ * each trace's sha256. A trace whose index records more than the limit is refused as
26
+ * ANALYSIS_SOURCE_TOO_LARGE, and any other trace the loader refuses as ANALYSIS_SOURCE_INVALID.
27
+ */
28
+ export declare function readParticipantTraces<T>(prepared: PreparedRunArtifactPaths, bundle: StoredRunBundle, keep: (stream: RunStream, index: number) => T): Promise<T[]>;
@@ -0,0 +1,118 @@
1
+ // How analysis reads a run's recorded source: run.json, then each participant's trace one at a time,
2
+ // and the refusals it gives for either.
3
+ import { isCommsReceivingEvidence } from "../comms/receiving-evidence.js";
4
+ import { isReadRunBundleSchema } from "../run/bundle.js";
5
+ import { runIdOf } from "../run/paths.js";
6
+ import { reduceRunTraces, StreamTraceRefusedError, StreamTraceTooLargeError, } from "../run/stream-trace.js";
7
+ import { isRecord } from "../run/type-guards.js";
8
+ import { EVIDENCE_LIMITS } from "./analysis-limits.js";
9
+ /**
10
+ * A refusal of the run's recorded source. Its message is the code, as every analysis error's is,
11
+ * and `reason` says which file analysis did not read and why, for the person who asked.
12
+ */
13
+ export class AnalysisSourceRefusedError extends Error {
14
+ reason;
15
+ constructor(code, reason) {
16
+ super(code);
17
+ this.reason = reason;
18
+ this.name = "AnalysisSourceRefusedError";
19
+ }
20
+ }
21
+ const bytes = (count) => `${count.toLocaleString("en-US")} bytes`;
22
+ const limit = (max) => `${bytes(max)} (${(max / (1024 * 1024)).toLocaleString("en-US", { maximumFractionDigits: 1 })} MiB)`;
23
+ const runFileInvalid = (reason) => new AnalysisSourceRefusedError("ANALYSIS_SOURCE_INVALID", `Analysis did not read run.json: ${reason}`);
24
+ const traceInvalid = (streamId, reason) => new AnalysisSourceRefusedError("ANALYSIS_SOURCE_INVALID", `Analysis did not read participant ${streamId}'s trace: ${reason}`);
25
+ /** The refusal of a run.json of `size` bytes, more than the `max` analysis reads of it. */
26
+ export function runFileTooLarge(size, max) {
27
+ return new AnalysisSourceRefusedError("ANALYSIS_SOURCE_TOO_LARGE", `Analysis did not read run.json: it holds ${bytes(size)}, more than the ${limit(max)} analysis reads of it. run.json grows with each participant and the study's mission; split the participants across studies to keep it under the limit.`);
28
+ }
29
+ /**
30
+ * run.json's bundle, when it is a run bundle of the prepared run that analysis reads: at most
31
+ * EVIDENCE_LIMITS.sourceBytes, at most EVIDENCE_LIMITS.participants participants, and each
32
+ * participant and event with the fields analysis reads.
33
+ */
34
+ export function parseAnalysisSource(prepared, bytes) {
35
+ if (bytes.length > EVIDENCE_LIMITS.sourceBytes)
36
+ throw runFileTooLarge(bytes.length, EVIDENCE_LIMITS.sourceBytes);
37
+ const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
38
+ if (!isRecord(value) || !isReadRunBundleSchema(value.schema))
39
+ throw runFileInvalid("it is not a run bundle of a schema this version of humanish reads.");
40
+ if (value.runId !== runIdOf(prepared))
41
+ throw runFileInvalid(`it records another run than ${runIdOf(prepared)}, the run in its folder.`);
42
+ if (!Array.isArray(value.streams) || !Array.isArray(value.events))
43
+ throw runFileInvalid("it has no participants or events list.");
44
+ if (value.streams.length > EVIDENCE_LIMITS.participants)
45
+ throw runFileInvalid(`it lists ${value.streams.length} participants, more than the ${EVIDENCE_LIMITS.participants} analysis reads; split the participants across studies.`);
46
+ if (value.commsReceiving !== undefined && !isCommsReceivingEvidence(value.commsReceiving))
47
+ throw runFileInvalid("its email receiving evidence holds a field or value analysis does not read.");
48
+ const ids = new Set();
49
+ for (const [index, stream] of value.streams.entries()) {
50
+ if (!isRecord(stream) ||
51
+ typeof stream.id !== "string" ||
52
+ stream.id.length === 0 ||
53
+ stream.id.length > 256 ||
54
+ ids.has(stream.id) ||
55
+ typeof stream.label !== "string" ||
56
+ typeof stream.status !== "string") {
57
+ throw runFileInvalid(`participant ${index + 1} needs an id of 1 to 256 characters no other participant has, a label and a status.`);
58
+ }
59
+ ids.add(stream.id);
60
+ if (stream.actor !== undefined && !isRecord(stream.actor))
61
+ throw runFileInvalid(`participant ${stream.id}'s actor is not an object.`);
62
+ }
63
+ for (const [index, event] of value.events.entries()) {
64
+ if (!isRecord(event) ||
65
+ typeof event.id !== "string" ||
66
+ typeof event.message !== "string" ||
67
+ typeof event.type !== "string")
68
+ throw runFileInvalid(`event ${index + 1} needs an id, a type and a message.`);
69
+ }
70
+ return value;
71
+ }
72
+ /**
73
+ * What `keep` takes from each stream of run.json's bundle with the stream's trace loaded, in stream
74
+ * order. Each trace file is read up to RUN_ARTIFACT_MAX_BYTES, as the loader, verify and the
75
+ * Observer read it, and is dropped once `keep` returns, so analysis holds one trace at a time
76
+ * whatever the number of participants. run.json's digest, which the analysis record binds, binds
77
+ * each trace's sha256. A trace whose index records more than the limit is refused as
78
+ * ANALYSIS_SOURCE_TOO_LARGE, and any other trace the loader refuses as ANALYSIS_SOURCE_INVALID.
79
+ */
80
+ export async function readParticipantTraces(prepared, bundle, keep) {
81
+ try {
82
+ return await reduceRunTraces(prepared, bundle, (stream, index) => {
83
+ validateTraceItems(stream);
84
+ return keep(stream, index);
85
+ });
86
+ }
87
+ catch (error) {
88
+ // The loader's refusal names the participant, the file, why, and what to do.
89
+ if (error instanceof StreamTraceRefusedError)
90
+ throw new AnalysisSourceRefusedError(error instanceof StreamTraceTooLargeError
91
+ ? "ANALYSIS_SOURCE_TOO_LARGE"
92
+ : "ANALYSIS_SOURCE_INVALID", error.message);
93
+ throw error;
94
+ }
95
+ }
96
+ function validateTraceItems(stream) {
97
+ const actor = stream.actor;
98
+ if (actor === undefined)
99
+ return;
100
+ if (!isRecord(actor) || !Array.isArray(actor.items))
101
+ throw traceInvalid(stream.id, "it has no items list.");
102
+ // The place each id was first seen. A refusal names places, since an id is trace text.
103
+ const firstPlace = new Map();
104
+ for (const [index, item] of actor.items.entries()) {
105
+ if (!isRecord(item) ||
106
+ typeof item.id !== "string" ||
107
+ item.id.length === 0 ||
108
+ item.id.length > 256 ||
109
+ typeof item.kind !== "string" ||
110
+ typeof item.title !== "string" ||
111
+ (item.text !== undefined && typeof item.text !== "string"))
112
+ throw traceInvalid(stream.id, `item ${index + 1} needs an id of 1 to 256 characters, a kind and a title, and a text that is a string if it has one.`);
113
+ const first = firstPlace.get(item.id);
114
+ if (first !== undefined)
115
+ throw traceInvalid(stream.id, `items ${first + 1} and ${index + 1} share an id, and analysis cites each item by its id.`);
116
+ firstPlace.set(item.id, index);
117
+ }
118
+ }
@@ -17,7 +17,6 @@ import { buildObserverData, OBSERVER_DATA_SCHEMA, observerDataFromSaved, } from
17
17
  import { resolveRunPath } from "../run/locate.js";
18
18
  import { loadRunTraces, StreamTraceRefusedError } from "../run/stream-trace.js";
19
19
  import { verifyRun } from "../verify/verify.js";
20
- import {} from "../run/bundle.js";
21
20
  import { exportRedactedBundle } from "./export-bundle.js";
22
21
  import { loadAnalysis } from "../analysis/load.js";
23
22
  import { readNotesForSharing } from "../run/note-files.js";
@@ -1,7 +1,7 @@
1
1
  import { type ActorEnding } from "../actors/stop-cause.js";
2
2
  import type { RunBundle, RunCostSummary, RunEvent, RunSimulation } from "../run/bundle.js";
3
3
  import { type RunDisplay } from "../run/display.js";
4
- import type { RunStream, RunStreamKind } from "../run/streams.js";
4
+ import type { RunStream, RunStreamKind, StoredRunStream } from "../run/streams.js";
5
5
  /**
6
6
  * The Observer's data. From v2 observer-data.json leaves out each participant's items, and from v3
7
7
  * each participant's per-request lists (REQUEST_LISTS in src/run/stream-trace.ts), which no part
@@ -96,7 +96,7 @@ export interface ObserverData {
96
96
  artifactRoot: string;
97
97
  };
98
98
  }
99
- export interface ObserverStream extends RunStream {
99
+ export interface ObserverStream extends StoredRunStream {
100
100
  /** Only the attached server may grant provider-origin access for a live desktop iframe. */
101
101
  embed?: NonNullable<RunStream["embed"]> & {
102
102
  runtimeDesktop?: true;