humanish 0.110.4 → 0.111.1
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 +31 -0
- package/CONTEXT.md +2 -2
- package/dist/actors/codex/restricted-participant-policy.d.ts +2 -0
- package/dist/actors/codex/restricted-participant-policy.js +30 -2
- package/dist/actors/codex/restricted-participant.js +1 -1
- package/dist/actors/computer-use/executor-diagnostic.d.ts +15 -0
- package/dist/actors/computer-use/executor-diagnostic.js +105 -0
- package/dist/actors/computer-use/executor-error.d.ts +18 -3
- package/dist/actors/computer-use/executor-error.js +30 -4
- package/dist/actors/computer-use/loop/actions.d.ts +6 -3
- package/dist/actors/computer-use/loop/actions.js +27 -7
- package/dist/actors/computer-use/loop/ending.js +9 -1
- package/dist/actors/computer-use/loop/types.d.ts +14 -0
- package/dist/actors/computer-use/loop.d.ts +1 -1
- package/dist/actors/computer-use/loop.js +1 -1
- package/dist/actors/computer-use/openai-context.d.ts +8 -2
- package/dist/actors/computer-use/openai-context.js +91 -26
- package/dist/actors/computer-use/openai-provider.js +55 -12
- package/dist/actors/computer-use/openai-requests.d.ts +2 -1
- package/dist/actors/computer-use/openai-requests.js +4 -1
- package/dist/actors/contract.d.ts +7 -1
- package/dist/actors/local-agent/claude-session.js +1 -1
- package/dist/actors/local-agent/cli.js +1 -1
- package/dist/actors/scripted-browser/actor.d.ts +2 -2
- package/dist/browser-control/client.js +5 -5
- package/dist/browser-control/protocol.d.ts +30 -1
- package/dist/browser-control/protocol.js +19 -2
- package/dist/browser-control/transport.d.ts +3 -2
- package/dist/browser-control/transport.js +12 -7
- package/dist/cli/commands/study-route-computer-use.js +2 -2
- package/dist/cli/commands/study-route-shared-world.js +2 -2
- package/dist/cli/commands/study.js +1 -4
- package/dist/cli/doctor.js +11 -0
- package/dist/evidence/encoded-text.d.ts +3 -3
- package/dist/evidence/encoded-text.js +10 -5
- package/dist/evidence/redaction.d.ts +2 -2
- package/dist/evidence/redaction.js +18 -7
- package/dist/guest/browser-tools.js +5 -1
- package/dist/guest/chromium-text.js +42 -18
- package/dist/guest/desktop-executor.js +5 -1
- package/dist/keys/key-resolution.d.ts +3 -0
- package/dist/keys/key-resolution.js +15 -2
- package/dist/routes/computer-use/bundle-parts.d.ts +4 -0
- package/dist/routes/computer-use/bundle-parts.js +7 -2
- package/dist/routes/computer-use/fanout-bundle.js +4 -2
- package/dist/routes/computer-use/local-vm.js +1 -2
- package/dist/routes/computer-use/participant-model.js +7 -2
- package/dist/routes/computer-use/plan.js +7 -10
- package/dist/routes/computer-use/preflight.js +1 -1
- package/dist/routes/computer-use/result.js +1 -1
- package/dist/routes/computer-use/route.js +2 -3
- package/dist/routes/computer-use/setup.js +2 -2
- package/dist/routes/computer-use/types.d.ts +4 -4
- package/dist/routes/preview.js +1 -0
- package/dist/routes/scripted/plan.js +4 -5
- package/dist/routes/scripted/route.js +3 -3
- package/dist/routes/scripted/scenario.d.ts +1 -1
- package/dist/routes/scripted/scenario.js +3 -3
- package/dist/routes/scripted/types.d.ts +2 -2
- package/dist/routes/shared-world/handoff.js +1 -1
- package/dist/routes/shared-world/participant-specs.js +1 -1
- package/dist/routes/shared-world/plan.js +6 -8
- package/dist/routes/shared-world/route.js +2 -2
- package/dist/routes/shared-world/setup.js +1 -0
- package/dist/routes/shared-world/types.d.ts +1 -1
- package/dist/routes/terminal/bundle.d.ts +3 -3
- package/dist/routes/terminal/dry-run.js +1 -0
- package/dist/routes/terminal/ledger.d.ts +2 -2
- package/dist/routes/terminal/lifetime.js +1 -1
- package/dist/routes/terminal/live-finish.js +25 -6
- package/dist/routes/terminal/live-sandbox.js +5 -1
- package/dist/routes/terminal/plan.js +3 -4
- package/dist/routes/terminal/recorder.d.ts +6 -0
- package/dist/routes/terminal/recorder.js +61 -6
- package/dist/routes/terminal/route.js +2 -3
- package/dist/routes/terminal/session.js +2 -1
- package/dist/routes/terminal/token-usage.d.ts +3 -0
- package/dist/routes/terminal/token-usage.js +5 -0
- package/dist/routes/terminal/types.d.ts +3 -2
- package/dist/routes/terminal/types.js +2 -1
- package/dist/run/dry-run.js +1 -0
- package/dist/run/results.d.ts +3 -1
- package/dist/run/run.d.ts +2 -0
- package/dist/run/run.js +16 -2
- package/dist/run/terminal-contract.d.ts +10 -0
- package/dist/run/terminal-contract.js +27 -9
- package/dist/run-study.d.ts +5 -3
- package/dist/run-study.js +7 -11
- package/dist/study/adapter-scorer-loader.d.ts +1 -1
- package/dist/study/adapter-scorer-loader.js +3 -3
- package/dist/study/composition-rules.js +36 -27
- package/dist/study/config.d.ts +29 -13
- package/dist/study/config.js +174 -157
- package/dist/study/discover.d.ts +1 -1
- package/dist/study/discover.js +1 -1
- package/dist/study/doctor.js +4 -5
- package/dist/study/keys.d.ts +250 -2
- package/dist/study/keys.js +32 -62
- package/dist/study/migrate/convert.js +3 -4
- package/dist/study/migrate/v2.d.ts +22 -7
- package/dist/study/migrate/v2.js +173 -23
- package/dist/study/parse/actors.d.ts +11 -32
- package/dist/study/parse/actors.js +11 -156
- package/dist/study/parse/comms.js +6 -2
- package/dist/study/parse/execution.d.ts +9 -4
- package/dist/study/parse/execution.js +4 -39
- package/dist/study/parse/{study-v3.d.ts → front.d.ts} +6 -17
- package/dist/study/parse/{study-v3.js → front.js} +24 -112
- package/dist/study/parse/subject.d.ts +2 -0
- package/dist/study/parse/subject.js +15 -18
- package/dist/study/parse/url-credentials.d.ts +3 -3
- package/dist/study/parse/url-credentials.js +155 -13
- package/dist/study/persona-resolve.js +7 -9
- package/dist/study/plan-base.d.ts +1 -1
- package/dist/study/plan-base.js +5 -7
- package/dist/study/plan-participants.d.ts +2 -2
- package/dist/study/plan-participants.js +3 -3
- package/dist/study/plan-types.d.ts +7 -5
- package/dist/study/plan.js +12 -6
- package/dist/study/preflight-probes.js +2 -2
- package/dist/study/preflight.d.ts +1 -1
- package/dist/study/preflight.js +1 -1
- package/dist/study/routing.d.ts +5 -5
- package/dist/study/routing.js +16 -16
- package/dist/study/run-study-options.d.ts +1 -1
- package/dist/study/run-study-options.js +2 -2
- package/dist/study/study-deps.d.ts +1 -1
- package/dist/study/study-fields.d.ts +4 -25
- package/dist/study/study-fields.js +15 -44
- package/dist/study/summary.js +13 -8
- package/dist/study/types.d.ts +51 -128
- package/dist/study/types.js +0 -1
- package/dist/study/url-credentials.js +3 -4
- package/dist/study/validation.d.ts +2 -10
- package/dist/study/validation.js +23 -53
- package/dist/study/warnings.d.ts +11 -3
- package/dist/study/warnings.js +56 -110
- package/dist/substrates/local/runtime-config.js +11 -13
- package/docs/architecture/browser-control.md +12 -2
- package/docs/architecture/guest-desktop.md +7 -1
- package/docs/architecture/terminal-product-route.md +3 -2
- package/docs/contracts/schemas.md +39 -9
- package/package.json +1 -1
- package/dist/study/parse/study.d.ts +0 -38
- package/dist/study/parse/study.js +0 -225
package/dist/study/migrate/v2.js
CHANGED
|
@@ -4,20 +4,113 @@
|
|
|
4
4
|
// rules the two formats share. The converter (convert.ts) rewrites the YAML document separately and
|
|
5
5
|
// compares what the two rewrites parse to.
|
|
6
6
|
//
|
|
7
|
-
// The v2
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// The v2 parser itself is gone. The rules here on keys v3 does not have (the actor list and roster,
|
|
8
|
+
// `laneFocus`, the `scenario` object, `subject.topology`, `execution.caps`, and the checks on their
|
|
9
|
+
// combinations) are kept with that parser's messages, so migrate refuses what it refused.
|
|
10
10
|
import { isRecord } from "../../run/type-guards.js";
|
|
11
|
-
import {
|
|
11
|
+
import { ACTOR, CAPS, EXECUTION, findUnknownKey, PARTICIPANT_ENTRY, SHARED_SECTIONS, SUBJECT, } from "../keys.js";
|
|
12
12
|
import { parseActorFields, parseActorType, parseParticipantEntries, PARTICIPANT_ID_MAX_CHARS, PARTICIPANT_ID_PATTERN, } from "../parse/actors.js";
|
|
13
13
|
import { parseComms } from "../parse/comms.js";
|
|
14
|
-
import { parseExecution, parsePolicies, parseReview } from "../parse/execution.js";
|
|
15
|
-
import {
|
|
16
|
-
import { parseSubject } from "../parse/subject.js";
|
|
14
|
+
import { parseCaps, parseExecution, parsePolicies, parseReview } from "../parse/execution.js";
|
|
15
|
+
import { hasSubjectSource, parseSubject } from "../parse/subject.js";
|
|
17
16
|
import { posInt, str } from "../parse/values.js";
|
|
18
17
|
import { isComputerUseComposition, isScriptedBrowserComposition, isSharedWorldComposition, isTerminalProductComposition, routeOf, } from "../routing.js";
|
|
19
18
|
import { desktopMediaValidationReason, outputTokenLimitValidationReason, smtpValidationReason, } from "../validation.js";
|
|
20
|
-
import { ID_PATTERN, STUDY_SCHEMA,
|
|
19
|
+
import { ID_PATTERN, STUDY_SCHEMA, } from "../types.js";
|
|
20
|
+
/** The schema of the study format before humanish.study.v3. */
|
|
21
|
+
export const V2_SCHEMA = "humanish.lab.v2";
|
|
22
|
+
/** The message for a humanish.lab.v2 document, which the package and the CLI no longer run. */
|
|
23
|
+
export const V2_UNSUPPORTED_MESSAGE = "This is a humanish.lab.v2 study, which humanish no longer reads. Convert its file with humanish migrate <path>.";
|
|
24
|
+
// The StudyConfig fields of humanish 0.110 and earlier, which a humanish.study.v3 config does not
|
|
25
|
+
// have, each with where its value goes now. A library caller may still build a config with them.
|
|
26
|
+
const V2_FIELDS = [
|
|
27
|
+
{
|
|
28
|
+
present: (config) => config.actors !== undefined,
|
|
29
|
+
move: "`actors[0]` is `actor`, and its `count`, `lanes`, `roster` and `laneFocus` are `participants` (`surfaces` on the scripted route)",
|
|
30
|
+
},
|
|
31
|
+
// A caller that moved `actors[0]` to `actor` as it was.
|
|
32
|
+
{
|
|
33
|
+
present: (config) => isRecord(config.actor) &&
|
|
34
|
+
["count", "lanes", "roster", "laneFocus"].some((key) => config.actor[key] !== undefined),
|
|
35
|
+
move: "`actor.count`, `actor.lanes`, `actor.roster` and `actor.laneFocus` are `participants` (`surfaces` on the scripted route)",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
present: (config) => isRecord(config.scenario),
|
|
39
|
+
move: "`scenario.ref` is `scenario`, a string, `scenario.mode` is `mode` and `scenario.caps` is `caps`",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
present: (config) => isRecord(config.execution) && config.execution.caps !== undefined,
|
|
43
|
+
move: "`execution.caps` is `caps`",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
present: (config) => isRecord(config.subject) && config.subject.topology !== undefined,
|
|
47
|
+
move: "`subject.topology: shared-world` is `route: shared-world`",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
present: (config) => config.personas !== undefined,
|
|
51
|
+
move: "`personas` has no field now; remove it",
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
/** Where each humanish 0.110 StudyConfig field a config still sets went; empty when it sets none. */
|
|
55
|
+
export function v2FieldMoves(config) {
|
|
56
|
+
if (!isRecord(config))
|
|
57
|
+
return [];
|
|
58
|
+
return V2_FIELDS.filter((field) => field.present(config)).map((field) => field.move);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The v2 key a hand-converted humanish.study.v3 file is most likely to keep, with where it went;
|
|
62
|
+
* undefined when the file keeps none. parse/front.ts refuses the file with it.
|
|
63
|
+
*/
|
|
64
|
+
export function movedV2KeyReason(raw) {
|
|
65
|
+
if (raw.actors !== undefined) {
|
|
66
|
+
return "A study has one `actor:` object, and its participants go in `participants:`. `actors:` is the humanish.lab.v2 spelling.";
|
|
67
|
+
}
|
|
68
|
+
if (isRecord(raw.actor)) {
|
|
69
|
+
for (const key of ["count", "lanes", "roster", "laneFocus"]) {
|
|
70
|
+
if (raw.actor[key] !== undefined) {
|
|
71
|
+
return `\`actor.${key}\` moved to \`participants:\`: a count, \`{ count, instruction }\`, or a list of participants.`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (isRecord(raw.subject) && raw.subject.topology !== undefined) {
|
|
76
|
+
return "`subject.topology` moved to `route:`. A shared world is `route: shared-world`.";
|
|
77
|
+
}
|
|
78
|
+
if (isRecord(raw.execution) && raw.execution.caps !== undefined) {
|
|
79
|
+
return "`execution.caps` moved to a top-level `caps:` block.";
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
// The keys of a humanish.lab.v2 file, in the order its parser listed them: the v3 sections with
|
|
84
|
+
// `subject.topology`, the actor list with its participant keys, `execution.caps`, `personas` and
|
|
85
|
+
// the `scenario` object.
|
|
86
|
+
const V2_KEYS = {
|
|
87
|
+
schema: true,
|
|
88
|
+
id: true,
|
|
89
|
+
title: true,
|
|
90
|
+
description: true,
|
|
91
|
+
subject: { source: true, topology: true, ...without(SUBJECT, "source") },
|
|
92
|
+
actors: {
|
|
93
|
+
type: true,
|
|
94
|
+
count: true,
|
|
95
|
+
lanes: PARTICIPANT_ENTRY,
|
|
96
|
+
// Roster groups are participants with a count; the parser expanded them into `lanes[]`.
|
|
97
|
+
roster: { ...PARTICIPANT_ENTRY, count: true },
|
|
98
|
+
...without(ACTOR, "type"),
|
|
99
|
+
laneFocus: { id: true, label: true, instruction: true },
|
|
100
|
+
},
|
|
101
|
+
execution: withAfter(EXECUTION, "egressAllow", { caps: CAPS }),
|
|
102
|
+
// Inline personas were validated by the persona resolver.
|
|
103
|
+
personas: true,
|
|
104
|
+
scenario: { ref: true, mode: true, inline: true, caps: CAPS },
|
|
105
|
+
...SHARED_SECTIONS,
|
|
106
|
+
};
|
|
107
|
+
function without(shape, ...keys) {
|
|
108
|
+
return Object.fromEntries(Object.entries(shape).filter(([key]) => !keys.includes(key)));
|
|
109
|
+
}
|
|
110
|
+
// `shape` with `added` inserted after the key `after`.
|
|
111
|
+
function withAfter(shape, after, added) {
|
|
112
|
+
return Object.fromEntries(Object.entries(shape).flatMap((entry) => entry[0] === after ? [entry, ...Object.entries(added)] : [entry]));
|
|
113
|
+
}
|
|
21
114
|
function refused(message) {
|
|
22
115
|
return { ok: false, message };
|
|
23
116
|
}
|
|
@@ -32,22 +125,26 @@ const ENTRY_PATHS = {
|
|
|
32
125
|
* message.
|
|
33
126
|
*/
|
|
34
127
|
export function checkV2(raw) {
|
|
35
|
-
const unknownKey =
|
|
128
|
+
const unknownKey = findUnknownKey(raw, V2_KEYS);
|
|
36
129
|
if (unknownKey)
|
|
37
130
|
return refused(unknownKey);
|
|
38
131
|
const id = str(raw.id);
|
|
39
132
|
if (!id || !ID_PATTERN.test(id)) {
|
|
40
133
|
return refused("The study id must be a public-safe token starting with a letter or digit (/^[A-Za-z0-9][A-Za-z0-9_.-]*$/).");
|
|
41
134
|
}
|
|
135
|
+
// The v2 parser checked `subject.topology` right after `subject.source`.
|
|
136
|
+
const topology = hasSubjectSource(raw.subject) ? readTopology(raw.subject) : undefined;
|
|
137
|
+
if (topology?.ok === false)
|
|
138
|
+
return topology;
|
|
42
139
|
const subject = parseSubject(raw.subject);
|
|
43
140
|
if (!subject.ok)
|
|
44
141
|
return refused(subject.error.message);
|
|
45
142
|
const actor = readActor(raw.actors);
|
|
46
143
|
if (!actor.ok)
|
|
47
144
|
return actor;
|
|
48
|
-
const execution =
|
|
145
|
+
const execution = readExecution(raw.execution);
|
|
49
146
|
if (!execution.ok)
|
|
50
|
-
return
|
|
147
|
+
return execution;
|
|
51
148
|
const scenario = readScenario(raw.scenario);
|
|
52
149
|
if (!scenario.ok)
|
|
53
150
|
return scenario;
|
|
@@ -71,22 +168,26 @@ export function checkV2(raw) {
|
|
|
71
168
|
if (!comms.ok)
|
|
72
169
|
return refused(comms.error.message);
|
|
73
170
|
const composition = {
|
|
74
|
-
schema:
|
|
171
|
+
schema: STUDY_SCHEMA,
|
|
75
172
|
id,
|
|
173
|
+
// A shared world is declared; any other route is derived below.
|
|
174
|
+
route: topology?.value === "shared-world" ? "shared-world" : "computer-use",
|
|
76
175
|
subject: subject.value,
|
|
77
|
-
|
|
78
|
-
...(execution.value === undefined ? {} : { execution: execution.value }),
|
|
176
|
+
actor: { type: actor.value.type, ...actor.value.fields },
|
|
177
|
+
...(execution.value.execution === undefined ? {} : { execution: execution.value.execution }),
|
|
79
178
|
...(policies.value === undefined ? {} : { policies: policies.value }),
|
|
80
179
|
...(comms.value === undefined ? {} : { comms: comms.value }),
|
|
81
180
|
};
|
|
82
181
|
const route = v2RouteOf(composition);
|
|
182
|
+
composition.route = route;
|
|
83
183
|
const study = {
|
|
84
184
|
route,
|
|
85
185
|
composition,
|
|
186
|
+
topology: topology?.value,
|
|
86
187
|
count: actor.value.count,
|
|
87
188
|
entries: actor.value.entries,
|
|
88
189
|
focus: actor.value.focus,
|
|
89
|
-
executionCaps: execution.value
|
|
190
|
+
executionCaps: execution.value.caps,
|
|
90
191
|
scenario: scenario.value,
|
|
91
192
|
personas: Array.isArray(raw.personas) && raw.personas.some(isRecord),
|
|
92
193
|
terminalTimeout: route === "terminal" && isRecord(raw.execution) && raw.execution.timeoutMs !== undefined,
|
|
@@ -107,7 +208,37 @@ export function checkV2(raw) {
|
|
|
107
208
|
* the shared-world checks' refusal, as it does from the v2 parser.
|
|
108
209
|
*/
|
|
109
210
|
function v2RouteOf(composition) {
|
|
110
|
-
return composition.
|
|
211
|
+
return composition.route === "shared-world" ? "shared-world" : routeOf(composition);
|
|
212
|
+
}
|
|
213
|
+
// parse/subject.ts read `subject.topology` as one of two values.
|
|
214
|
+
function readTopology(subject) {
|
|
215
|
+
if (!isRecord(subject) || subject.topology === undefined)
|
|
216
|
+
return { ok: true, value: undefined };
|
|
217
|
+
const topology = str(subject.topology);
|
|
218
|
+
if (topology !== "per-lane-worlds" && topology !== "shared-world") {
|
|
219
|
+
return refused("`subject.topology` must be per-lane-worlds (the default) or shared-world.");
|
|
220
|
+
}
|
|
221
|
+
return { ok: true, value: topology };
|
|
222
|
+
}
|
|
223
|
+
// parse/execution.ts parseExecution, which read `execution.caps` after `execution.desktop` and
|
|
224
|
+
// before `execution.terminal`.
|
|
225
|
+
function readExecution(raw) {
|
|
226
|
+
let caps;
|
|
227
|
+
if (isRecord(raw)) {
|
|
228
|
+
const { target, timeoutMs, completionTimeoutMs, concurrency, desktop } = raw;
|
|
229
|
+
const head = parseExecution({ target, timeoutMs, completionTimeoutMs, concurrency, desktop });
|
|
230
|
+
if (!head.ok)
|
|
231
|
+
return refused(head.error.message);
|
|
232
|
+
const parsed = parseCaps(raw.caps);
|
|
233
|
+
if (!parsed.ok)
|
|
234
|
+
return refused(parsed.error.message);
|
|
235
|
+
caps = parsed.value;
|
|
236
|
+
}
|
|
237
|
+
// parseExecution reads every key but `caps`.
|
|
238
|
+
const execution = parseExecution(raw);
|
|
239
|
+
if (!execution.ok)
|
|
240
|
+
return refused(execution.error.message);
|
|
241
|
+
return { ok: true, value: { execution: execution.value, caps } };
|
|
111
242
|
}
|
|
112
243
|
// parse/actors.ts parseActors, for the one actor it accepts.
|
|
113
244
|
function readActor(raw) {
|
|
@@ -252,7 +383,7 @@ export function v2ShapeReason(study) {
|
|
|
252
383
|
const { route, composition, count, entries, focus } = study;
|
|
253
384
|
if (route === "scripted") {
|
|
254
385
|
// composition-rules.ts scriptedBrowserValidationReason, in its order.
|
|
255
|
-
if (composition.subject.source === "clone" &&
|
|
386
|
+
if (composition.subject.source === "clone" && study.topology !== undefined) {
|
|
256
387
|
return "A clone scripted-browser study does not support `subject.topology` yet: it runs one synthetic subject for its scripted actor. Remove `subject.topology`.";
|
|
257
388
|
}
|
|
258
389
|
if ((count ?? 1) > 2) {
|
|
@@ -280,13 +411,13 @@ export function v2ShapeReason(study) {
|
|
|
280
411
|
const caps = study.scenario?.caps;
|
|
281
412
|
if (composition.subject.source === "app-url" &&
|
|
282
413
|
composition.execution?.target === "local" &&
|
|
283
|
-
composition.
|
|
414
|
+
composition.actor.type === "local-agent" &&
|
|
284
415
|
(caps?.maxUsd !== undefined || caps?.maxTotalUsd !== undefined)) {
|
|
285
416
|
return "Local Codex participants currently use gpt-6-astra at low effort. Account dollar/output-token caps are unavailable; use API participants for those controls.";
|
|
286
417
|
}
|
|
287
418
|
return undefined;
|
|
288
419
|
}
|
|
289
|
-
// The caps keys each route reads, as
|
|
420
|
+
// The caps keys each route reads, as parse/front.ts CAPS_KEYS has them.
|
|
290
421
|
const CAPS_READ = {
|
|
291
422
|
preview: [],
|
|
292
423
|
"computer-use": ["maxUsd", "maxTotalUsd"],
|
|
@@ -394,7 +525,7 @@ const V2_DROP_ROWS = [
|
|
|
394
525
|
},
|
|
395
526
|
{
|
|
396
527
|
path: "subject.topology",
|
|
397
|
-
applies: (study, routes) => !routes.shared && study.
|
|
528
|
+
applies: (study, routes) => !routes.shared && study.topology !== undefined,
|
|
398
529
|
},
|
|
399
530
|
{
|
|
400
531
|
path: "execution.caps",
|
|
@@ -458,15 +589,34 @@ const DROP_ORDER = [
|
|
|
458
589
|
"review.scorer",
|
|
459
590
|
"personas",
|
|
460
591
|
];
|
|
592
|
+
/**
|
|
593
|
+
* The v2 path of a field parseStudyV3 reports in report mode. Each of these keys exists in both
|
|
594
|
+
* formats: `actor.<key>` is `actors[0].<key>`, a `participants` entry's key is
|
|
595
|
+
* `actors[0].lanes[].<key>` (field-paths.ts finds it under `roster` when the file has one), `mode`
|
|
596
|
+
* is `scenario.mode` and `scenario` is `scenario.ref`. Every other path is the same.
|
|
597
|
+
*/
|
|
598
|
+
function v2PathOf(path) {
|
|
599
|
+
if (path.startsWith("actor."))
|
|
600
|
+
return `actors[0].${path.slice("actor.".length)}`;
|
|
601
|
+
if (path.startsWith("participants[]")) {
|
|
602
|
+
return `actors[0].lanes[]${path.slice("participants[]".length)}`;
|
|
603
|
+
}
|
|
604
|
+
if (path === "mode")
|
|
605
|
+
return "scenario.mode";
|
|
606
|
+
if (path === "scenario")
|
|
607
|
+
return "scenario.ref";
|
|
608
|
+
return path;
|
|
609
|
+
}
|
|
461
610
|
/**
|
|
462
611
|
* The keys migrate drops: the v2-only rows above and `inert`, the fields parseStudyV3 reports the
|
|
463
|
-
* route does not read, then terminal `execution.timeoutMs`,
|
|
464
|
-
* come before their children, so a child under a dropped
|
|
612
|
+
* route does not read (v3 paths, mapped back to the file's), then terminal `execution.timeoutMs`,
|
|
613
|
+
* which no terminal code reads. Parents come before their children, so a child under a dropped
|
|
614
|
+
* parent is reported with the parent.
|
|
465
615
|
*/
|
|
466
616
|
export function droppedPaths(study, inert) {
|
|
467
617
|
const routes = routesOf(study);
|
|
468
618
|
const found = new Set([
|
|
469
|
-
...inert,
|
|
619
|
+
...inert.map(v2PathOf),
|
|
470
620
|
...V2_DROP_ROWS.filter((row) => row.applies(study, routes)).map((row) => row.path),
|
|
471
621
|
]);
|
|
472
622
|
const paths = [
|
|
@@ -1,56 +1,35 @@
|
|
|
1
|
-
import type { StudyActor, StudyParticipantEntry,
|
|
1
|
+
import type { StudyActor, StudyParticipantEntry, StudyParseFailure } from "../types.js";
|
|
2
2
|
export declare const PARTICIPANT_ID_PATTERN: RegExp;
|
|
3
3
|
export declare const PARTICIPANT_ID_MAX_CHARS = 40;
|
|
4
|
-
/** The participant entries an actor declares, after roster groups expand into them. The manifest
|
|
5
|
-
* spells them `actors[].lanes`. Undefined when the actor declares none. */
|
|
6
|
-
export declare function rosterOf<Entry = StudyParticipantEntry>(actor: {
|
|
7
|
-
readonly lanes?: readonly Entry[];
|
|
8
|
-
} | undefined): readonly Entry[] | undefined;
|
|
9
|
-
/** An actor's per-participant focus on the app-url route. The manifest spells it `laneFocus`. */
|
|
10
|
-
export declare function focusOf(actor: Pick<StudyActor, "laneFocus"> | undefined): StudyParticipantFocus | undefined;
|
|
11
|
-
export declare function parseActors(raw: unknown): {
|
|
12
|
-
ok: true;
|
|
13
|
-
value: StudyActor[];
|
|
14
|
-
} | StudyParseFailure;
|
|
15
4
|
/** An actor's `type`, which a study route must run. `path` names the actor in messages. */
|
|
16
5
|
export declare function parseActorType(entry: Record<string, unknown>, path: string): {
|
|
17
6
|
ok: true;
|
|
18
7
|
value: string;
|
|
19
8
|
} | StudyParseFailure;
|
|
20
|
-
/** An actor's keys other than `type
|
|
21
|
-
type ActorFields = Omit<
|
|
9
|
+
/** An actor's keys other than `type`. */
|
|
10
|
+
type ActorFields = Omit<StudyActor, "type">;
|
|
22
11
|
/**
|
|
23
|
-
* Every actor key other than `type
|
|
24
|
-
*
|
|
12
|
+
* Every actor key other than `type`. The parser reads them after the participant entries, so a file
|
|
13
|
+
* with errors in both reports the entry first.
|
|
25
14
|
*/
|
|
26
15
|
export declare function parseActorFields(entry: Record<string, unknown>, path: string): {
|
|
27
16
|
ok: true;
|
|
28
17
|
value: ActorFields;
|
|
29
18
|
} | StudyParseFailure;
|
|
30
|
-
/** The participants a v2 actor declares, which a v3 study declares outside its actor. */
|
|
31
|
-
interface ActorParticipants {
|
|
32
|
-
readonly count?: number | undefined;
|
|
33
|
-
readonly entries?: StudyParticipantEntry[] | undefined;
|
|
34
|
-
readonly instruction?: string | undefined;
|
|
35
|
-
}
|
|
36
|
-
/** A v3 study's actor in the v2 spelling: the actor holds its participants. */
|
|
37
|
-
export declare function withParticipants(actor: StudyV3["actor"], { count, entries, instruction }: ActorParticipants): StudyActor;
|
|
38
|
-
/** A v2 actor without the keys that declare its participants: the actor of a v3 study. */
|
|
39
|
-
export declare function withoutParticipants(actor: StudyActor): StudyV3["actor"];
|
|
40
19
|
/** How a participant list's messages name the list and each entry in it. */
|
|
41
20
|
export interface ParticipantPaths {
|
|
42
21
|
readonly list: string;
|
|
43
22
|
readonly entry: (index: number) => string;
|
|
44
23
|
}
|
|
45
24
|
/**
|
|
46
|
-
* Parse a participant list into a fan-out roster (computer-use E2B route): `
|
|
47
|
-
*
|
|
48
|
-
* each entry is `{ id?, actorType?, surface?, caseGroup?, persona?, device?,
|
|
49
|
-
* entry?, host?, reasoningEffort?, stopWhen?, dwell? }`.
|
|
25
|
+
* Parse a participant list into a fan-out roster (computer-use E2B route): `participants` in a
|
|
26
|
+
* study, `actors[0].lanes` in the v2 file migrate reads, each named in messages by `paths`.
|
|
27
|
+
* Structural only: each entry is `{ id?, actorType?, surface?, caseGroup?, persona?, device?,
|
|
28
|
+
* instruction?, target?, entry?, host?, reasoningEffort?, stopWhen?, dwell? }`.
|
|
50
29
|
* Participant ids (when declared) must be public-safe path tokens and unique; grouping metadata
|
|
51
30
|
* must be public-safe tokens; a device must be a known preset name. The
|
|
52
|
-
* route-scoped cross-validation (
|
|
53
|
-
*
|
|
31
|
+
* route-scoped cross-validation (device XOR raw resolution, targets, cap 16) runs in parseStudy
|
|
32
|
+
* where the route is known.
|
|
54
33
|
*/
|
|
55
34
|
export declare function parseParticipantEntries(raw: unknown, paths: ParticipantPaths): {
|
|
56
35
|
ok: true;
|
|
@@ -4,24 +4,15 @@ import { isMaxOutputTokens } from "../../actors/output-token-limit.js";
|
|
|
4
4
|
import { isHttpUrl } from "./subject.js";
|
|
5
5
|
import { entryCredentialReason, urlCredentialReason } from "./url-credentials.js";
|
|
6
6
|
import { registeredComputerUseActors, registeredScriptedBrowserActors, registeredTerminalActors, } from "../routing.js";
|
|
7
|
-
import { invalid,
|
|
7
|
+
import { invalid, str } from "./values.js";
|
|
8
8
|
import { isRecord } from "../../run/type-guards.js";
|
|
9
9
|
// A participant id interpolates into its evidence paths (screenshots/<id>/, actors/<id>.json), so
|
|
10
10
|
// it must be a public-safe path token, same shape as a study id.
|
|
11
11
|
export const PARTICIPANT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
12
12
|
export const PARTICIPANT_ID_MAX_CHARS = 40;
|
|
13
13
|
const METADATA_MAX_CHARS = 80;
|
|
14
|
-
/** The participant entries an actor declares, after roster groups expand into them. The manifest
|
|
15
|
-
* spells them `actors[].lanes`. Undefined when the actor declares none. */
|
|
16
|
-
export function rosterOf(actor) {
|
|
17
|
-
return actor?.lanes;
|
|
18
|
-
}
|
|
19
|
-
/** An actor's per-participant focus on the app-url route. The manifest spells it `laneFocus`. */
|
|
20
|
-
export function focusOf(actor) {
|
|
21
|
-
return actor?.laneFocus;
|
|
22
|
-
}
|
|
23
14
|
// Actor ids humanish no longer registers. Rejecting them at parse keeps a study that names one
|
|
24
|
-
// from running on a route that ignores
|
|
15
|
+
// from running on a route that ignores actor.type, such as a this-repo dry run.
|
|
25
16
|
const REMOVED_ACTOR_TYPES = new Set(["pi-agent-core", "claude-agent-sdk"]);
|
|
26
17
|
// Only actors of a run kind a route dispatches. codex-app-server is registered but declares only
|
|
27
18
|
// the "code" run kind, which no study route runs, so naming it would lead to the same dead end.
|
|
@@ -32,56 +23,6 @@ function routableActorTypes() {
|
|
|
32
23
|
...registeredTerminalActors(),
|
|
33
24
|
];
|
|
34
25
|
}
|
|
35
|
-
export function parseActors(raw) {
|
|
36
|
-
if (!Array.isArray(raw) || raw.length === 0) {
|
|
37
|
-
return invalid("`actors` must be a non-empty array.");
|
|
38
|
-
}
|
|
39
|
-
// Multi-actor fan-out is not wired yet (only actors[0] is consumed). Fail closed rather than
|
|
40
|
-
// silently ignore actors[1..]; multi-actor support lands in a later slice.
|
|
41
|
-
if (raw.length > 1) {
|
|
42
|
-
return invalid("Multiple actors are not supported yet (only the first actor runs); declare a single actor.");
|
|
43
|
-
}
|
|
44
|
-
const actors = [];
|
|
45
|
-
for (const [index, entry] of raw.entries()) {
|
|
46
|
-
if (!isRecord(entry)) {
|
|
47
|
-
return invalid(`actors[${index}] must be an object.`);
|
|
48
|
-
}
|
|
49
|
-
const path = `actors[${index}]`;
|
|
50
|
-
const type = parseActorType(entry, path);
|
|
51
|
-
if (!type.ok)
|
|
52
|
-
return type;
|
|
53
|
-
const actor = { type: type.value };
|
|
54
|
-
const count = posInt(entry.count);
|
|
55
|
-
if (count !== undefined)
|
|
56
|
-
actor.count = count;
|
|
57
|
-
if (entry.lanes !== undefined && entry.roster !== undefined) {
|
|
58
|
-
return invalid(`Set either actors[${index}].lanes or actors[${index}].roster, not both: list participants one by one in \`lanes\`, or in groups in \`roster\`.`);
|
|
59
|
-
}
|
|
60
|
-
if (entry.roster !== undefined && count !== undefined) {
|
|
61
|
-
return invalid(`Set either actors[${index}].roster or actors[${index}].count, not both: \`roster\` declares groups of distinct participants, and \`count\` declares identical ones.`);
|
|
62
|
-
}
|
|
63
|
-
if (entry.roster !== undefined && entry.laneFocus !== undefined) {
|
|
64
|
-
return invalid(`actors[${index}].roster and actors[${index}].laneFocus are mutually exclusive: a roster group's instruction is each participant's steer.`);
|
|
65
|
-
}
|
|
66
|
-
const rosterResult = entry.roster !== undefined
|
|
67
|
-
? parseRosterGroups(entry.roster, index)
|
|
68
|
-
: parseParticipantEntries(entry.lanes, lanePaths(index));
|
|
69
|
-
if (!rosterResult.ok) {
|
|
70
|
-
return rosterResult;
|
|
71
|
-
}
|
|
72
|
-
if (rosterResult.value)
|
|
73
|
-
actor.lanes = rosterResult.value;
|
|
74
|
-
const fields = parseActorFields(entry, path);
|
|
75
|
-
if (!fields.ok)
|
|
76
|
-
return fields;
|
|
77
|
-
Object.assign(actor, fields.value);
|
|
78
|
-
const focus = parseFocus(entry.laneFocus);
|
|
79
|
-
if (focus)
|
|
80
|
-
actor.laneFocus = focus;
|
|
81
|
-
actors.push(actor);
|
|
82
|
-
}
|
|
83
|
-
return { ok: true, value: actors };
|
|
84
|
-
}
|
|
85
26
|
/** An actor's `type`, which a study route must run. `path` names the actor in messages. */
|
|
86
27
|
export function parseActorType(entry, path) {
|
|
87
28
|
const type = str(entry.type);
|
|
@@ -94,8 +35,8 @@ export function parseActorType(entry, path) {
|
|
|
94
35
|
return { ok: true, value: type };
|
|
95
36
|
}
|
|
96
37
|
/**
|
|
97
|
-
* Every actor key other than `type
|
|
98
|
-
*
|
|
38
|
+
* Every actor key other than `type`. The parser reads them after the participant entries, so a file
|
|
39
|
+
* with errors in both reports the entry first.
|
|
99
40
|
*/
|
|
100
41
|
export function parseActorFields(entry, path) {
|
|
101
42
|
const actor = {};
|
|
@@ -143,101 +84,15 @@ export function parseActorFields(entry, path) {
|
|
|
143
84
|
actor.tasks = tasksResult.value;
|
|
144
85
|
return { ok: true, value: actor };
|
|
145
86
|
}
|
|
146
|
-
/** A v3 study's actor in the v2 spelling: the actor holds its participants. */
|
|
147
|
-
export function withParticipants(actor, { count, entries, instruction }) {
|
|
148
|
-
const { type, ...fields } = actor;
|
|
149
|
-
return {
|
|
150
|
-
type,
|
|
151
|
-
...(count === undefined ? {} : { count }),
|
|
152
|
-
...(entries === undefined ? {} : { lanes: entries }),
|
|
153
|
-
...fields,
|
|
154
|
-
...(instruction === undefined ? {} : { laneFocus: { instruction } }),
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
/** A v2 actor without the keys that declare its participants: the actor of a v3 study. */
|
|
158
|
-
export function withoutParticipants(actor) {
|
|
159
|
-
const fields = { ...actor };
|
|
160
|
-
delete fields.count;
|
|
161
|
-
delete fields.lanes;
|
|
162
|
-
delete fields.laneFocus;
|
|
163
|
-
return fields;
|
|
164
|
-
}
|
|
165
|
-
function parseFocus(raw) {
|
|
166
|
-
if (!isRecord(raw)) {
|
|
167
|
-
return undefined;
|
|
168
|
-
}
|
|
169
|
-
const focus = {};
|
|
170
|
-
const id = str(raw.id);
|
|
171
|
-
if (id)
|
|
172
|
-
focus.id = id;
|
|
173
|
-
const label = str(raw.label);
|
|
174
|
-
if (label)
|
|
175
|
-
focus.label = label;
|
|
176
|
-
const instruction = str(raw.instruction);
|
|
177
|
-
if (instruction)
|
|
178
|
-
focus.instruction = instruction;
|
|
179
|
-
return Object.keys(focus).length > 0 ? focus : undefined;
|
|
180
|
-
}
|
|
181
|
-
/**
|
|
182
|
-
* Parse `actors[index].roster` compact groups into concrete `lanes[]` entries. This is authoring sugar for
|
|
183
|
-
* "N users of M adapter-owned types across S surfaces"; the runtime receives only `lanes[]`.
|
|
184
|
-
*/
|
|
185
|
-
function parseRosterGroups(raw, actorIndex) {
|
|
186
|
-
if (raw === undefined) {
|
|
187
|
-
return { ok: true, value: undefined };
|
|
188
|
-
}
|
|
189
|
-
if (!Array.isArray(raw) || raw.length === 0) {
|
|
190
|
-
return invalid(`actors[${actorIndex}].roster must be a non-empty array of group objects ({ id, count, actorType?, surface?, caseGroup?, persona?, device?, instruction?, target?, entry?, host?, reasoningEffort?, stopWhen?, dwell? }) when set.`);
|
|
191
|
-
}
|
|
192
|
-
const expanded = [];
|
|
193
|
-
const seenGroupIds = new Set();
|
|
194
|
-
for (const [groupIndex, entry] of raw.entries()) {
|
|
195
|
-
if (!isRecord(entry)) {
|
|
196
|
-
return invalid(`actors[${actorIndex}].roster[${groupIndex}] must be an object ({ id, count, actorType?, surface?, caseGroup?, persona?, device?, instruction?, target?, entry?, host?, reasoningEffort?, stopWhen?, dwell? }).`);
|
|
197
|
-
}
|
|
198
|
-
const groupId = str(entry.id);
|
|
199
|
-
if (groupId === undefined) {
|
|
200
|
-
return invalid(`actors[${actorIndex}].roster[${groupIndex}].id is required and must be a public-safe token matching ${PARTICIPANT_ID_PATTERN}.`);
|
|
201
|
-
}
|
|
202
|
-
if (!PARTICIPANT_ID_PATTERN.test(groupId) || groupId.length > PARTICIPANT_ID_MAX_CHARS - 3) {
|
|
203
|
-
return invalid(`actors[${actorIndex}].roster[${groupIndex}].id must match ${PARTICIPANT_ID_PATTERN} and be at most ${PARTICIPANT_ID_MAX_CHARS - 3} characters, because each generated participant id adds a suffix like "-01"; "${groupId}" is not.`);
|
|
204
|
-
}
|
|
205
|
-
if (seenGroupIds.has(groupId)) {
|
|
206
|
-
return invalid(`actors[${actorIndex}].roster group ids must be unique (duplicate "${groupId}").`);
|
|
207
|
-
}
|
|
208
|
-
seenGroupIds.add(groupId);
|
|
209
|
-
const count = posInt(entry.count);
|
|
210
|
-
if (count === undefined) {
|
|
211
|
-
return invalid(`actors[${actorIndex}].roster[${groupIndex}].count is required and must be a positive integer.`);
|
|
212
|
-
}
|
|
213
|
-
const groupEntryInput = { ...entry };
|
|
214
|
-
delete groupEntryInput.id;
|
|
215
|
-
delete groupEntryInput.count;
|
|
216
|
-
for (let i = 1; i <= count; i += 1) {
|
|
217
|
-
expanded.push({
|
|
218
|
-
...groupEntryInput,
|
|
219
|
-
id: `${groupId}-${String(i).padStart(2, "0")}`,
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
return parseParticipantEntries(expanded, lanePaths(actorIndex));
|
|
224
|
-
}
|
|
225
|
-
// The v2 spelling, `actors[0].lanes[3]`. A roster group's participants are named the same way.
|
|
226
|
-
function lanePaths(actorIndex) {
|
|
227
|
-
return {
|
|
228
|
-
list: `actors[${actorIndex}].lanes`,
|
|
229
|
-
entry: (index) => `actors[${actorIndex}].lanes[${index}]`,
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
87
|
/**
|
|
233
|
-
* Parse a participant list into a fan-out roster (computer-use E2B route): `
|
|
234
|
-
*
|
|
235
|
-
* each entry is `{ id?, actorType?, surface?, caseGroup?, persona?, device?,
|
|
236
|
-
* entry?, host?, reasoningEffort?, stopWhen?, dwell? }`.
|
|
88
|
+
* Parse a participant list into a fan-out roster (computer-use E2B route): `participants` in a
|
|
89
|
+
* study, `actors[0].lanes` in the v2 file migrate reads, each named in messages by `paths`.
|
|
90
|
+
* Structural only: each entry is `{ id?, actorType?, surface?, caseGroup?, persona?, device?,
|
|
91
|
+
* instruction?, target?, entry?, host?, reasoningEffort?, stopWhen?, dwell? }`.
|
|
237
92
|
* Participant ids (when declared) must be public-safe path tokens and unique; grouping metadata
|
|
238
93
|
* must be public-safe tokens; a device must be a known preset name. The
|
|
239
|
-
* route-scoped cross-validation (
|
|
240
|
-
*
|
|
94
|
+
* route-scoped cross-validation (device XOR raw resolution, targets, cap 16) runs in parseStudy
|
|
95
|
+
* where the route is known.
|
|
241
96
|
*/
|
|
242
97
|
export function parseParticipantEntries(raw, paths) {
|
|
243
98
|
if (raw === undefined) {
|
|
@@ -415,7 +270,7 @@ function parseDwell(raw, field) {
|
|
|
415
270
|
...(whenResult.value === undefined ? {} : { when: whenResult.value }),
|
|
416
271
|
ms,
|
|
417
272
|
everyMs,
|
|
418
|
-
// oxlint-disable-next-line unicorn/no-thenable -- `then` is the documented dwell field of
|
|
273
|
+
// oxlint-disable-next-line unicorn/no-thenable -- `then` is the documented dwell field of a study
|
|
419
274
|
then,
|
|
420
275
|
},
|
|
421
276
|
};
|
|
@@ -76,9 +76,13 @@ function parseCommsEmail(raw) {
|
|
|
76
76
|
}
|
|
77
77
|
return { ok: true, value: email };
|
|
78
78
|
}
|
|
79
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* A real inbox: a saved connection, with optional link and allowed origins and nothing else. The
|
|
81
|
+
* parsed config adds `kind: real`, so a config parseStudy returned parses again with it.
|
|
82
|
+
*/
|
|
80
83
|
function parseEmailConnection(raw) {
|
|
81
|
-
const unsupported = Object.keys(raw).filter((key) => !["connection", "linkOrigin", "allowedOrigins"].includes(key)
|
|
84
|
+
const unsupported = Object.keys(raw).filter((key) => !["connection", "linkOrigin", "allowedOrigins"].includes(key) &&
|
|
85
|
+
!(key === "kind" && raw.kind === "real"));
|
|
82
86
|
if (unsupported.length)
|
|
83
87
|
return invalid("An email connection cannot be mixed with capture settings, kind, recipients, or provider options. Use only connection, optional linkOrigin and allowedOrigins.");
|
|
84
88
|
if (typeof raw.connection !== "string" || !/^[a-z][a-z0-9-]{0,47}$/.test(raw.connection))
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import type { StudyParseFailure, StudyDefaults, StudyExecution, StudyPolicies, StudyReview
|
|
1
|
+
import type { StudyCaps, StudyParseFailure, StudyDefaults, StudyExecution, StudyPolicies, StudyReview } from "../types.js";
|
|
2
2
|
export declare function parseExecution(raw: unknown): {
|
|
3
3
|
ok: true;
|
|
4
4
|
value: StudyExecution | undefined;
|
|
5
5
|
} | StudyParseFailure;
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Parse a `caps` block's values. Returns a parse failure on a malformed value rather than silently
|
|
8
|
+
* dropping a budget declaration (a cap that silently does nothing would claim protection it
|
|
9
|
+
* does not give). Each cap must be a non-negative finite number. readStudyFront has checked the
|
|
10
|
+
* keys against the route; migrate checks a v2 file's two caps blocks with it.
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseCaps(raw: unknown): {
|
|
8
13
|
ok: true;
|
|
9
|
-
value:
|
|
14
|
+
value: StudyCaps | undefined;
|
|
10
15
|
} | StudyParseFailure;
|
|
11
16
|
export declare function parsePolicies(raw: unknown): {
|
|
12
17
|
ok: true;
|