humanish 0.127.1 → 0.129.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 +26 -0
- package/CONTEXT.md +2 -2
- package/README.md +1 -1
- package/dist/actors/computer-use/loop/output-limit.js +3 -0
- package/dist/actors/computer-use/loop/types.d.ts +16 -10
- package/dist/actors/computer-use/loop.js +6 -1
- package/dist/analysis/evidence.js +1 -4
- package/dist/analysis/validation.js +1 -1
- package/dist/cli/commands/study-format.js +2 -1
- package/dist/feedback/export.js +3 -2
- package/dist/observer/data.d.ts +13 -6
- package/dist/observer/data.js +40 -10
- 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 +8 -8
- package/dist/routes/computer-use/fanout-bundle.js +4 -7
- package/dist/routes/computer-use/participant-model.js +1 -1
- 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.d.ts +7 -4
- package/dist/run/bundle.js +10 -4
- package/dist/run/detail.js +8 -2
- package/dist/run/locate.d.ts +2 -2
- package/dist/run/locate.js +2 -2
- package/dist/run/outcomes.d.ts +7 -0
- package/dist/run/outcomes.js +14 -0
- package/dist/run/run.js +11 -3
- package/dist/run/stream-trace.d.ts +41 -14
- package/dist/run/stream-trace.js +195 -42
- package/dist/run/streams.d.ts +4 -1
- package/dist/study/bundle-estimate.js +17 -17
- package/dist/verify/costs.d.ts +7 -3
- package/dist/verify/costs.js +27 -12
- package/dist/verify/verify.js +14 -8
- package/docs/architecture/github-feedback-loop.md +1 -1
- package/docs/architecture/observer.md +32 -10
- package/docs/contracts/README.md +1 -1
- package/docs/contracts/run-bundle.md +102 -18
- package/docs/contracts/schemas.md +55 -45
- package/docs/contracts/study-analysis.md +1 -1
- package/docs/decisions/0002-observer-is-one-self-contained-file.md +1 -1
- package/package.json +1 -1
package/dist/run/stream-trace.js
CHANGED
|
@@ -1,25 +1,37 @@
|
|
|
1
1
|
// Where a participant's trace lives. A v1 run.json holds every stream's trace items and screenshot
|
|
2
2
|
// entries inline. From v2 the run writer moves them into one file per stream and leaves an index
|
|
3
3
|
// in run.json, so run.json stays a summary whatever the length of the sessions. From v3 a running
|
|
4
|
-
// participant's items so far are in a live file run.json indexes too (src/run/live-items.ts).
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// participant's items so far are in a live file run.json indexes too (src/run/live-items.ts). From
|
|
5
|
+
// v4 the per-request lists (REQUEST_LISTS) are only in the trace file, and a running participant
|
|
6
|
+
// carries its totals and the estimate priced from its rows. Every reader that needs the items or
|
|
7
|
+
// the lists loads them here, which accepts every version and checks each file against its index.
|
|
7
8
|
import { createHash } from "node:crypto";
|
|
8
|
-
import { RUN_BUNDLE_FILE, RUN_BUNDLE_SCHEMA, RUN_BUNDLE_V2_SCHEMA, } from "./bundle.js";
|
|
9
|
+
import { RUN_BUNDLE_FILE, RUN_BUNDLE_SCHEMA, RUN_BUNDLE_V2_SCHEMA, RUN_BUNDLE_V3_SCHEMA, } from "./bundle.js";
|
|
9
10
|
import { isRunBundle } from "./bundle-shape.js";
|
|
10
11
|
import { readContainedPrefix, readContainedRegularFile, refusalText, RUN_ARTIFACT_MAX_BYTES, } from "./contained-output.js";
|
|
11
12
|
import { isLiveItemsIndex, LIVE_ACTOR_SCHEMA, LIVE_ACTOR_V1_SCHEMA, liveItemsAfter, liveItemsFromBytes, } from "./live-items.js";
|
|
13
|
+
import { estimateActorCostForExecution } from "./pricing.js";
|
|
12
14
|
import { isRunStreamArtifact, isStreamTraceIndex, isStreamTracePath } from "./stream-shape.js";
|
|
13
15
|
import { isRecord } from "./type-guards.js";
|
|
14
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* A trace that does not match its index, or cannot be read. The message names the stream, and ends
|
|
18
|
+
* with `advice`: by default, restore the run folder from a copy.
|
|
19
|
+
*/
|
|
15
20
|
export class StreamTraceRefusedError extends Error {
|
|
16
21
|
streamId;
|
|
17
|
-
constructor(streamId, detail) {
|
|
18
|
-
super(`Participant ${streamId}'s trace was not read: ${detail}.
|
|
22
|
+
constructor(streamId, detail, advice = "A trace file must stay as the run wrote it; restore the run folder from a copy.") {
|
|
23
|
+
super(`Participant ${streamId}'s trace was not read: ${detail}. ${advice}`);
|
|
19
24
|
this.streamId = streamId;
|
|
20
25
|
this.name = "StreamTraceRefusedError";
|
|
21
26
|
}
|
|
22
27
|
}
|
|
28
|
+
/** Why the loader reads no trace or live file whose index records more than `maxBytes`. */
|
|
29
|
+
const pastLimitDetail = (index, maxBytes) => `run.json records ${index.path} at ${index.bytes} bytes, more than the ${maxBytes} bytes humanish reads of one file`;
|
|
30
|
+
/**
|
|
31
|
+
* The refusal of a trace or live file whose index records more than `maxBytes`. The loader reads
|
|
32
|
+
* no file past the limit, so a copy of the file is refused too.
|
|
33
|
+
*/
|
|
34
|
+
const tooLarge = (streamId, index, maxBytes) => new StreamTraceRefusedError(streamId, pastLimitDetail(index, maxBytes), "Restoring a copy does not help; open the file with another tool, such as jq, to read its items.");
|
|
23
35
|
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
24
36
|
const isScreenshot = (artifact) => artifact.kind === "screenshot";
|
|
25
37
|
/**
|
|
@@ -43,12 +55,64 @@ function tracePath(stream, index, claimed) {
|
|
|
43
55
|
!POSITION_TRACE_FILE.test(artifact.path))?.path;
|
|
44
56
|
return own !== undefined && !claimed.has(traceFileKey(own)) ? own : `actors/${index + 1}.json`;
|
|
45
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* A participant's lists that gain one entry per model request, as paths into its actor trace. From
|
|
60
|
+
* run bundle v4 run.json leaves them out of `actor` and `liveActor`, and the trace file alone holds
|
|
61
|
+
* them; the Observer's data leaves them out too.
|
|
62
|
+
*/
|
|
63
|
+
const REQUEST_LISTS = ["tokenUsage.turns", "conversation.requests", "providerRequests"];
|
|
64
|
+
/** The value at `list`'s path in `trace`, when the object holding it is there. */
|
|
65
|
+
function listIn(trace, list) {
|
|
66
|
+
const [first, second] = list.split(".");
|
|
67
|
+
if (second === undefined)
|
|
68
|
+
return trace[first];
|
|
69
|
+
const parent = trace[first];
|
|
70
|
+
return isRecord(parent) ? parent[second] : undefined;
|
|
71
|
+
}
|
|
72
|
+
/** `trace` without the per-request lists; every other field as it is. */
|
|
73
|
+
export function withoutRequestLists(trace) {
|
|
74
|
+
const copy = { ...trace };
|
|
75
|
+
for (const list of REQUEST_LISTS) {
|
|
76
|
+
const [first, second] = list.split(".");
|
|
77
|
+
const parent = copy[first];
|
|
78
|
+
if (second === undefined)
|
|
79
|
+
delete copy[first];
|
|
80
|
+
else if (isRecord(parent) && second in parent) {
|
|
81
|
+
const { [second]: _list, ...rest } = parent;
|
|
82
|
+
copy[first] = rest;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return copy;
|
|
86
|
+
}
|
|
87
|
+
/** The first per-request list `trace` holds, if any. */
|
|
88
|
+
function heldRequestList(trace) {
|
|
89
|
+
return REQUEST_LISTS.find((list) => listIn(trace, list) !== undefined);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* run.json's live actor: without the per-request lists, and with the estimate priced from the
|
|
93
|
+
* rows it leaves out. Long-context pricing needs each request's input size, so a figure priced
|
|
94
|
+
* from the totals alone could be under half the real one. A live actor read back from run.json
|
|
95
|
+
* has no rows and keeps the estimate they gave.
|
|
96
|
+
*/
|
|
97
|
+
function liveSummary(live) {
|
|
98
|
+
const summary = withoutRequestLists(live);
|
|
99
|
+
const model = live.ids?.model;
|
|
100
|
+
if (live.tokenUsage === undefined || typeof model !== "string")
|
|
101
|
+
return summary;
|
|
102
|
+
if (live.tokenUsage.turns === undefined && live.estimatedCost !== undefined)
|
|
103
|
+
return summary;
|
|
104
|
+
return {
|
|
105
|
+
...summary,
|
|
106
|
+
estimatedCost: estimateActorCostForExecution(live.tokenUsage, model, live.executionProfile),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
46
109
|
/**
|
|
47
110
|
* The run.json the run writes, and the trace files it writes first. Each stream with a finished
|
|
48
|
-
* actor keeps that actor without `items
|
|
49
|
-
* trace file; the file is the actor trace with the stream's
|
|
50
|
-
* stream already split (its actor read back from
|
|
51
|
-
* since its file is on disk
|
|
111
|
+
* actor keeps that actor without `items` and the per-request lists, its artifacts other than
|
|
112
|
+
* screenshots, and an index of its trace file; the file is the whole actor trace with the stream's
|
|
113
|
+
* screenshot entries as `artifacts`. A stream already split (its actor read back from run.json
|
|
114
|
+
* without its items) keeps its index, since its file is on disk, and loses any lists an older
|
|
115
|
+
* run.json held beside it. A running participant's live actor is written by liveSummary.
|
|
52
116
|
*/
|
|
53
117
|
export function splitStreamTraces(bundle) {
|
|
54
118
|
const files = [];
|
|
@@ -56,10 +120,15 @@ export function splitStreamTraces(bundle) {
|
|
|
56
120
|
for (const stream of bundle.streams)
|
|
57
121
|
if (stream.trace)
|
|
58
122
|
claimed.add(traceFileKey(stream.trace.path));
|
|
59
|
-
const streams = bundle.streams.map((
|
|
123
|
+
const streams = bundle.streams.map((written, index) => {
|
|
124
|
+
const stream = written.liveActor === undefined
|
|
125
|
+
? written
|
|
126
|
+
: { ...written, liveActor: liveSummary(written.liveActor) };
|
|
60
127
|
const actor = stream.actor;
|
|
61
|
-
if (actor === undefined
|
|
128
|
+
if (actor === undefined)
|
|
62
129
|
return stream;
|
|
130
|
+
if (!Array.isArray(actor.items))
|
|
131
|
+
return { ...stream, actor: withoutRequestLists(actor) };
|
|
63
132
|
const { trace: _previous, ...rest } = stream;
|
|
64
133
|
const filePath = tracePath(rest, index, claimed);
|
|
65
134
|
// Only a stream already split that moved position can hold this stream's position name.
|
|
@@ -72,8 +141,8 @@ export function splitStreamTraces(bundle) {
|
|
|
72
141
|
const { items, ...summary } = actor;
|
|
73
142
|
return {
|
|
74
143
|
...rest,
|
|
75
|
-
// run.json's actor is the summary: every field of the trace except its items.
|
|
76
|
-
actor: summary,
|
|
144
|
+
// run.json's actor is the summary: every field of the trace except its items and lists.
|
|
145
|
+
actor: withoutRequestLists(summary),
|
|
77
146
|
artifacts: stream.artifacts.filter((artifact) => !isScreenshot(artifact)),
|
|
78
147
|
trace: {
|
|
79
148
|
path: filePath,
|
|
@@ -125,13 +194,38 @@ function joinTrace(stream, bytes) {
|
|
|
125
194
|
refuse(`${index.path} lists an artifact that is not a screenshot entry`);
|
|
126
195
|
if (value.items.length !== index.items || artifacts.length !== index.screenshots)
|
|
127
196
|
refuse(`${index.path} holds ${value.items.length} items and ${artifacts.length} screenshots and run.json records ${index.items} and ${index.screenshots}`);
|
|
197
|
+
const actor = joinRequestLists(stream.actor, value, index.path, refuse);
|
|
128
198
|
const { trace: _index, ...rest } = stream;
|
|
129
199
|
return {
|
|
130
200
|
...rest,
|
|
131
|
-
actor: { ...
|
|
201
|
+
actor: { ...actor, items: value.items },
|
|
132
202
|
artifacts: [...stream.artifacts, ...artifacts],
|
|
133
203
|
};
|
|
134
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* run.json's actor with each per-request list its trace file holds, whatever the bundle's
|
|
207
|
+
* version: before v4 the writer wrote run.json's lists and the file's from one actor, so they are
|
|
208
|
+
* equal. A nested list goes into run.json's `tokenUsage` or `conversation` when run.json has it.
|
|
209
|
+
*/
|
|
210
|
+
function joinRequestLists(summary, file, filePath, refuse) {
|
|
211
|
+
if (isRecord(file.conversation) && !Array.isArray(file.conversation.requests))
|
|
212
|
+
refuse(`${filePath} records a conversation without its requests list`);
|
|
213
|
+
const joined = { ...summary };
|
|
214
|
+
for (const list of REQUEST_LISTS) {
|
|
215
|
+
const value = listIn(file, list);
|
|
216
|
+
if (value === undefined)
|
|
217
|
+
continue;
|
|
218
|
+
if (!Array.isArray(value))
|
|
219
|
+
refuse(`${filePath} holds ${list} as something other than a list`);
|
|
220
|
+
const [first, second] = list.split(".");
|
|
221
|
+
const parent = joined[first];
|
|
222
|
+
if (second === undefined)
|
|
223
|
+
joined[first] = value;
|
|
224
|
+
else if (isRecord(parent))
|
|
225
|
+
joined[first] = { ...parent, [second]: value };
|
|
226
|
+
}
|
|
227
|
+
return joined;
|
|
228
|
+
}
|
|
135
229
|
/** Refuses an index the shape check would refuse, naming a file that is not a trace file name. */
|
|
136
230
|
function assertIndex(stream) {
|
|
137
231
|
const index = stream.trace;
|
|
@@ -166,10 +260,22 @@ async function readLiveItems(read, stream, maxBytes, liveAfter) {
|
|
|
166
260
|
return refuse("run.json records a malformed live items index");
|
|
167
261
|
if (live.items !== undefined)
|
|
168
262
|
refuse("run.json holds in-progress items beside their live file");
|
|
169
|
-
if (index.bytes > maxBytes)
|
|
170
|
-
refuse(`${index.path} is indexed at ${index.bytes} bytes, more than the ${maxBytes} read`);
|
|
171
263
|
const { itemsFile: _index, ...summary } = live;
|
|
172
264
|
const after = liveAfter?.(index);
|
|
265
|
+
if (index.bytes > maxBytes) {
|
|
266
|
+
if (after === undefined || !cursorFits(index, after))
|
|
267
|
+
throw tooLarge(stream.id, index, maxBytes);
|
|
268
|
+
// The caller keeps the prefix it holds, read when the file was under the limit.
|
|
269
|
+
return {
|
|
270
|
+
stream: { ...stream, liveActor: { ...summary, items: [] } },
|
|
271
|
+
read: {
|
|
272
|
+
streamId: stream.id,
|
|
273
|
+
index,
|
|
274
|
+
after,
|
|
275
|
+
stopped: `Participant ${stream.id}'s items past ${after.items} were not read: ${pastLimitDetail(index, maxBytes)}. The items read before stay shown; open the file with another tool, such as jq, to read the rest.`,
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
173
279
|
const tail = after === undefined ? undefined : await readLiveTail(read, stream, index, after);
|
|
174
280
|
if (tail !== undefined)
|
|
175
281
|
return {
|
|
@@ -190,6 +296,15 @@ async function readLiveItems(read, stream, maxBytes, liveAfter) {
|
|
|
190
296
|
read: { streamId: stream.id, index },
|
|
191
297
|
};
|
|
192
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* Whether `after` can be a prefix of the file `index` describes: some bytes, no more items or bytes
|
|
301
|
+
* than the index, and at its end in both counts or in neither. Whether it ends on a line end is
|
|
302
|
+
* checked only when the tail is read.
|
|
303
|
+
*/
|
|
304
|
+
const cursorFits = (index, after) => after.bytes > 0 &&
|
|
305
|
+
after.items <= index.items &&
|
|
306
|
+
after.bytes <= index.bytes &&
|
|
307
|
+
(after.items === index.items) === (after.bytes === index.bytes);
|
|
193
308
|
/**
|
|
194
309
|
* The items of the live file `index` describes past `after`, and the bytes they were read from;
|
|
195
310
|
* undefined when the cursor does not fit the index or the file's lines, so the caller reads the
|
|
@@ -197,10 +312,7 @@ async function readLiveItems(read, stream, maxBytes, liveAfter) {
|
|
|
197
312
|
* LiveFileUnreadableError, as a whole read does.
|
|
198
313
|
*/
|
|
199
314
|
async function readLiveTail(read, stream, index, after) {
|
|
200
|
-
if (after
|
|
201
|
-
after.items > index.items ||
|
|
202
|
-
after.bytes > index.bytes ||
|
|
203
|
-
(after.items === index.items) !== (after.bytes === index.bytes))
|
|
315
|
+
if (!cursorFits(index, after))
|
|
204
316
|
return undefined;
|
|
205
317
|
if (after.bytes === index.bytes)
|
|
206
318
|
return { items: [], bytes: Buffer.alloc(0) };
|
|
@@ -220,6 +332,8 @@ async function readStreamTrace(read, stream, maxBytes, liveAfter) {
|
|
|
220
332
|
let joined = stream;
|
|
221
333
|
if (stream.trace !== undefined) {
|
|
222
334
|
assertIndex(stream);
|
|
335
|
+
if (stream.trace.bytes > maxBytes)
|
|
336
|
+
throw tooLarge(stream.id, stream.trace, maxBytes);
|
|
223
337
|
const file = await read(stream.trace.path, maxBytes);
|
|
224
338
|
if ("refused" in file)
|
|
225
339
|
throw new StreamTraceRefusedError(stream.id, file.refused);
|
|
@@ -236,22 +350,37 @@ async function readStreamTrace(read, stream, maxBytes, liveAfter) {
|
|
|
236
350
|
* stream without an index is returned as it is (a v1 stream, or one with no actor). An indexed
|
|
237
351
|
* stream's file must have a trace file name (isStreamTracePath); it is read under `maxBytes` and
|
|
238
352
|
* checked against the index's byte count, sha256, item count and screenshot count, and any
|
|
239
|
-
* difference throws StreamTraceRefusedError.
|
|
353
|
+
* difference throws StreamTraceRefusedError. An index that records more than `maxBytes` throws
|
|
354
|
+
* before the file is read. The rules that need the whole bundle (an unindexed v2
|
|
240
355
|
* actor, two participants indexing one file) are loadStreamTraces'.
|
|
241
356
|
*/
|
|
242
357
|
export async function loadStreamTrace(runPaths, stream, maxBytes = RUN_ARTIFACT_MAX_BYTES) {
|
|
243
358
|
return (await readStreamTrace(containedReader(runPaths), stream, maxBytes)).stream;
|
|
244
359
|
}
|
|
360
|
+
/**
|
|
361
|
+
* What each version's writer never puts in run.json, so a run.json of that version that holds it
|
|
362
|
+
* did not come from the writer. From v2 the writer indexes every actor on every write, mid-run
|
|
363
|
+
* flushes included, and the final write replaces `liveActor`. From v3 a running participant's
|
|
364
|
+
* items are in a live file (humanish.live-actor.v2). From v4 the per-request lists are only in
|
|
365
|
+
* the trace file. A run.json of v1, or of no version this table names, is read as v1 is.
|
|
366
|
+
*/
|
|
367
|
+
const WRITER_RULES = new Map([
|
|
368
|
+
[RUN_BUNDLE_V2_SCHEMA, { liveFiles: false, listsOut: false }],
|
|
369
|
+
[RUN_BUNDLE_V3_SCHEMA, { liveFiles: true, listsOut: false }],
|
|
370
|
+
[RUN_BUNDLE_SCHEMA, { liveFiles: true, listsOut: true }],
|
|
371
|
+
]);
|
|
372
|
+
/**
|
|
373
|
+
* Whether a run.json of `schema` holds the per-request lists (REQUEST_LISTS) the writer kept in
|
|
374
|
+
* it: v1 to v3, and a schema WRITER_RULES does not name, which is read as v1. From v4 they are
|
|
375
|
+
* only in the trace file, so a running participant's `liveActor` holds none.
|
|
376
|
+
*/
|
|
377
|
+
export const keepsRequestListsInRunJson = (schema) => WRITER_RULES.get(schema)?.listsOut !== true;
|
|
245
378
|
/**
|
|
246
379
|
* Why a stream of this bundle cannot be read, before any file is. A trace file belongs to one
|
|
247
380
|
* participant, so every stream indexing a path another stream indexes, compared as a
|
|
248
|
-
* case-insensitive file system compares them, is refused.
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
* participants' items so far as `liveActor`: inline in v2 (humanish.live-actor.v1), in a live file
|
|
252
|
-
* from v3 (humanish.live-actor.v2), so a v3 run.json with inline live items did not come from the
|
|
253
|
-
* writer. A run.json whose outcome says the run finished never carries `liveActor`, since the
|
|
254
|
-
* final write replaces it.
|
|
381
|
+
* case-insensitive file system compares them, is refused in every version. The other rules are
|
|
382
|
+
* WRITER_RULES': a v2 actor without an index lost its trace file, and a run.json whose outcome says
|
|
383
|
+
* the run finished never carries `liveActor`.
|
|
255
384
|
*/
|
|
256
385
|
function bundleRefusal(bundle, stream, owners) {
|
|
257
386
|
const shared = isRecord(stream.trace)
|
|
@@ -259,16 +388,23 @@ function bundleRefusal(bundle, stream, owners) {
|
|
|
259
388
|
: undefined;
|
|
260
389
|
if (shared !== undefined && shared.length > 1)
|
|
261
390
|
return `run.json indexes ${String(stream.trace.path)} for participants ${shared.join(", ")} (paths that differ only in case name one file on some file systems), and a trace file belongs to one participant`;
|
|
262
|
-
|
|
391
|
+
const rules = WRITER_RULES.get(String(bundle.schema));
|
|
392
|
+
if (rules === undefined)
|
|
263
393
|
return undefined;
|
|
394
|
+
const version = String(bundle.schema).replace("humanish.run-bundle.", "");
|
|
264
395
|
if (stream.actor !== undefined && stream.trace === undefined)
|
|
265
396
|
return "run.json holds its actor and names no trace file";
|
|
266
397
|
if (stream.liveActor !== undefined && bundle.outcome?.state === "finished")
|
|
267
398
|
return "run.json records a finished run and still holds in-progress items";
|
|
268
|
-
if (
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
399
|
+
if (rules.liveFiles && stream.liveActor !== undefined && isInlineLiveActor(stream.liveActor))
|
|
400
|
+
return `run.json ${version} holds in-progress items inline, which the writer keeps in a live file`;
|
|
401
|
+
if (rules.listsOut)
|
|
402
|
+
for (const field of ["actor", "liveActor"]) {
|
|
403
|
+
const trace = stream[field];
|
|
404
|
+
const list = isRecord(trace) ? heldRequestList(trace) : undefined;
|
|
405
|
+
if (list !== undefined)
|
|
406
|
+
return `run.json ${version} holds ${list} in its ${field}, which the writer keeps only in the trace file`;
|
|
407
|
+
}
|
|
272
408
|
return undefined;
|
|
273
409
|
}
|
|
274
410
|
/**
|
|
@@ -282,7 +418,9 @@ function bundleRefusal(bundle, stream, owners) {
|
|
|
282
418
|
* `liveAfter` names, for a live file's index, the prefix the caller holds already (the Observer
|
|
283
419
|
* feed's cursor). The loader then reads and joins only the items past it, checked by liveItemsAfter
|
|
284
420
|
* without the indexed sha256, and `live` records the cursor; a cursor past the index or off the
|
|
285
|
-
* file's lines gets the whole prefix, checked as always.
|
|
421
|
+
* file's lines gets the whole prefix, checked as always. A live file whose index records more than
|
|
422
|
+
* `maxBytes` is not read: for a cursor that fits the index the stream gets no items past it and
|
|
423
|
+
* `live` records `stopped`, and without one the stream is refused.
|
|
286
424
|
*
|
|
287
425
|
* A live file that cannot be read may have been removed by the writer after `bundle` was read: it
|
|
288
426
|
* removes a superseded generation once run.json names the next one, and every live file once the
|
|
@@ -348,13 +486,16 @@ async function loadEveryStream(read, bundle, maxBytes, liveAfter) {
|
|
|
348
486
|
}
|
|
349
487
|
/**
|
|
350
488
|
* A loaded bundle as a reader that shows a run presents it. A refused stream keeps run.json's
|
|
351
|
-
* summary, shows none of the items run.json or a file supplied for it (its
|
|
352
|
-
* in-progress items, its screenshot entries), and carries
|
|
353
|
-
* warn event on its timeline says the same.
|
|
489
|
+
* summary, shows none of the items or per-request lists run.json or a file supplied for it (its
|
|
490
|
+
* actor's items and lists, its in-progress items, its screenshot entries), and carries
|
|
491
|
+
* `traceUnavailable` with the reason; a warn event on its timeline says the same. A reader that
|
|
492
|
+
* shows the run then never prices or checks rows the loader refused. A live file the loader
|
|
493
|
+
* stopped reading (LiveItemsRead's `stopped`) gives its stream a `live-items.not-read` warn event.
|
|
354
494
|
*/
|
|
355
495
|
export function displayedTraces(loaded) {
|
|
356
496
|
const reasons = new Map(loaded.refused.map((error) => [error.streamId, error.message]));
|
|
357
|
-
|
|
497
|
+
const stopped = (loaded.live ?? []).filter((read) => read.stopped !== undefined);
|
|
498
|
+
if (reasons.size === 0 && stopped.length === 0)
|
|
358
499
|
return loaded.bundle;
|
|
359
500
|
const streams = loaded.bundle.streams.map((stream) => {
|
|
360
501
|
const reason = reasons.get(stream.id);
|
|
@@ -363,7 +504,7 @@ export function displayedTraces(loaded) {
|
|
|
363
504
|
const { liveActor: _live, trace: _index, actor, ...rest } = stream;
|
|
364
505
|
return {
|
|
365
506
|
...rest,
|
|
366
|
-
...(actor === undefined ? {} : { actor: { ...actor, items: [] } }),
|
|
507
|
+
...(actor === undefined ? {} : { actor: { ...withoutRequestLists(actor), items: [] } }),
|
|
367
508
|
artifacts: stream.artifacts.filter((artifact) => !isScreenshot(artifact)),
|
|
368
509
|
traceUnavailable: reason,
|
|
369
510
|
};
|
|
@@ -378,7 +519,19 @@ export function displayedTraces(loaded) {
|
|
|
378
519
|
message: reasons.get(stream.id),
|
|
379
520
|
streamId: stream.id,
|
|
380
521
|
}));
|
|
381
|
-
|
|
522
|
+
const notRead = stopped.map((read) => ({
|
|
523
|
+
id: `event-live-items-not-read-${read.streamId}`,
|
|
524
|
+
at: loaded.bundle.streams.find((stream) => stream.id === read.streamId)?.updatedAt ?? "",
|
|
525
|
+
level: "warn",
|
|
526
|
+
type: "live-items.not-read",
|
|
527
|
+
message: read.stopped,
|
|
528
|
+
streamId: read.streamId,
|
|
529
|
+
}));
|
|
530
|
+
return {
|
|
531
|
+
...loaded.bundle,
|
|
532
|
+
streams,
|
|
533
|
+
events: [...loaded.bundle.events, ...events, ...notRead],
|
|
534
|
+
};
|
|
382
535
|
}
|
|
383
536
|
/** The bundle with every stream loaded; the first refusal throws. */
|
|
384
537
|
export async function loadRunTraces(source, bundle, maxBytes) {
|
package/dist/run/streams.d.ts
CHANGED
|
@@ -289,7 +289,10 @@ export interface RunStream {
|
|
|
289
289
|
* completionReason, or completedAt, and this shape cannot claim them. Present only on
|
|
290
290
|
* `inProgress` bundles; the final write replaces it with the real `actor` and never
|
|
291
291
|
* carries it. A v3 run.json holds `humanish.live-actor.v2` with `itemsFile` in place of `items`;
|
|
292
|
-
* the trace loader (src/run/stream-trace.ts) reads the file and puts the items back.
|
|
292
|
+
* the trace loader (src/run/stream-trace.ts) reads the file and puts the items back. A v4
|
|
293
|
+
* run.json's live actor also leaves out `tokenUsage.turns` and `providerRequests`, which are
|
|
294
|
+
* written only in the trace when the participant finishes, and carries the `estimatedCost` the
|
|
295
|
+
* run writer priced from those rows.
|
|
293
296
|
*/
|
|
294
297
|
liveActor?: {
|
|
295
298
|
schema: "humanish.live-actor.v1" | "humanish.live-actor.v2";
|
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
// RUN_ARTIFACT_MAX_BYTES, and the public-safety scan visits at most runEntryLimit(participants)
|
|
5
5
|
// files and folders.
|
|
6
6
|
//
|
|
7
|
-
// observer/index.html carries every participant's items gzipped, so with busy participants it
|
|
8
|
-
//
|
|
9
|
-
// working 8 hours.
|
|
10
|
-
//
|
|
11
|
-
// 16 MiB,
|
|
7
|
+
// observer/index.html carries every participant's items gzipped, so with busy participants it
|
|
8
|
+
// passes verify's 32 MiB long before run.json passes analysis's 16 MiB: at these rates, 20
|
|
9
|
+
// participants working 8 hours. From run bundle v4 run.json grows by participant, not by model
|
|
10
|
+
// request: each participant's per-request lists are only in its trace. run.json can still pass
|
|
11
|
+
// 16 MiB, since a study's mission is not capped and each participant's stream copies it.
|
|
12
|
+
// observer-data.json leaves the lists out too and was 0.94 to 1.12 of run.json's size on the live
|
|
13
|
+
// runs measured, so it passes 32 MiB only after run.json passes 16 MiB, and the estimate does not
|
|
14
|
+
// check it.
|
|
12
15
|
import { EVIDENCE_LIMITS } from "../analysis/analysis-limits.js";
|
|
13
16
|
import { RUN_ARTIFACT_MAX_BYTES } from "../run/contained-output.js";
|
|
14
17
|
import { formatDuration } from "../run/projection.js";
|
|
@@ -20,19 +23,16 @@ import { runEntryLimit } from "../verify/scan-limit.js";
|
|
|
20
23
|
// own files. Pace, session length and screenshots: the live computer-use runs kept on the
|
|
21
24
|
// maintainer's machine. A trace's rate a turn on the public site/public/runs/lobby-0927 run
|
|
22
25
|
// (8 participants, 20 to 78 turns) was 1,579 bytes plus 91 for the page, under the live fit.
|
|
26
|
+
// The run.json and page data rates were measured again on those runs written as v4.
|
|
23
27
|
const RATES = Object.freeze({
|
|
24
28
|
/** run.json bytes for the run itself: its header, study, review and cost. Live fit: 4,381. */
|
|
25
29
|
runBytes: 4_400,
|
|
26
30
|
/**
|
|
27
|
-
* run.json bytes for each participant
|
|
28
|
-
*
|
|
31
|
+
* run.json bytes for each participant: its stream, actor summary and closing report request.
|
|
32
|
+
* Live fit on v3: 16,239. Written as v4, the kept runs of one participant held 20,571 to 23,160
|
|
33
|
+
* bytes and those of two 30,647 to 38,948, beside 20,900 and 37,400 at these rates.
|
|
29
34
|
*/
|
|
30
35
|
participantBytes: 16_500,
|
|
31
|
-
/**
|
|
32
|
-
* run.json bytes a model turn adds: the request's entries in `tokenUsage.turns` and
|
|
33
|
-
* `conversation.requests`. Live: 30,623 bytes over 137 requests, 223.5 each.
|
|
34
|
-
*/
|
|
35
|
-
turnRunBytes: 225,
|
|
36
36
|
/** A trace file's bytes before its turns: the actor summary. Live fit: 5,378. */
|
|
37
37
|
participantTraceBytes: 5_400,
|
|
38
38
|
/**
|
|
@@ -63,10 +63,10 @@ const RATES = Object.freeze({
|
|
|
63
63
|
/** observer/index.html's app before any run data: 1,199,831 bytes on the dry runs. */
|
|
64
64
|
observerAppBytes: 1_200_000,
|
|
65
65
|
/**
|
|
66
|
-
* The page's compact copy of observer-data.json, as a share of run.json: 0.
|
|
67
|
-
*
|
|
66
|
+
* The page's compact copy of observer-data.json, as a share of run.json: 0.71 to 0.84 on the
|
|
67
|
+
* live runs written as v4, where neither carries the per-request lists.
|
|
68
68
|
*/
|
|
69
|
-
observerPageDataShare: 0.
|
|
69
|
+
observerPageDataShare: 0.85,
|
|
70
70
|
/**
|
|
71
71
|
* The page's gzipped base64 items, as a share of all the traces' bytes: 0.087 to 0.114 on the
|
|
72
72
|
* live runs of two participants, 0.097 for the busiest participant alone (about 475 bytes a
|
|
@@ -124,7 +124,7 @@ export function estimateBundle(participants, sessionMs) {
|
|
|
124
124
|
largestTrace = Math.max(largestTrace, trace);
|
|
125
125
|
}
|
|
126
126
|
const count = participants.length;
|
|
127
|
-
const runFileBytes = RATES.runBytes + count * RATES.participantBytes
|
|
127
|
+
const runFileBytes = RATES.runBytes + count * RATES.participantBytes;
|
|
128
128
|
const screenshots = count + turns + frames;
|
|
129
129
|
const observerPageBytes = Math.round(RATES.observerAppBytes +
|
|
130
130
|
RATES.observerPageDataShare * runFileBytes +
|
|
@@ -204,7 +204,7 @@ export function bundleLimitReason(estimate) {
|
|
|
204
204
|
: `${changes.slice(0, -1).join(", ")}, or ${changes.at(-1)}`;
|
|
205
205
|
return [
|
|
206
206
|
...passed,
|
|
207
|
-
`The estimate counts ${counted.join(" and ")}, at a model turn every ${formatDuration(RATES.secondsPerTurn * 1000)} that adds
|
|
207
|
+
`The estimate counts ${counted.join(" and ")}, at a model turn every ${formatDuration(RATES.secondsPerTurn * 1000)} that adds up to ${(RATES.turnTraceBytes / 1000).toFixed(1)} KB to the participant's trace.`,
|
|
208
208
|
"The run goes ahead.",
|
|
209
209
|
`To stay under ${passed.length === 1 ? "the limit" : "the limits"}, ${advice}.`,
|
|
210
210
|
].join(" ");
|
package/dist/verify/costs.d.ts
CHANGED
|
@@ -24,7 +24,11 @@ export declare function contradictsAccountBilling(streams: readonly {
|
|
|
24
24
|
* a null estimate, or a participant without estimatedCost is fine. A non-null figure must carry its
|
|
25
25
|
* ratesAsOf date + source; a number total must equal round6(sum of only the non-null lines) and a
|
|
26
26
|
* null line may never be coerced to 0. A null estimate must be declared (a reason + null
|
|
27
|
-
* ratesAsOf), mirroring the terminal no-spend proof's null-discipline.
|
|
28
|
-
*
|
|
27
|
+
* ratesAsOf), mirroring the terminal no-spend proof's null-discipline. A running participant's
|
|
28
|
+
* estimate is checked the same way. Account-billed participants also have their execution profile
|
|
29
|
+
* and provider request receipts checked here. From v4 a running participant's receipts are written
|
|
30
|
+
* only in its trace when it finishes, so its live actor holds none. The receipts of a stream in
|
|
31
|
+
* `refused` (its trace the loader refused) are not checked: from v4 they are only in that trace,
|
|
32
|
+
* and verify reports the refusal.
|
|
29
33
|
*/
|
|
30
|
-
export declare function costAndReceiptFindings(bundle: RunBundle): string[];
|
|
34
|
+
export declare function costAndReceiptFindings(bundle: RunBundle, refused: ReadonlySet<string>): string[];
|
package/dist/verify/costs.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// totals match their known lines, and account-billed participants never acquire a price.
|
|
3
3
|
import { validActorExecutionProfile, validActorProviderRequests } from "../actors/contract.js";
|
|
4
4
|
import { round6 } from "../run/pricing.js";
|
|
5
|
+
import { keepsRequestListsInRunJson } from "../run/stream-trace.js";
|
|
5
6
|
/** Account-billed participants must never acquire a price through an aggregate or ambiguous model line. */
|
|
6
7
|
export function contradictsAccountBilling(streams, cost) {
|
|
7
8
|
const account = (stream) => (stream.liveActor?.executionProfile ?? stream.actor?.executionProfile)?.billing ===
|
|
@@ -28,18 +29,23 @@ export function contradictsAccountBilling(streams, cost) {
|
|
|
28
29
|
* a null estimate, or a participant without estimatedCost is fine. A non-null figure must carry its
|
|
29
30
|
* ratesAsOf date + source; a number total must equal round6(sum of only the non-null lines) and a
|
|
30
31
|
* null line may never be coerced to 0. A null estimate must be declared (a reason + null
|
|
31
|
-
* ratesAsOf), mirroring the terminal no-spend proof's null-discipline.
|
|
32
|
-
*
|
|
32
|
+
* ratesAsOf), mirroring the terminal no-spend proof's null-discipline. A running participant's
|
|
33
|
+
* estimate is checked the same way. Account-billed participants also have their execution profile
|
|
34
|
+
* and provider request receipts checked here. From v4 a running participant's receipts are written
|
|
35
|
+
* only in its trace when it finishes, so its live actor holds none. The receipts of a stream in
|
|
36
|
+
* `refused` (its trace the loader refused) are not checked: from v4 they are only in that trace,
|
|
37
|
+
* and verify reports the refusal.
|
|
33
38
|
*/
|
|
34
|
-
export function costAndReceiptFindings(bundle) {
|
|
39
|
+
export function costAndReceiptFindings(bundle, refused) {
|
|
35
40
|
const findings = [];
|
|
36
41
|
if (contradictsAccountBilling(bundle.streams, bundle.cost))
|
|
37
42
|
findings.push("Run cost lines contradict account billing identity");
|
|
38
43
|
findings.push(...costSummaryFindings(bundle.cost));
|
|
44
|
+
const liveReceipts = keepsRequestListsInRunJson(bundle.schema);
|
|
39
45
|
for (const stream of bundle.streams) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
46
|
+
const loaded = !refused.has(stream.id);
|
|
47
|
+
findings.push(...executionReceiptFindings(stream.actor, loaded));
|
|
48
|
+
findings.push(...executionReceiptFindings(stream.liveActor, liveReceipts && loaded));
|
|
43
49
|
findings.push(...participantEstimateFindings(stream));
|
|
44
50
|
}
|
|
45
51
|
return findings;
|
|
@@ -81,14 +87,17 @@ function costSummaryFindings(cost) {
|
|
|
81
87
|
}
|
|
82
88
|
return findings;
|
|
83
89
|
}
|
|
84
|
-
/**
|
|
85
|
-
|
|
90
|
+
/**
|
|
91
|
+
* An account-billed participant's execution profile and, when `receipts` says the trace holds
|
|
92
|
+
* them, its provider request receipts.
|
|
93
|
+
*/
|
|
94
|
+
function executionReceiptFindings(actor, receipts) {
|
|
86
95
|
const findings = [];
|
|
87
96
|
if (actor?.executionProfile === undefined)
|
|
88
97
|
return findings;
|
|
89
98
|
if (!validActorExecutionProfile(actor.executionProfile))
|
|
90
99
|
findings.push("Invalid actor execution profile");
|
|
91
|
-
if (!validActorProviderRequests(actor.providerRequests))
|
|
100
|
+
if (receipts && !validActorProviderRequests(actor.providerRequests))
|
|
92
101
|
findings.push("Invalid account participant request receipts");
|
|
93
102
|
if (actor.historyTurnsOmitted !== undefined &&
|
|
94
103
|
(!Number.isSafeInteger(actor.historyTurnsOmitted) || actor.historyTurnsOmitted < 0))
|
|
@@ -99,14 +108,20 @@ function executionReceiptFindings(actor) {
|
|
|
99
108
|
findings.push("Account participant dollars must remain unknown");
|
|
100
109
|
return findings;
|
|
101
110
|
}
|
|
111
|
+
/** The labeling of a participant's estimate: its finished actor's, and its live actor's while it runs. */
|
|
102
112
|
function participantEstimateFindings(stream) {
|
|
113
|
+
const participantLabel = stream.laneId ?? stream.id;
|
|
114
|
+
return [
|
|
115
|
+
...estimateFindings(stream.actor?.estimatedCost, participantLabel, "actor"),
|
|
116
|
+
...estimateFindings(stream.liveActor?.estimatedCost, participantLabel, "live actor"),
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
function estimateFindings(estimate, participantLabel, field) {
|
|
103
120
|
const findings = [];
|
|
104
|
-
const estimate = stream.actor?.estimatedCost;
|
|
105
121
|
if (!estimate)
|
|
106
122
|
return findings;
|
|
107
|
-
const participantLabel = stream.laneId ?? stream.id;
|
|
108
123
|
if (estimate.schema !== "humanish.actor-estimated-cost.v1") {
|
|
109
|
-
findings.push(`participant ${participantLabel}
|
|
124
|
+
findings.push(`participant ${participantLabel} ${field} estimatedCost schema is ${String(estimate.schema)}, expected humanish.actor-estimated-cost.v1`);
|
|
110
125
|
}
|
|
111
126
|
if (estimate.estimatedCostUsd !== null) {
|
|
112
127
|
if (typeof estimate.ratesAsOf !== "string" || estimate.ratesAsOf.length === 0) {
|
package/dist/verify/verify.js
CHANGED
|
@@ -5,7 +5,7 @@ import { analysisSharingProblems } from "../analysis/sharing.js";
|
|
|
5
5
|
import { containsSensitive, REDACTED_SANDBOX_ID } from "../evidence/redaction.js";
|
|
6
6
|
import { keyedSandboxIds, readRunSandboxIds } from "../run/sandbox-ids.js";
|
|
7
7
|
import { validatePreparedRunArtifactPaths } from "../run/paths.js";
|
|
8
|
-
import { isReadRunBundleSchema, RUN_BUNDLE_FILE, RUN_BUNDLE_SCHEMA, RUN_BUNDLE_V1_SCHEMA, RUN_BUNDLE_V2_SCHEMA, } from "../run/bundle.js";
|
|
8
|
+
import { isReadRunBundleSchema, RUN_BUNDLE_FILE, RUN_BUNDLE_SCHEMA, RUN_BUNDLE_V1_SCHEMA, RUN_BUNDLE_V2_SCHEMA, RUN_BUNDLE_V3_SCHEMA, } from "../run/bundle.js";
|
|
9
9
|
import { ContainedReadRefusedError, refusalText } from "../run/contained-output.js";
|
|
10
10
|
import { isCleanupResult, isRunBundle } from "../run/bundle-shape.js";
|
|
11
11
|
import { loadStreamTraces } from "../run/stream-trace.js";
|
|
@@ -139,7 +139,7 @@ export async function verifyResolvedRun(cwd, runInput, runPaths) {
|
|
|
139
139
|
? [`missing local evidence artifacts: ${missingEvidenceArtifacts.join(", ")}`]
|
|
140
140
|
: []),
|
|
141
141
|
]));
|
|
142
|
-
checks.push(...(await evidenceChecks(runPaths, bundle, cleanupRead)));
|
|
142
|
+
checks.push(...(await evidenceChecks(runPaths, bundle, cleanupRead, traces.refusedStreams)));
|
|
143
143
|
const recordingOk = checks.every((check) => check.ok);
|
|
144
144
|
if (derivedPublicSafetyFindings.length > 0)
|
|
145
145
|
checks.push({
|
|
@@ -225,12 +225,13 @@ function evidenceArtifactsCheck(shaped, problems) {
|
|
|
225
225
|
}
|
|
226
226
|
/**
|
|
227
227
|
* run.json with each stream's trace loaded, one file at a time, and the refusal of each trace that
|
|
228
|
-
* does not match its index. A run.json that fails the shape guard
|
|
228
|
+
* does not match its index, with the ids of those streams. A run.json that fails the shape guard
|
|
229
|
+
* is returned as it is.
|
|
229
230
|
*/
|
|
230
231
|
async function loadTraces(runPaths, stored) {
|
|
231
232
|
const scanned = { sensitive: [], opaque: [] };
|
|
232
233
|
if (!isRunBundle(stored))
|
|
233
|
-
return { bundle: stored, refusedTraces: [], ...scanned, pages: [] };
|
|
234
|
+
return { bundle: stored, refusedTraces: [], refusedStreams: new Set(), ...scanned, pages: [] };
|
|
234
235
|
// Each trace is source evidence: scanned by the bytes it was joined from, apart from the
|
|
235
236
|
// directory walk and any path exemption the walk makes.
|
|
236
237
|
const { bundle, refused } = await loadStreamTraces(runPaths, stored, {
|
|
@@ -243,6 +244,7 @@ async function loadTraces(runPaths, stored) {
|
|
|
243
244
|
return {
|
|
244
245
|
bundle,
|
|
245
246
|
refusedTraces: refused.map((error) => error.message),
|
|
247
|
+
refusedStreams: new Set(refused.map((error) => error.streamId)),
|
|
246
248
|
...scanned,
|
|
247
249
|
pages: pageFindings(bundle),
|
|
248
250
|
};
|
|
@@ -312,7 +314,7 @@ function bundlePresenceChecks(bundleRead, review) {
|
|
|
312
314
|
? `run.json declares ${String(schema)}`
|
|
313
315
|
: record === undefined
|
|
314
316
|
? "no readable run.json, so no schema"
|
|
315
|
-
: `run.json declares ${typeof schema === "string" ? schema : "no schema"}; verify reads ${RUN_BUNDLE_SCHEMA}, ${RUN_BUNDLE_V2_SCHEMA} and ${RUN_BUNDLE_V1_SCHEMA}`,
|
|
317
|
+
: `run.json declares ${typeof schema === "string" ? schema : "no schema"}; verify reads ${RUN_BUNDLE_SCHEMA}, ${RUN_BUNDLE_V3_SCHEMA}, ${RUN_BUNDLE_V2_SCHEMA} and ${RUN_BUNDLE_V1_SCHEMA}`,
|
|
316
318
|
},
|
|
317
319
|
{
|
|
318
320
|
name: "run bundle shape",
|
|
@@ -378,8 +380,12 @@ function findingsCheck(name, findings, okMessage, failLabel) {
|
|
|
378
380
|
* check has no findings and stays ok; the shape check is the failure.
|
|
379
381
|
*/
|
|
380
382
|
const SHAPE_UNCHECKED = "not checked, because run.json failed the shape check";
|
|
381
|
-
/**
|
|
382
|
-
|
|
383
|
+
/**
|
|
384
|
+
* The bundle-content checks, in report order. A bundle that fails its shape guard has no findings.
|
|
385
|
+
* `refusedStreams` names the streams whose traces the loader refused, which the evidence-artifacts
|
|
386
|
+
* check reports.
|
|
387
|
+
*/
|
|
388
|
+
async function evidenceChecks(runPaths, bundle, cleanupRead, refusedStreams) {
|
|
383
389
|
const cleanupJson = runJsonValue(cleanupRead);
|
|
384
390
|
const valid = isRunBundle(bundle) ? bundle : null;
|
|
385
391
|
const pass = (message) => (valid ? message : SHAPE_UNCHECKED);
|
|
@@ -409,7 +415,7 @@ async function evidenceChecks(runPaths, bundle, cleanupRead) {
|
|
|
409
415
|
// Absence passes (fail-open on display); a claimed dollar figure without its ratesAsOf date +
|
|
410
416
|
// source, or a total that does not match its known lines, fails. Magnitude is never inspected:
|
|
411
417
|
// a correctly-labeled huge estimate still passes.
|
|
412
|
-
findingsCheck("cost estimate labeling", valid ? costAndReceiptFindings(valid) : [], pass("every cost estimate carries its ratesAsOf date and source and the total matches its lines, or the run claims no cost"), "cost labeling findings"),
|
|
418
|
+
findingsCheck("cost estimate labeling", valid ? costAndReceiptFindings(valid, refusedStreams) : [], pass("every cost estimate carries its ratesAsOf date and source and the total matches its lines, or the run claims no cost"), "cost labeling findings"),
|
|
413
419
|
];
|
|
414
420
|
return checks;
|
|
415
421
|
}
|