humanish 0.110.2 → 0.110.4
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/ARCHITECTURE.md +10 -15
- package/CHANGELOG.md +27 -0
- package/CONTEXT.md +1 -1
- package/dist/actors/computer-use/actor.js +2 -2
- package/dist/actors/computer-use/loop/ending.js +1 -1
- package/dist/actors/registry.js +1 -1
- package/dist/actors/scripted-browser/actor.js +17 -8
- package/dist/actors/scripted-browser/launch-env.d.ts +12 -0
- package/dist/actors/scripted-browser/launch-env.js +50 -0
- package/dist/analysis/automatic-completion.d.ts +2 -1
- package/dist/analysis/automatic-completion.js +15 -2
- package/dist/cli/commands/migrate.js +1 -1
- package/dist/cli/commands/observe.js +3 -2
- package/dist/cli/commands/runs.js +5 -2
- package/dist/cli/commands/study-route-computer-use.js +3 -2
- package/dist/cli/commands/study-route-preview.js +2 -1
- package/dist/cli/commands/study-route-shared-world.js +3 -3
- package/dist/cli/commands/study-run.js +9 -19
- package/dist/cli/commands/study.js +4 -1
- package/dist/cli/findings.d.ts +5 -0
- package/dist/cli/findings.js +21 -0
- package/dist/cli/io.d.ts +2 -1
- package/dist/cli/io.js +3 -2
- package/dist/cli/observer-follow.d.ts +6 -2
- package/dist/cli/observer-follow.js +15 -5
- package/dist/evidence/redaction.js +15 -4
- package/dist/routes/computer-use/live-phase.js +8 -1
- package/dist/routes/computer-use/local-vm.js +2 -1
- package/dist/routes/computer-use/participant-runs.d.ts +6 -9
- package/dist/routes/computer-use/participant-runs.js +16 -36
- package/dist/routes/computer-use/plan.d.ts +2 -2
- package/dist/routes/computer-use/plan.js +7 -6
- package/dist/routes/computer-use/route.d.ts +5 -18
- package/dist/routes/computer-use/route.js +19 -79
- package/dist/routes/computer-use/setup.d.ts +3 -2
- package/dist/routes/computer-use/setup.js +2 -1
- package/dist/routes/computer-use/types.d.ts +6 -10
- package/dist/routes/scripted/plan.js +5 -4
- package/dist/routes/scripted/route.d.ts +4 -17
- package/dist/routes/scripted/route.js +15 -43
- package/dist/routes/scripted/surface-sessions.d.ts +2 -0
- package/dist/routes/scripted/surface-sessions.js +2 -1
- package/dist/routes/scripted/types.d.ts +1 -6
- package/dist/routes/shared-world/participant-specs.js +5 -1
- package/dist/routes/shared-world/plan.js +7 -6
- package/dist/routes/shared-world/route.d.ts +5 -11
- package/dist/routes/shared-world/route.js +27 -66
- package/dist/routes/shared-world/types.d.ts +2 -6
- package/dist/routes/terminal/plan.js +4 -3
- package/dist/routes/terminal/route.d.ts +3 -13
- package/dist/routes/terminal/route.js +21 -60
- package/dist/routes/terminal/session.d.ts +2 -3
- package/dist/routes/terminal/session.js +3 -3
- package/dist/routes/terminal/types.d.ts +2 -7
- package/dist/run/display.d.ts +10 -0
- package/dist/run/display.js +22 -0
- package/dist/run/reclaim.d.ts +4 -2
- package/dist/run/reclaim.js +61 -18
- package/dist/run/route-shell.d.ts +60 -0
- package/dist/run/route-shell.js +51 -0
- package/dist/run/run.d.ts +16 -1
- package/dist/run/run.js +33 -5
- package/dist/run/status.d.ts +8 -0
- package/dist/run/status.js +6 -0
- package/dist/run/stored-runs.js +26 -11
- package/dist/study/composition-rules.js +33 -33
- package/dist/study/config.d.ts +9 -2
- package/dist/study/config.js +14 -5
- package/dist/study/doctor.js +5 -4
- package/dist/study/init-templates.d.ts +1 -1
- package/dist/study/init-templates.js +43 -34
- package/dist/study/init.js +1 -1
- package/dist/study/{convert.d.ts → migrate/convert.d.ts} +1 -1
- package/dist/study/{convert.js → migrate/convert.js} +29 -31
- package/dist/study/{migrate.d.ts → migrate/migrate.d.ts} +1 -1
- package/dist/study/{migrate.js → migrate/migrate.js} +2 -2
- package/dist/study/{owned-files.js → migrate/owned-files.js} +1 -1
- package/dist/study/migrate/v2.d.ts +68 -0
- package/dist/study/migrate/v2.js +480 -0
- package/dist/study/parse/actors.d.ts +46 -1
- package/dist/study/parse/actors.js +116 -69
- package/dist/study/parse/execution.js +2 -2
- package/dist/study/parse/study-v3.d.ts +26 -0
- package/dist/study/parse/study-v3.js +53 -35
- package/dist/study/parse/study.d.ts +38 -0
- package/dist/study/parse/study.js +225 -0
- package/dist/study/parse/subject.js +1 -1
- package/dist/study/persona-resolve.d.ts +2 -4
- package/dist/study/persona-resolve.js +2 -1
- package/dist/study/plan-base.d.ts +1 -1
- package/dist/study/plan-base.js +5 -4
- package/dist/study/plan-participants.js +12 -11
- package/dist/study/plan.js +4 -3
- package/dist/study/preflight-probes.js +2 -2
- package/dist/study/preflight.js +2 -2
- package/dist/study/routing.js +12 -15
- package/dist/study/run-study-options.js +3 -3
- package/dist/study/study-fields.d.ts +28 -0
- package/dist/study/study-fields.js +51 -0
- package/dist/study/summary.js +13 -17
- package/dist/study/types.d.ts +40 -0
- package/dist/study/url-credentials.js +2 -1
- package/dist/study/validation.d.ts +2 -0
- package/dist/study/validation.js +46 -41
- package/dist/study/warnings.js +5 -4
- package/dist/substrates/local/runtime-config.js +9 -9
- package/dist/tui-app.js +3 -3
- package/docs/architecture/actor-contract.md +1 -1
- package/docs/architecture/external-public-shared-world.md +1 -1
- package/docs/architecture/terminal-product-route.md +13 -13
- package/docs/contracts/schemas.md +13 -4
- package/docs/contracts/study-analysis.md +5 -2
- package/docs/product/automatic-analysis.md +1 -1
- package/package.json +1 -1
- /package/dist/study/{owned-files.d.ts → migrate/owned-files.d.ts} +0 -0
|
@@ -46,14 +46,11 @@ export function parseActors(raw) {
|
|
|
46
46
|
if (!isRecord(entry)) {
|
|
47
47
|
return invalid(`actors[${index}] must be an object.`);
|
|
48
48
|
}
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return invalid(`actors[${index}].type "${type}" is no longer a humanish actor. Use an actor a study route runs (one of: ${routableActorTypes().join(", ")}). To drive a study with a signed-in Claude Code, use type: local-agent with localAgent: claude.`);
|
|
55
|
-
}
|
|
56
|
-
const actor = { type };
|
|
49
|
+
const path = `actors[${index}]`;
|
|
50
|
+
const type = parseActorType(entry, path);
|
|
51
|
+
if (!type.ok)
|
|
52
|
+
return type;
|
|
53
|
+
const actor = { type: type.value };
|
|
57
54
|
const count = posInt(entry.count);
|
|
58
55
|
if (count !== undefined)
|
|
59
56
|
actor.count = count;
|
|
@@ -68,54 +65,16 @@ export function parseActors(raw) {
|
|
|
68
65
|
}
|
|
69
66
|
const rosterResult = entry.roster !== undefined
|
|
70
67
|
? parseRosterGroups(entry.roster, index)
|
|
71
|
-
: parseParticipantEntries(entry.lanes, index);
|
|
68
|
+
: parseParticipantEntries(entry.lanes, lanePaths(index));
|
|
72
69
|
if (!rosterResult.ok) {
|
|
73
70
|
return rosterResult;
|
|
74
71
|
}
|
|
75
72
|
if (rosterResult.value)
|
|
76
73
|
actor.lanes = rosterResult.value;
|
|
77
|
-
const
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (mission)
|
|
82
|
-
actor.mission = mission;
|
|
83
|
-
const model = str(entry.model);
|
|
84
|
-
if (model)
|
|
85
|
-
actor.model = model;
|
|
86
|
-
if (entry.maxOutputTokens !== undefined) {
|
|
87
|
-
if (!isMaxOutputTokens(entry.maxOutputTokens))
|
|
88
|
-
return invalid(`actors[${index}].maxOutputTokens must be a positive safe integer.`);
|
|
89
|
-
actor.maxOutputTokens = entry.maxOutputTokens;
|
|
90
|
-
}
|
|
91
|
-
const localAgent = str(entry.localAgent);
|
|
92
|
-
if (entry.localAgent !== undefined) {
|
|
93
|
-
if (localAgent !== "codex" && localAgent !== "claude") {
|
|
94
|
-
return invalid(`actors[${index}].localAgent must be "codex" or "claude" (the locally signed-in CLI that drives the study).`);
|
|
95
|
-
}
|
|
96
|
-
actor.localAgent = localAgent;
|
|
97
|
-
}
|
|
98
|
-
if (entry.reasoningEffort !== undefined) {
|
|
99
|
-
if (!isReasoningEffort(entry.reasoningEffort)) {
|
|
100
|
-
return invalid(`actors[${index}].reasoningEffort must be one of: ${reasoningEffortNames()}. Support is model-dependent, so a level this model does not accept fails on the first turn rather than being silently downgraded.`);
|
|
101
|
-
}
|
|
102
|
-
actor.reasoningEffort = entry.reasoningEffort;
|
|
103
|
-
}
|
|
104
|
-
const stopWhenResult = parseStopWhen(entry.stopWhen, `actors[${index}].stopWhen`);
|
|
105
|
-
if (!stopWhenResult.ok)
|
|
106
|
-
return stopWhenResult;
|
|
107
|
-
if (stopWhenResult.value !== undefined)
|
|
108
|
-
actor.stopWhen = stopWhenResult.value;
|
|
109
|
-
const dwellResult = parseDwell(entry.dwell, `actors[${index}].dwell`);
|
|
110
|
-
if (!dwellResult.ok)
|
|
111
|
-
return dwellResult;
|
|
112
|
-
if (dwellResult.value !== undefined)
|
|
113
|
-
actor.dwell = dwellResult.value;
|
|
114
|
-
const tasksResult = parseTasks(entry.tasks, `actors[${index}].tasks`);
|
|
115
|
-
if (!tasksResult.ok)
|
|
116
|
-
return tasksResult;
|
|
117
|
-
if (tasksResult.value !== undefined)
|
|
118
|
-
actor.tasks = tasksResult.value;
|
|
74
|
+
const fields = parseActorFields(entry, path);
|
|
75
|
+
if (!fields.ok)
|
|
76
|
+
return fields;
|
|
77
|
+
Object.assign(actor, fields.value);
|
|
119
78
|
const focus = parseFocus(entry.laneFocus);
|
|
120
79
|
if (focus)
|
|
121
80
|
actor.laneFocus = focus;
|
|
@@ -123,6 +82,86 @@ export function parseActors(raw) {
|
|
|
123
82
|
}
|
|
124
83
|
return { ok: true, value: actors };
|
|
125
84
|
}
|
|
85
|
+
/** An actor's `type`, which a study route must run. `path` names the actor in messages. */
|
|
86
|
+
export function parseActorType(entry, path) {
|
|
87
|
+
const type = str(entry.type);
|
|
88
|
+
if (!type) {
|
|
89
|
+
return invalid(`${path}.type is required.`);
|
|
90
|
+
}
|
|
91
|
+
if (REMOVED_ACTOR_TYPES.has(type)) {
|
|
92
|
+
return invalid(`${path}.type "${type}" is no longer a humanish actor. Use an actor a study route runs (one of: ${routableActorTypes().join(", ")}). To drive a study with a signed-in Claude Code, use type: local-agent with localAgent: claude.`);
|
|
93
|
+
}
|
|
94
|
+
return { ok: true, value: type };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Every actor key other than `type` and the participant keys. The v2 parser reads them after the
|
|
98
|
+
* participant entries, so a file with errors in both reports the entry first.
|
|
99
|
+
*/
|
|
100
|
+
export function parseActorFields(entry, path) {
|
|
101
|
+
const actor = {};
|
|
102
|
+
const persona = str(entry.persona);
|
|
103
|
+
if (persona)
|
|
104
|
+
actor.persona = persona;
|
|
105
|
+
const mission = str(entry.mission);
|
|
106
|
+
if (mission)
|
|
107
|
+
actor.mission = mission;
|
|
108
|
+
const model = str(entry.model);
|
|
109
|
+
if (model)
|
|
110
|
+
actor.model = model;
|
|
111
|
+
if (entry.maxOutputTokens !== undefined) {
|
|
112
|
+
if (!isMaxOutputTokens(entry.maxOutputTokens))
|
|
113
|
+
return invalid(`${path}.maxOutputTokens must be a positive safe integer.`);
|
|
114
|
+
actor.maxOutputTokens = entry.maxOutputTokens;
|
|
115
|
+
}
|
|
116
|
+
const localAgent = str(entry.localAgent);
|
|
117
|
+
if (entry.localAgent !== undefined) {
|
|
118
|
+
if (localAgent !== "codex" && localAgent !== "claude") {
|
|
119
|
+
return invalid(`${path}.localAgent must be "codex" or "claude" (the locally signed-in CLI that drives the study).`);
|
|
120
|
+
}
|
|
121
|
+
actor.localAgent = localAgent;
|
|
122
|
+
}
|
|
123
|
+
if (entry.reasoningEffort !== undefined) {
|
|
124
|
+
if (!isReasoningEffort(entry.reasoningEffort)) {
|
|
125
|
+
return invalid(`${path}.reasoningEffort must be one of: ${reasoningEffortNames()}. Support is model-dependent, so a level this model does not accept fails on the first turn rather than being silently downgraded.`);
|
|
126
|
+
}
|
|
127
|
+
actor.reasoningEffort = entry.reasoningEffort;
|
|
128
|
+
}
|
|
129
|
+
const stopWhenResult = parseStopWhen(entry.stopWhen, `${path}.stopWhen`);
|
|
130
|
+
if (!stopWhenResult.ok)
|
|
131
|
+
return stopWhenResult;
|
|
132
|
+
if (stopWhenResult.value !== undefined)
|
|
133
|
+
actor.stopWhen = stopWhenResult.value;
|
|
134
|
+
const dwellResult = parseDwell(entry.dwell, `${path}.dwell`);
|
|
135
|
+
if (!dwellResult.ok)
|
|
136
|
+
return dwellResult;
|
|
137
|
+
if (dwellResult.value !== undefined)
|
|
138
|
+
actor.dwell = dwellResult.value;
|
|
139
|
+
const tasksResult = parseTasks(entry.tasks, `${path}.tasks`);
|
|
140
|
+
if (!tasksResult.ok)
|
|
141
|
+
return tasksResult;
|
|
142
|
+
if (tasksResult.value !== undefined)
|
|
143
|
+
actor.tasks = tasksResult.value;
|
|
144
|
+
return { ok: true, value: actor };
|
|
145
|
+
}
|
|
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
|
+
}
|
|
126
165
|
function parseFocus(raw) {
|
|
127
166
|
if (!isRecord(raw)) {
|
|
128
167
|
return undefined;
|
|
@@ -181,10 +220,18 @@ function parseRosterGroups(raw, actorIndex) {
|
|
|
181
220
|
});
|
|
182
221
|
}
|
|
183
222
|
}
|
|
184
|
-
return parseParticipantEntries(expanded, actorIndex);
|
|
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
|
+
};
|
|
185
231
|
}
|
|
186
232
|
/**
|
|
187
|
-
* Parse
|
|
233
|
+
* Parse a participant list into a fan-out roster (computer-use E2B route): `actors[index].lanes` in
|
|
234
|
+
* a v2 file, `participants` in a v3 one, each named in messages by `paths`. Structural only:
|
|
188
235
|
* each entry is `{ id?, actorType?, surface?, caseGroup?, persona?, device?, instruction?, target?,
|
|
189
236
|
* entry?, host?, reasoningEffort?, stopWhen?, dwell? }`.
|
|
190
237
|
* Participant ids (when declared) must be public-safe path tokens and unique; grouping metadata
|
|
@@ -192,27 +239,27 @@ function parseRosterGroups(raw, actorIndex) {
|
|
|
192
239
|
* route-scoped cross-validation (`lanes` XOR `count`/`laneFocus`, device XOR raw resolution, cap 16)
|
|
193
240
|
* runs in parseStudy where the route is known.
|
|
194
241
|
*/
|
|
195
|
-
function parseParticipantEntries(raw,
|
|
242
|
+
export function parseParticipantEntries(raw, paths) {
|
|
196
243
|
if (raw === undefined) {
|
|
197
244
|
return { ok: true, value: undefined };
|
|
198
245
|
}
|
|
199
246
|
if (!Array.isArray(raw) || raw.length === 0) {
|
|
200
|
-
return invalid(
|
|
247
|
+
return invalid(`${paths.list} must be a non-empty array of participant objects ({ id?, actorType?, surface?, caseGroup?, persona?, device?, instruction?, target?, entry?, host?, reasoningEffort?, stopWhen?, dwell? }) when set.`);
|
|
201
248
|
}
|
|
202
249
|
const entries = [];
|
|
203
250
|
const seenIds = new Set();
|
|
204
251
|
for (const [entryIndex, entry] of raw.entries()) {
|
|
205
252
|
if (!isRecord(entry)) {
|
|
206
|
-
return invalid(
|
|
253
|
+
return invalid(`${paths.entry(entryIndex)} must be an object ({ id?, actorType?, surface?, caseGroup?, persona?, device?, instruction?, target?, entry?, host?, reasoningEffort?, stopWhen?, dwell? }).`);
|
|
207
254
|
}
|
|
208
255
|
const parsedEntry = {};
|
|
209
256
|
const id = str(entry.id);
|
|
210
257
|
if (id !== undefined) {
|
|
211
258
|
if (!PARTICIPANT_ID_PATTERN.test(id) || id.length > PARTICIPANT_ID_MAX_CHARS) {
|
|
212
|
-
return invalid(
|
|
259
|
+
return invalid(`${paths.entry(entryIndex)}.id must be a public-safe token matching ${PARTICIPANT_ID_PATTERN} and at most ${PARTICIPANT_ID_MAX_CHARS} chars (it names the participant's evidence paths); got "${id}".`);
|
|
213
260
|
}
|
|
214
261
|
if (seenIds.has(id)) {
|
|
215
|
-
return invalid(
|
|
262
|
+
return invalid(`${paths.list} ids must be unique (duplicate "${id}").`);
|
|
216
263
|
}
|
|
217
264
|
seenIds.add(id);
|
|
218
265
|
parsedEntry.id = id;
|
|
@@ -220,24 +267,24 @@ function parseParticipantEntries(raw, actorIndex) {
|
|
|
220
267
|
const device = str(entry.device);
|
|
221
268
|
if (device !== undefined) {
|
|
222
269
|
if (!isDevicePresetName(device)) {
|
|
223
|
-
return invalid(
|
|
270
|
+
return invalid(`${paths.entry(entryIndex)}.device must be one of: ${DEVICE_PRESET_NAMES.join(", ")}.`);
|
|
224
271
|
}
|
|
225
272
|
parsedEntry.device = device;
|
|
226
273
|
}
|
|
227
274
|
const persona = str(entry.persona);
|
|
228
275
|
if (persona !== undefined)
|
|
229
276
|
parsedEntry.persona = persona;
|
|
230
|
-
const actorType = parseEntryMetadata(entry.actorType,
|
|
277
|
+
const actorType = parseEntryMetadata(entry.actorType, `${paths.entry(entryIndex)}.actorType`);
|
|
231
278
|
if (!actorType.ok)
|
|
232
279
|
return actorType;
|
|
233
280
|
if (actorType.value !== undefined)
|
|
234
281
|
parsedEntry.actorType = actorType.value;
|
|
235
|
-
const surface = parseEntryMetadata(entry.surface,
|
|
282
|
+
const surface = parseEntryMetadata(entry.surface, `${paths.entry(entryIndex)}.surface`);
|
|
236
283
|
if (!surface.ok)
|
|
237
284
|
return surface;
|
|
238
285
|
if (surface.value !== undefined)
|
|
239
286
|
parsedEntry.surface = surface.value;
|
|
240
|
-
const caseGroup = parseEntryMetadata(entry.caseGroup,
|
|
287
|
+
const caseGroup = parseEntryMetadata(entry.caseGroup, `${paths.entry(entryIndex)}.caseGroup`);
|
|
241
288
|
if (!caseGroup.ok)
|
|
242
289
|
return caseGroup;
|
|
243
290
|
if (caseGroup.value !== undefined)
|
|
@@ -245,28 +292,28 @@ function parseParticipantEntries(raw, actorIndex) {
|
|
|
245
292
|
const instruction = str(entry.instruction);
|
|
246
293
|
if (instruction !== undefined)
|
|
247
294
|
parsedEntry.instruction = instruction;
|
|
248
|
-
const stopWhenResult = parseStopWhen(entry.stopWhen,
|
|
295
|
+
const stopWhenResult = parseStopWhen(entry.stopWhen, `${paths.entry(entryIndex)}.stopWhen`);
|
|
249
296
|
if (!stopWhenResult.ok)
|
|
250
297
|
return stopWhenResult;
|
|
251
298
|
if (stopWhenResult.value !== undefined)
|
|
252
299
|
parsedEntry.stopWhen = stopWhenResult.value;
|
|
253
|
-
const dwellResult = parseDwell(entry.dwell,
|
|
300
|
+
const dwellResult = parseDwell(entry.dwell, `${paths.entry(entryIndex)}.dwell`);
|
|
254
301
|
if (!dwellResult.ok)
|
|
255
302
|
return dwellResult;
|
|
256
303
|
if (dwellResult.value !== undefined)
|
|
257
304
|
parsedEntry.dwell = dwellResult.value;
|
|
258
305
|
if (entry.reasoningEffort !== undefined) {
|
|
259
306
|
if (!isReasoningEffort(entry.reasoningEffort)) {
|
|
260
|
-
return invalid(
|
|
307
|
+
return invalid(`${paths.entry(entryIndex)}.reasoningEffort must be one of: ${reasoningEffortNames()}. Support is model-dependent, so a level this model does not accept fails on the first turn rather than being silently downgraded.`);
|
|
261
308
|
}
|
|
262
309
|
parsedEntry.reasoningEffort = entry.reasoningEffort;
|
|
263
310
|
}
|
|
264
311
|
const target = str(entry.target);
|
|
265
312
|
if (target !== undefined) {
|
|
266
313
|
if (!isHttpUrl(target)) {
|
|
267
|
-
return invalid(
|
|
314
|
+
return invalid(`${paths.entry(entryIndex)}.target must be an absolute http(s) URL.`);
|
|
268
315
|
}
|
|
269
|
-
const credential = urlCredentialReason(
|
|
316
|
+
const credential = urlCredentialReason(`${paths.entry(entryIndex)}.target`, target);
|
|
270
317
|
if (credential)
|
|
271
318
|
return invalid(credential);
|
|
272
319
|
parsedEntry.target = target;
|
|
@@ -275,7 +322,7 @@ function parseParticipantEntries(raw, actorIndex) {
|
|
|
275
322
|
// it runs in sharedWorldValidationReason (where the route + serve.url are known).
|
|
276
323
|
const entryPath = str(entry.entry);
|
|
277
324
|
if (entryPath !== undefined) {
|
|
278
|
-
const credential = entryCredentialReason(
|
|
325
|
+
const credential = entryCredentialReason(`${paths.entry(entryIndex)}.entry`, entryPath);
|
|
279
326
|
if (credential)
|
|
280
327
|
return invalid(credential);
|
|
281
328
|
parsedEntry.entry = entryPath;
|
|
@@ -284,7 +331,7 @@ function parseParticipantEntries(raw, actorIndex) {
|
|
|
284
331
|
// exactly-one-host check runs in externalPublicSharedWorldValidationReason (route context).
|
|
285
332
|
if (entry.host !== undefined) {
|
|
286
333
|
if (typeof entry.host !== "boolean") {
|
|
287
|
-
return invalid(
|
|
334
|
+
return invalid(`${paths.entry(entryIndex)}.host must be a boolean (marks the designated host participant on the external-public shared-world route).`);
|
|
288
335
|
}
|
|
289
336
|
if (entry.host)
|
|
290
337
|
parsedEntry.host = true;
|
|
@@ -255,7 +255,7 @@ function parseCaps(raw) {
|
|
|
255
255
|
return { ok: true, value: undefined };
|
|
256
256
|
}
|
|
257
257
|
if (!isRecord(raw)) {
|
|
258
|
-
return invalid("`
|
|
258
|
+
return invalid("`caps` must be an object ({ maxUsd?, maxTotalUsd?, maxJobs?, maxMinutes? }).");
|
|
259
259
|
}
|
|
260
260
|
const caps = {};
|
|
261
261
|
for (const key of ["maxUsd", "maxTotalUsd", "maxJobs", "maxMinutes"]) {
|
|
@@ -263,7 +263,7 @@ function parseCaps(raw) {
|
|
|
263
263
|
continue;
|
|
264
264
|
const value = nonNegNumber(raw[key]);
|
|
265
265
|
if (value === undefined) {
|
|
266
|
-
return invalid(`\`
|
|
266
|
+
return invalid(`\`caps.${key}\` must be a non-negative number.`);
|
|
267
267
|
}
|
|
268
268
|
caps[key] = value;
|
|
269
269
|
}
|
|
@@ -7,12 +7,38 @@ export interface StudyDocument {
|
|
|
7
7
|
/** For each expanded participant, the index of the `participants` entry it came from. */
|
|
8
8
|
readonly participantSource: readonly number[];
|
|
9
9
|
}
|
|
10
|
+
/** A document's `participants`, read before the participant entries parse. */
|
|
11
|
+
export interface Participants {
|
|
12
|
+
count?: number;
|
|
13
|
+
instruction?: unknown;
|
|
14
|
+
/** The list entries, with each entry that has a count expanded into its participants. */
|
|
15
|
+
entries?: unknown[];
|
|
16
|
+
/** For each expanded entry, the index of the `participants` entry it came from. */
|
|
17
|
+
source: number[];
|
|
18
|
+
}
|
|
19
|
+
/** What a v3 document declares before its sections parse. */
|
|
20
|
+
export interface StudyFront {
|
|
21
|
+
readonly route: StudyRoute;
|
|
22
|
+
readonly actor: Record<string, unknown>;
|
|
23
|
+
readonly participants: Participants;
|
|
24
|
+
/** The scripted surfaces as a count: 1 is desktop, 2 adds mobile. */
|
|
25
|
+
readonly surfaces: number | undefined;
|
|
26
|
+
}
|
|
10
27
|
type Parsed<T> = {
|
|
11
28
|
ok: true;
|
|
12
29
|
value: T;
|
|
13
30
|
} | StudyParseFailure;
|
|
14
31
|
/** Rewrite a humanish.study.v3 document into humanish.lab.v2, or refuse it. */
|
|
15
32
|
export declare function studyToV2(raw: Record<string, unknown>): Parsed<StudyDocument>;
|
|
33
|
+
/**
|
|
34
|
+
* The checks a v3 document passes before its sections parse: no v2 key and no unknown key, a
|
|
35
|
+
* route, a mode, an `actor` object, a scenario string, and the `participants`, `surfaces` and
|
|
36
|
+
* `caps` keys its route takes. `anySharedWorldParticipants`, for migrate's v3 form of a v2 file:
|
|
37
|
+
* a shared world takes `participants` in every form computer use takes, or none, because a v2
|
|
38
|
+
* shared world could declare a count or no list. The shared-world checks then refuse it, as the v2
|
|
39
|
+
* parser does.
|
|
40
|
+
*/
|
|
41
|
+
export declare function readStudyFront(raw: Record<string, unknown>, anySharedWorldParticipants?: boolean): Parsed<StudyFront>;
|
|
16
42
|
/**
|
|
17
43
|
* Spell a v2 parser message the way the study file is written: `actors[0].lanes[3]` becomes the
|
|
18
44
|
* `participants` entry it came from, `execution.caps` and `scenario.caps` become `caps`, and so on.
|
|
@@ -26,46 +26,18 @@ const CAPS_KEYS = {
|
|
|
26
26
|
};
|
|
27
27
|
/** Rewrite a humanish.study.v3 document into humanish.lab.v2, or refuse it. */
|
|
28
28
|
export function studyToV2(raw) {
|
|
29
|
-
const
|
|
30
|
-
if (
|
|
31
|
-
return
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
return invalid(unknownKey);
|
|
35
|
-
const route = ROUTES.find((name) => name === raw.route);
|
|
36
|
-
if (route === undefined) {
|
|
37
|
-
return invalid(`A study needs \`route:\`, one of ${ROUTES.join(", ")}.`);
|
|
38
|
-
}
|
|
39
|
-
if (raw.mode !== undefined && raw.mode !== "dry-run" && raw.mode !== "live") {
|
|
40
|
-
return invalid("`mode` must be `dry-run` or `live`. Leave it out for a dry run.");
|
|
41
|
-
}
|
|
42
|
-
if (!isRecord(raw.actor)) {
|
|
43
|
-
return invalid("A study needs `actor:`, an object with at least `type`.");
|
|
44
|
-
}
|
|
45
|
-
if (raw.scenario !== undefined && typeof raw.scenario !== "string") {
|
|
46
|
-
return invalid("`scenario` is a scenario id or a path to one, as a string.");
|
|
47
|
-
}
|
|
48
|
-
const participants = participantsOf(route, raw.participants);
|
|
49
|
-
if (!participants.ok)
|
|
50
|
-
return participants;
|
|
51
|
-
const surfaces = surfaceCount(route, raw.surfaces);
|
|
52
|
-
if (!surfaces.ok)
|
|
53
|
-
return surfaces;
|
|
54
|
-
const capsReason = capsKeysReason(route, raw.caps);
|
|
55
|
-
if (capsReason)
|
|
56
|
-
return invalid(capsReason);
|
|
57
|
-
// The v2 inert-field table does not list this one, but the terminal planner never reads it.
|
|
58
|
-
if (route === "terminal" && isRecord(raw.execution) && raw.execution.timeoutMs !== undefined) {
|
|
59
|
-
return invalid("route: terminal does not read `execution.timeoutMs`: `caps.maxMinutes` is the command deadline. Remove it.");
|
|
60
|
-
}
|
|
61
|
-
const { count, instruction, entries, source } = participants.value;
|
|
29
|
+
const front = readStudyFront(raw);
|
|
30
|
+
if (!front.ok)
|
|
31
|
+
return front;
|
|
32
|
+
const { route } = front.value;
|
|
33
|
+
const { count, instruction, entries, source } = front.value.participants;
|
|
62
34
|
// The v2 actor keys that hold participants.
|
|
63
35
|
const v2Participants = [
|
|
64
|
-
["count", count ??
|
|
36
|
+
["count", count ?? front.value.surfaces],
|
|
65
37
|
["laneFocus", instruction === undefined ? undefined : { instruction }],
|
|
66
38
|
["lanes", entries],
|
|
67
39
|
];
|
|
68
|
-
const actor = { ...
|
|
40
|
+
const actor = { ...front.value.actor };
|
|
69
41
|
for (const [key, value] of v2Participants)
|
|
70
42
|
if (value !== undefined)
|
|
71
43
|
actor[key] = value;
|
|
@@ -100,6 +72,52 @@ export function studyToV2(raw) {
|
|
|
100
72
|
v2[key] = value;
|
|
101
73
|
return { ok: true, value: { route, v2, participantSource: source } };
|
|
102
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* The checks a v3 document passes before its sections parse: no v2 key and no unknown key, a
|
|
77
|
+
* route, a mode, an `actor` object, a scenario string, and the `participants`, `surfaces` and
|
|
78
|
+
* `caps` keys its route takes. `anySharedWorldParticipants`, for migrate's v3 form of a v2 file:
|
|
79
|
+
* a shared world takes `participants` in every form computer use takes, or none, because a v2
|
|
80
|
+
* shared world could declare a count or no list. The shared-world checks then refuse it, as the v2
|
|
81
|
+
* parser does.
|
|
82
|
+
*/
|
|
83
|
+
export function readStudyFront(raw, anySharedWorldParticipants = false) {
|
|
84
|
+
const moved = movedKeyReason(raw);
|
|
85
|
+
if (moved)
|
|
86
|
+
return invalid(moved);
|
|
87
|
+
const unknownKey = findUnknownStudyKey(raw);
|
|
88
|
+
if (unknownKey)
|
|
89
|
+
return invalid(unknownKey);
|
|
90
|
+
const route = ROUTES.find((name) => name === raw.route);
|
|
91
|
+
if (route === undefined) {
|
|
92
|
+
return invalid(`A study needs \`route:\`, one of ${ROUTES.join(", ")}.`);
|
|
93
|
+
}
|
|
94
|
+
if (raw.mode !== undefined && raw.mode !== "dry-run" && raw.mode !== "live") {
|
|
95
|
+
return invalid("`mode` must be `dry-run` or `live`. Leave it out for a dry run.");
|
|
96
|
+
}
|
|
97
|
+
if (!isRecord(raw.actor)) {
|
|
98
|
+
return invalid("A study needs `actor:`, an object with at least `type`.");
|
|
99
|
+
}
|
|
100
|
+
if (raw.scenario !== undefined && typeof raw.scenario !== "string") {
|
|
101
|
+
return invalid("`scenario` is a scenario id or a path to one, as a string.");
|
|
102
|
+
}
|
|
103
|
+
const participants = participantsOf(anySharedWorldParticipants && route === "shared-world" ? "computer-use" : route, raw.participants);
|
|
104
|
+
if (!participants.ok)
|
|
105
|
+
return participants;
|
|
106
|
+
const surfaces = surfaceCount(route, raw.surfaces);
|
|
107
|
+
if (!surfaces.ok)
|
|
108
|
+
return surfaces;
|
|
109
|
+
const capsReason = capsKeysReason(route, raw.caps);
|
|
110
|
+
if (capsReason)
|
|
111
|
+
return invalid(capsReason);
|
|
112
|
+
// The v2 inert-field table does not list this one, but the terminal planner never reads it.
|
|
113
|
+
if (route === "terminal" && isRecord(raw.execution) && raw.execution.timeoutMs !== undefined) {
|
|
114
|
+
return invalid("route: terminal does not read `execution.timeoutMs`: `caps.maxMinutes` is the command deadline. Remove it.");
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
ok: true,
|
|
118
|
+
value: { route, actor: raw.actor, participants: participants.value, surfaces: surfaces.value },
|
|
119
|
+
};
|
|
120
|
+
}
|
|
103
121
|
/**
|
|
104
122
|
* Spell a v2 parser message the way the study file is written: `actors[0].lanes[3]` becomes the
|
|
105
123
|
* `participants` entry it came from, `execution.caps` and `scenario.caps` become `caps`, and so on.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type StudyConfig, type StudyParseFailure, type StudyScenarioCaps, type StudyV3 } from "../types.js";
|
|
2
|
+
interface StudyV3Options {
|
|
3
|
+
/**
|
|
4
|
+
* `report`, for `humanish migrate`: list the fields the route does not read in `inert`, as v2
|
|
5
|
+
* paths, in place of refusing the study for them, and give the checks' messages as they wrote
|
|
6
|
+
* them, in the v2 spelling of the file migrate converts. A shared world may then give
|
|
7
|
+
* `participants` as a count or leave it out, as a v2 file could (readStudyFront).
|
|
8
|
+
*/
|
|
9
|
+
readonly inert?: "refuse" | "report";
|
|
10
|
+
}
|
|
11
|
+
type StudyV3ParseResult = {
|
|
12
|
+
ok: true;
|
|
13
|
+
config: StudyV3;
|
|
14
|
+
warnings: string[];
|
|
15
|
+
inert?: string[];
|
|
16
|
+
} | StudyParseFailure;
|
|
17
|
+
type Parsed<T> = {
|
|
18
|
+
ok: true;
|
|
19
|
+
value: T;
|
|
20
|
+
} | StudyParseFailure;
|
|
21
|
+
/**
|
|
22
|
+
* Validate a parsed humanish.study.v3 document into a StudyV3, with the messages parseStudy gives.
|
|
23
|
+
* Pure: the caller owns file IO. parseStudy refuses a humanish.lab.v2 document before this runs.
|
|
24
|
+
*/
|
|
25
|
+
export declare function parseStudyV3(raw: unknown, options?: StudyV3Options): StudyV3ParseResult;
|
|
26
|
+
/**
|
|
27
|
+
* A v3 study in the v2 spelling: the actor holds its participants, `caps` moves under `execution`
|
|
28
|
+
* or `scenario`, and a shared world is `subject.topology: shared-world`. The checks that read more
|
|
29
|
+
* than one section run on this view until StudyConfig has the v3 shape.
|
|
30
|
+
*/
|
|
31
|
+
export declare function toLegacy(study: StudyV3): StudyConfig;
|
|
32
|
+
/**
|
|
33
|
+
* A caps block's values. A value that is not a non-negative number refuses the study: a cap that
|
|
34
|
+
* does nothing must not look like one that holds. readStudyFront has checked the keys against the
|
|
35
|
+
* route; migrate checks a v2 file's two blocks with it.
|
|
36
|
+
*/
|
|
37
|
+
export declare function parseCaps(raw: unknown): Parsed<StudyScenarioCaps | undefined>;
|
|
38
|
+
export {};
|