humanish 0.110.4 → 0.111.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 +15 -0
- package/CONTEXT.md +2 -2
- 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/cli/commands/study.js +2 -4
- package/dist/routes/computer-use/participant-model.js +1 -1
- package/dist/routes/computer-use/plan.js +4 -6
- 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 +1 -1
- package/dist/routes/computer-use/types.d.ts +4 -4
- package/dist/routes/scripted/route.js +1 -1
- 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 +3 -4
- package/dist/routes/shared-world/types.d.ts +1 -1
- package/dist/routes/terminal/bundle.d.ts +3 -3
- package/dist/routes/terminal/ledger.d.ts +2 -2
- package/dist/routes/terminal/lifetime.js +1 -1
- package/dist/routes/terminal/live-finish.js +1 -1
- package/dist/routes/terminal/plan.js +1 -1
- package/dist/routes/terminal/route.js +1 -1
- package/dist/routes/terminal/types.d.ts +1 -1
- package/dist/run/results.d.ts +1 -1
- 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 +21 -11
- 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/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 +17 -7
- package/dist/study/migrate/v2.js +150 -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} +22 -91
- package/dist/study/parse/subject.d.ts +2 -0
- package/dist/study/parse/subject.js +15 -18
- package/dist/study/persona-resolve.js +7 -9
- package/dist/study/plan-base.d.ts +1 -1
- package/dist/study/plan-base.js +3 -4
- package/dist/study/plan-participants.d.ts +2 -2
- package/dist/study/plan-types.d.ts +5 -5
- package/dist/study/plan.js +1 -1
- 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 +5 -5
- package/dist/study/run-study-options.d.ts +1 -1
- package/dist/study/study-deps.d.ts +1 -1
- package/dist/study/study-fields.d.ts +12 -21
- package/dist/study/study-fields.js +29 -34
- package/dist/study/summary.js +9 -4
- package/dist/study/types.d.ts +50 -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 +16 -46
- package/dist/study/warnings.d.ts +3 -3
- package/dist/study/warnings.js +40 -109
- package/dist/substrates/local/runtime-config.js +9 -11
- package/docs/contracts/schemas.md +23 -4
- package/package.json +1 -1
- package/dist/study/parse/study.d.ts +0 -38
- package/dist/study/parse/study.js +0 -225
|
@@ -4,13 +4,22 @@
|
|
|
4
4
|
// a library caller's config itself, under its route's error codes. The planners call the shared
|
|
5
5
|
// checks in study/validation.ts and desktopCliProductReason below. The other subject rules here have
|
|
6
6
|
// planner counterparts whose conditions and wording differ (clone serve and repo, local-tree
|
|
7
|
-
// target, in-process fan-out, loopback targets, scripted scenario
|
|
7
|
+
// target, in-process fan-out, loopback targets, scripted `scenario`).
|
|
8
8
|
import { isLoopbackUrl } from "./parse/subject.js";
|
|
9
9
|
import { REPO_SLUG_PATTERN } from "./parse/values.js";
|
|
10
10
|
import { declaredTargets } from "./plan-participants.js";
|
|
11
11
|
import { actorResolvesToComputerUse, actorResolvesToScriptedBrowser, actorResolvesToTerminal, computerUseParticipantCount, registeredComputerUseActors, registeredScriptedBrowserActors, registeredTerminalActors, isComputerUseComposition, } from "./routing.js";
|
|
12
12
|
import { cloneTargetValidationReason, computerUseValidationReason, sharedWorldValidationReason, } from "./validation.js";
|
|
13
13
|
import { actorOf, participantList, declaredParticipantCount, surfaceCount, modeOf, scenarioRefOf, declaresSharedWorld, } from "./study-fields.js";
|
|
14
|
+
/**
|
|
15
|
+
* The count a scripted or terminal composition reads: its surfaces, or a participant count.
|
|
16
|
+
* parse/front.ts refuses `participants` on route: scripted and route: terminal, and `surfaces` off
|
|
17
|
+
* route: scripted, but a study can declare a route its composition does not take, and these rules
|
|
18
|
+
* run before parseStudy compares the two. migrate's view of a v2 file reaches them the same way.
|
|
19
|
+
*/
|
|
20
|
+
function participantOrSurfaceCount(config) {
|
|
21
|
+
return declaredParticipantCount(config) ?? surfaceCount(config);
|
|
22
|
+
}
|
|
14
23
|
/** The first composition rule a config breaks, in the parser's order, or null. */
|
|
15
24
|
export function compositionReason(config) {
|
|
16
25
|
return (thisRepoValidationReason(config) ??
|
|
@@ -62,7 +71,7 @@ function localAppValidationReason(config) {
|
|
|
62
71
|
return `actor.type must be a registered computer-use actor for local-app subjects (one of: ${registeredComputerUseActors().join(", ")}); the caller's custom executor runs the computer-use loop. Got "${type}".`;
|
|
63
72
|
}
|
|
64
73
|
if (computerUseParticipantCount(config) > 1) {
|
|
65
|
-
return "Fan-out to more than one participant is not supported on the in-process/local-app route: fan-out provisions one independent E2B desktop per participant, which the in-process route deliberately skips.
|
|
74
|
+
return "Fan-out to more than one participant is not supported on the in-process/local-app route: fan-out provisions one independent E2B desktop per participant, which the in-process route deliberately skips. Remove `participants`, or set `participants: 1`. For fan-out, use an app-url or clone subject on execution.target: e2b-desktop.";
|
|
66
75
|
}
|
|
67
76
|
if (participantList(config) !== undefined) {
|
|
68
77
|
return "`participants` (fan-out roster) is not supported on the in-process/local-app route: it provisions one E2B desktop per participant, which this route skips. Use an app-url or clone subject with execution.target: e2b-desktop.";
|
|
@@ -88,7 +97,7 @@ function appUrlValidationReason(config) {
|
|
|
88
97
|
if (!scenarioRefOf(config)) {
|
|
89
98
|
return "A scripted-browser study needs `scenario`: the actor runs the browser steps in that scenario file.";
|
|
90
99
|
}
|
|
91
|
-
if ((
|
|
100
|
+
if ((participantOrSurfaceCount(config) ?? 1) > 2) {
|
|
92
101
|
return "A scripted-browser study takes `surfaces` 1 (desktop) or 2 (desktop and mobile). Higher counts are not supported yet.";
|
|
93
102
|
}
|
|
94
103
|
if (config.policies?.redactScreenshots === true) {
|
|
@@ -109,9 +118,8 @@ function appUrlValidationReason(config) {
|
|
|
109
118
|
return `actor.type must be a registered computer-use actor for app-url × e2b-desktop studies (one of: ${registeredComputerUseActors().join(", ")}); for local scripted execution use a registered scripted-browser actor (${registeredScriptedBrowserActors().join(", ")}). Got "${type}".`;
|
|
110
119
|
}
|
|
111
120
|
// Multi-participant fan-out is consumed on this route (`per-lane-worlds`; the shared cua
|
|
112
|
-
// cross-validation below enforces
|
|
113
|
-
//
|
|
114
|
-
// fan-out).
|
|
121
|
+
// cross-validation below enforces the 16 cap, the per-participant target gates, and the
|
|
122
|
+
// allowPublicTargets+N>1 rejection for ambiguous one-target fan-out).
|
|
115
123
|
// Loopback by default; an owner may declare a public/preview target via policies.
|
|
116
124
|
const targets = [config.subject.appUrl ?? "", ...declaredTargets(config)];
|
|
117
125
|
const unsafeTarget = targets.find((target) => !config.policies?.allowPublicTargets && !isLoopbackUrl(target));
|
|
@@ -142,7 +150,9 @@ function scriptedBrowserValidationReason(config) {
|
|
|
142
150
|
if (!REPO_SLUG_PATTERN.test(repo)) {
|
|
143
151
|
return `subject.repos[0] must be an owner/repo slug (got "${repo}").`;
|
|
144
152
|
}
|
|
145
|
-
|
|
153
|
+
// migrate converts a v2 file's `subject.topology: shared-world` to `route: shared-world`, and
|
|
154
|
+
// refuses it here with the v2 parser's message.
|
|
155
|
+
if (declaresSharedWorld(config)) {
|
|
146
156
|
return "A clone scripted-browser study does not support `subject.topology` yet: it runs one synthetic subject for its scripted actor. Remove `subject.topology`.";
|
|
147
157
|
}
|
|
148
158
|
if (config.subject.clone?.fanout !== undefined || config.subject.clone?.keep === true) {
|
|
@@ -151,7 +161,7 @@ function scriptedBrowserValidationReason(config) {
|
|
|
151
161
|
if (!scenarioRefOf(config)) {
|
|
152
162
|
return "A scripted-browser study needs `scenario`: the actor runs the browser steps in that scenario file.";
|
|
153
163
|
}
|
|
154
|
-
if ((
|
|
164
|
+
if ((participantOrSurfaceCount(config) ?? 1) > 2) {
|
|
155
165
|
return "A scripted-browser study takes `surfaces` 1 (desktop) or 2 (desktop and mobile). Higher counts are not supported yet.";
|
|
156
166
|
}
|
|
157
167
|
if (participantList(config) !== undefined) {
|
|
@@ -195,8 +205,8 @@ function cloneComputerUseValidationReason(config) {
|
|
|
195
205
|
return `subject.repos[0] must be an owner/repo slug (got "${repo}").`;
|
|
196
206
|
}
|
|
197
207
|
// Fan-out is consumed here: N participants each clone the same single repo into their own
|
|
198
|
-
// E2B desktop (`per-lane-worlds`). The shared cua cross-validation below enforces the
|
|
199
|
-
//
|
|
208
|
+
// E2B desktop (`per-lane-worlds`). The shared cua cross-validation below enforces the 16 cap;
|
|
209
|
+
// the single-repo rule above is unchanged.
|
|
200
210
|
}
|
|
201
211
|
return null;
|
|
202
212
|
}
|
|
@@ -262,7 +272,7 @@ function terminalValidationReason(config) {
|
|
|
262
272
|
if (!actorResolvesToTerminal(type)) {
|
|
263
273
|
return `actor.type must be a registered terminal actor for terminal-product subjects (one of: ${registeredTerminalActors().join(", ")}). Got "${type}".`;
|
|
264
274
|
}
|
|
265
|
-
if ((
|
|
275
|
+
if ((participantOrSurfaceCount(config) ?? 1) > 1) {
|
|
266
276
|
return "Terminal fan-out to more than one participant is not supported yet; set participants to 1.";
|
|
267
277
|
}
|
|
268
278
|
}
|
package/dist/study/config.d.ts
CHANGED
|
@@ -1,22 +1,38 @@
|
|
|
1
|
-
import { type StudyConfig, type StudyParseResult } from "./types.js";
|
|
2
|
-
/** The message for a humanish.lab.v2 document, which the package and the CLI no longer run. */
|
|
3
|
-
export declare const V2_UNSUPPORTED_MESSAGE = "This is a humanish.lab.v2 study, which humanish no longer reads. Convert its file with humanish migrate <path>.";
|
|
1
|
+
import { type StudyConfig, type StudyParseFailure, type StudyParseResult } from "./types.js";
|
|
4
2
|
/**
|
|
5
3
|
* The package's study parser (src/index.ts) and the one discovery uses: a humanish.study.v3
|
|
6
4
|
* document. It refuses a humanish.lab.v2 one with HUMANISH_STUDY_V2_UNSUPPORTED.
|
|
7
5
|
*/
|
|
8
6
|
export declare function parseStudy(raw: unknown): StudyParseResult;
|
|
7
|
+
interface StudyV3Options {
|
|
8
|
+
/**
|
|
9
|
+
* `report`, for `humanish migrate`: list the fields the route does not read in `inert`, in place
|
|
10
|
+
* of refusing the study for them. A shared world may then give `participants` as a count or
|
|
11
|
+
* leave it out, as a v2 file could (readStudyFront).
|
|
12
|
+
*/
|
|
13
|
+
readonly inert?: "refuse" | "report";
|
|
14
|
+
}
|
|
15
|
+
type StudyV3ParseResult = {
|
|
16
|
+
ok: true;
|
|
17
|
+
config: StudyConfig;
|
|
18
|
+
warnings: string[];
|
|
19
|
+
inert?: string[];
|
|
20
|
+
} | StudyParseFailure;
|
|
9
21
|
/**
|
|
10
|
-
* Validate a
|
|
11
|
-
*
|
|
12
|
-
* accepted but reported in `warnings` so `study show` never silently swallows a setting that does
|
|
13
|
-
* nothing. The v2 parser's tests call it directly; migrate reads v2 files through migrate/v2.ts.
|
|
22
|
+
* Validate a humanish.study.v3 document into a StudyConfig. Pure: the caller owns file IO. Fields the
|
|
23
|
+
* route does not read refuse the study; migrate's report mode lists them in `inert` instead.
|
|
14
24
|
*/
|
|
15
|
-
export declare function
|
|
25
|
+
export declare function parseStudyV3(raw: Record<string, unknown>, options?: StudyV3Options): StudyV3ParseResult;
|
|
26
|
+
/** Why runStudy refuses a config before anything runs. */
|
|
27
|
+
interface ConfigRefusal {
|
|
28
|
+
readonly code: "HUMANISH_STUDY_V2_UNSUPPORTED" | "HUMANISH_STUDY_INVALID";
|
|
29
|
+
readonly message: string;
|
|
30
|
+
}
|
|
16
31
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
32
|
+
* Why runStudy refuses a config a library caller passes, or undefined: a humanish.lab.v2 config,
|
|
33
|
+
* one that still sets a StudyConfig field of humanish 0.110, one with no `actor` object, and one
|
|
34
|
+
* whose `route` is not the route its subject and actor take, with parseStudy's message. A planner
|
|
35
|
+
* reads only the v3 fields, so a budget left in `execution.caps` would otherwise run uncapped.
|
|
21
36
|
*/
|
|
22
|
-
export declare function
|
|
37
|
+
export declare function libraryConfigRefusal(config: StudyConfig): ConfigRefusal | undefined;
|
|
38
|
+
export {};
|
package/dist/study/config.js
CHANGED
|
@@ -1,204 +1,224 @@
|
|
|
1
|
-
// The
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// The study parser: a humanish.study.v3 document into a StudyConfig with the file's own keys.
|
|
2
|
+
// parse/front.ts checks what the document declares before its sections parse (the route, the
|
|
3
|
+
// actor object, the participants or surfaces form the route takes, and the caps keys it reads). The
|
|
4
|
+
// sections parse here, checkStudyConfig runs the checks that read more than one section, and the
|
|
5
|
+
// study must then take the route it declares and set no field that route does not read
|
|
6
|
+
// (warnings.ts). A humanish.lab.v2 document is refused; `humanish migrate` reads it
|
|
7
|
+
// (migrate/v2.ts).
|
|
5
8
|
//
|
|
6
|
-
// Scope (read before trusting field names): the
|
|
7
|
-
// subject.source × execution.target
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// execution.desktop.{browser,resolution,sandboxTimeoutMs}, and (clone)
|
|
17
|
-
// subject.{serve,env,state,clone.depth}.
|
|
18
|
-
// On the scripted-browser route (app-url × local-or-absent, or clone × e2b-desktop, with a
|
|
19
|
-
// registered scripted-browser actor), `actors[0].type` selects the actor too, and the route
|
|
20
|
-
// consumes scenario.ref (required there, because the committed scenario's browser steps are what the
|
|
21
|
-
// actor executes), actors[0].{persona,count}, and execution.timeoutMs. On the provisioned
|
|
22
|
-
// clone slice it also consumes subject.{repos,serve,env,state,exposure,clone.depth} and
|
|
23
|
-
// execution.desktop.template. actors[0].{mission,laneFocus,model} are inert on that route
|
|
24
|
-
// because no model runs, and most execution.desktop.* fields remain forward-declared (device
|
|
25
|
-
// presets belong to the computer-use route; scripted surfaces are the driver's own desktop/mobile
|
|
26
|
-
// viewports where isMobile/DSF genuinely render via playwright emulation).
|
|
27
|
-
// On the other routes those fields remain forward-declared and are not yet consumed:
|
|
28
|
-
// parseStudy emits a warning listing any such field that is set, so `study show` shows
|
|
29
|
-
// the truth.
|
|
9
|
+
// Scope (read before trusting field names): routeOf derives the route from
|
|
10
|
+
// subject.source × execution.target × the actor's run kind, with `route: shared-world` as the
|
|
11
|
+
// declaration that turns a computer-use composition into a shared world. On the computer-use
|
|
12
|
+
// routes (app-url × e2b-desktop or local, and clone, local-tree or desktop-cli on e2b-desktop with a
|
|
13
|
+
// computer-use actor), `actor.type` must resolve to a registered computer-use actor, and that
|
|
14
|
+
// descriptor runs the session. Those routes consume actor.{mission,persona,model,reasoningEffort},
|
|
15
|
+
// `participants`, `caps`, execution.timeoutMs and execution.desktop.*. The scripted route
|
|
16
|
+
// (app-url × local, or clone × e2b-desktop, with a registered scripted-browser actor) consumes
|
|
17
|
+
// `scenario` (required: its browser steps are what the actor runs), actor.persona and `surfaces`.
|
|
18
|
+
// The terminal route consumes subject.product, `caps` and execution.{terminal,runtimeAuth}.
|
|
30
19
|
//
|
|
31
|
-
// NOTE on
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
// independent E2B desktop (`per-lane-worlds`, the default topology). `roster[]` is parser sugar for
|
|
41
|
-
// repeated groups and is normalized into `lanes[]` before the engine sees it. `lanes|roster` XOR
|
|
42
|
-
// `count` (declare a differentiated roster or a homogeneous count, never both); `lanes|roster`
|
|
43
|
-
// XOR `actors[0].laneFocus` (each entry's `instruction` is the roster's steer); `lanes[].device` XOR
|
|
44
|
-
// raw `execution.desktop.resolution`. `execution.concurrency` bounds in-flight participants (default:
|
|
45
|
-
// the participant count, all at once; env HUMANISH_CUA_MAX_CONCURRENCY may only lower it, per invariant
|
|
46
|
-
// 3). On every non-computer-use route normalized `lanes` are inert (warned). subject.clone.fanout
|
|
47
|
-
// is rejected on the computer-use route. `lanes[].target` is app-url × computer-use only: an
|
|
48
|
-
// absolute browser URL this participant opens instead of `subject.appUrl`; it is the generic
|
|
49
|
-
// setup-produced-target handoff, not a service topology primitive.
|
|
50
|
-
//
|
|
51
|
-
// There is deliberately no v1 compatibility: v1 had zero real users. Breaking schema changes
|
|
52
|
-
// bump the version.
|
|
20
|
+
// NOTE on `participants`: a count (preview: the simulated count; computer use: N identical
|
|
21
|
+
// participants, each its own desktop, capped at 16), `{ count, instruction }` (identical
|
|
22
|
+
// participants with one steer), or a list of `{ id?, persona?, device?, instruction?, target? ... }`
|
|
23
|
+
// entries, each its own desktop. A list entry with a `count` is a group that expands into
|
|
24
|
+
// `<id>-01` to `<id>-NN`. A shared world takes a list of at least two. `participants[].device`
|
|
25
|
+
// excludes a raw `execution.desktop.resolution`, and `participants[].target` is app-url computer
|
|
26
|
+
// use only: an absolute browser URL this participant opens in place of `subject.appUrl`.
|
|
27
|
+
// `execution.concurrency` bounds in-flight participants (default: all at once; env
|
|
28
|
+
// HUMANISH_CUA_MAX_CONCURRENCY may only lower it).
|
|
53
29
|
import { isLocalBrowserStudy, localBrowserDefaults, localBrowserUnsupportedReason, } from "../substrates/local/runtime-config.js";
|
|
54
30
|
import { isRecord } from "../run/type-guards.js";
|
|
55
31
|
import { compositionReason } from "./composition-rules.js";
|
|
56
|
-
import {
|
|
57
|
-
import {
|
|
32
|
+
import { V2_SCHEMA, V2_UNSUPPORTED_MESSAGE, v2FieldMoves } from "./migrate/v2.js";
|
|
33
|
+
import { parseActorFields, parseActorType, parseParticipantEntries } from "./parse/actors.js";
|
|
58
34
|
import { parseComms, recipientParticipantId } from "./parse/comms.js";
|
|
59
|
-
import { parseDefaults, parseExecution,
|
|
60
|
-
import {
|
|
35
|
+
import { parseCaps, parseDefaults, parseExecution, parsePolicies, parseReview, } from "./parse/execution.js";
|
|
36
|
+
import { ACTOR_REQUIRED, readStudyFront, routeNameReason, } from "./parse/front.js";
|
|
61
37
|
import { parseSubject } from "./parse/subject.js";
|
|
62
38
|
import { invalid, optionalStr, str } from "./parse/values.js";
|
|
63
39
|
import { isComputerUseComposition, isSharedWorldComposition, routeOf } from "./routing.js";
|
|
64
40
|
import { declaredParticipantIds } from "./plan-participants.js";
|
|
65
|
-
import {
|
|
66
|
-
import {
|
|
67
|
-
import {
|
|
41
|
+
import { declaredParticipantCount, participantList } from "./study-fields.js";
|
|
42
|
+
import { ID_PATTERN, STUDY_SCHEMA, } from "./types.js";
|
|
43
|
+
import { automaticAnalysisRouteReason, desktopMediaValidationReason, outputTokenLimitValidationReason, receivingEmailValidationReason, smtpValidationReason, taskProtocolValidationReason, } from "./validation.js";
|
|
44
|
+
import { forwardDeclaredWarnings, inertFieldLabels, inertFieldPaths } from "./warnings.js";
|
|
68
45
|
import { plural } from "../run/text.js";
|
|
69
|
-
/** The message for a humanish.lab.v2 document, which the package and the CLI no longer run. */
|
|
70
|
-
export const V2_UNSUPPORTED_MESSAGE = "This is a humanish.lab.v2 study, which humanish no longer reads. Convert its file with humanish migrate <path>.";
|
|
71
46
|
/**
|
|
72
47
|
* The package's study parser (src/index.ts) and the one discovery uses: a humanish.study.v3
|
|
73
48
|
* document. It refuses a humanish.lab.v2 one with HUMANISH_STUDY_V2_UNSUPPORTED.
|
|
74
49
|
*/
|
|
75
50
|
export function parseStudy(raw) {
|
|
76
|
-
if (isRecord(raw)
|
|
51
|
+
if (!isRecord(raw))
|
|
52
|
+
return invalid("A study file must be a YAML object.");
|
|
53
|
+
if (raw.schema === V2_SCHEMA)
|
|
77
54
|
return {
|
|
78
55
|
ok: false,
|
|
79
56
|
error: { code: "HUMANISH_STUDY_V2_UNSUPPORTED", message: V2_UNSUPPORTED_MESSAGE },
|
|
80
57
|
};
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Validate a parsed YAML object, humanish.study.v3 or humanish.lab.v2, into a StudyConfig. Pure:
|
|
85
|
-
* the caller owns file IO. Structural validation only. Fields the engine does not yet consume are
|
|
86
|
-
* accepted but reported in `warnings` so `study show` never silently swallows a setting that does
|
|
87
|
-
* nothing. The v2 parser's tests call it directly; migrate reads v2 files through migrate/v2.ts.
|
|
88
|
-
*/
|
|
89
|
-
export function parseStudyDocument(raw) {
|
|
90
|
-
if (!isRecord(raw)) {
|
|
91
|
-
return invalid("A study file must be a YAML object.");
|
|
92
|
-
}
|
|
93
|
-
if (raw.schema === STUDY_SCHEMA)
|
|
94
|
-
return parseV3(raw);
|
|
95
|
-
if (raw.schema !== V2_SCHEMA) {
|
|
58
|
+
if (raw.schema !== STUDY_SCHEMA) {
|
|
96
59
|
return invalid(`The study schema must be ${STUDY_SCHEMA} or ${V2_SCHEMA}.`);
|
|
97
60
|
}
|
|
98
|
-
return
|
|
61
|
+
return parseStudyV3(raw);
|
|
99
62
|
}
|
|
100
63
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
64
|
+
* Validate a humanish.study.v3 document into a StudyConfig. Pure: the caller owns file IO. Fields the
|
|
65
|
+
* route does not read refuse the study; migrate's report mode lists them in `inert` instead.
|
|
103
66
|
*/
|
|
104
|
-
function
|
|
105
|
-
const
|
|
106
|
-
if (!
|
|
107
|
-
return
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
67
|
+
export function parseStudyV3(raw, options = {}) {
|
|
68
|
+
const front = readStudyFront(raw, options.inert === "report");
|
|
69
|
+
if (!front.ok)
|
|
70
|
+
return front;
|
|
71
|
+
const study = parseSections(raw, front.value);
|
|
72
|
+
if (!study.ok)
|
|
73
|
+
return study;
|
|
74
|
+
const checked = checkStudyConfig(study.value);
|
|
75
|
+
if (!checked.ok)
|
|
76
|
+
return checked;
|
|
77
|
+
const { config, warnings } = checked;
|
|
78
|
+
const { route } = config;
|
|
79
|
+
const mismatch = routeMismatchReason(config);
|
|
80
|
+
if (mismatch)
|
|
81
|
+
return invalid(mismatch);
|
|
82
|
+
if (options.inert === "report") {
|
|
83
|
+
return { ok: true, config, warnings, inert: inertFieldPaths(config) };
|
|
116
84
|
}
|
|
117
|
-
const inert = inertFieldLabels(
|
|
85
|
+
const inert = inertFieldLabels(config);
|
|
118
86
|
if (inert.length > 0) {
|
|
119
|
-
return invalid(
|
|
87
|
+
return invalid(`route: ${route} does not read ${inert.join(", ")}. Remove ${inert.length === 1 ? "it" : "them"}.`);
|
|
120
88
|
}
|
|
121
|
-
return {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
89
|
+
return { ok: true, config, warnings };
|
|
90
|
+
}
|
|
91
|
+
/** Why a config's declared route is not the one its subject and actor take, or undefined. */
|
|
92
|
+
function routeMismatchReason(config) {
|
|
93
|
+
const taken = routeOf(config);
|
|
94
|
+
if (taken === config.route)
|
|
95
|
+
return undefined;
|
|
96
|
+
return `This study declares route: ${config.route}, but its subject (source: ${config.subject.source}) and actor (type: ${config.actor.type}) take the ${taken} route. Change \`route\`, or change the subject or actor.`;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Why runStudy refuses a config a library caller passes, or undefined: a humanish.lab.v2 config,
|
|
100
|
+
* one that still sets a StudyConfig field of humanish 0.110, one with no `actor` object, and one
|
|
101
|
+
* whose `route` is not the route its subject and actor take, with parseStudy's message. A planner
|
|
102
|
+
* reads only the v3 fields, so a budget left in `execution.caps` would otherwise run uncapped.
|
|
103
|
+
*/
|
|
104
|
+
export function libraryConfigRefusal(config) {
|
|
105
|
+
const raw = config;
|
|
106
|
+
if (!isRecord(raw))
|
|
107
|
+
return undefined;
|
|
108
|
+
if (raw.schema === V2_SCHEMA)
|
|
109
|
+
return { code: "HUMANISH_STUDY_V2_UNSUPPORTED", message: V2_UNSUPPORTED_MESSAGE };
|
|
110
|
+
const moves = v2FieldMoves(raw);
|
|
111
|
+
if (moves.length > 0)
|
|
112
|
+
return {
|
|
113
|
+
code: "HUMANISH_STUDY_V2_UNSUPPORTED",
|
|
114
|
+
message: `This StudyConfig sets fields that humanish 0.111.0 renamed: ${moves.join("; ")}. Set the humanish.study.v3 fields, or parse the study file with parseStudy. The 0.111.0 release notes list each renamed field.`,
|
|
115
|
+
};
|
|
116
|
+
if (!isRecord(raw.actor))
|
|
117
|
+
return { code: "HUMANISH_STUDY_INVALID", message: ACTOR_REQUIRED };
|
|
118
|
+
const message = routeNameReason(raw.route) ?? routeMismatchReason(config);
|
|
119
|
+
return message === undefined ? undefined : { code: "HUMANISH_STUDY_INVALID", message };
|
|
126
120
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
return invalid(unknownKey);
|
|
121
|
+
// Each section in a fixed order, so a file with more than one error always reports the same one
|
|
122
|
+
// first. migrate's checkV2 reads a v2 file's sections in this order too.
|
|
123
|
+
function parseSections(raw, front) {
|
|
131
124
|
const id = str(raw.id);
|
|
132
125
|
if (!id || !ID_PATTERN.test(id)) {
|
|
133
126
|
return invalid("The study id must be a public-safe token starting with a letter or digit (/^[A-Za-z0-9][A-Za-z0-9_.-]*$/).");
|
|
134
127
|
}
|
|
135
|
-
const
|
|
136
|
-
if (!
|
|
137
|
-
return
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
actors: actorsResult.value,
|
|
154
|
-
...(executionResult.value ? { execution: executionResult.value } : {}),
|
|
155
|
-
};
|
|
156
|
-
const personas = parsePersonas(raw.personas);
|
|
157
|
-
if (personas)
|
|
158
|
-
config.personas = personas;
|
|
159
|
-
const scenarioResult = parseScenario(raw.scenario);
|
|
160
|
-
if (!scenarioResult.ok) {
|
|
161
|
-
return scenarioResult;
|
|
162
|
-
}
|
|
163
|
-
if (scenarioResult.value)
|
|
164
|
-
config.scenario = scenarioResult.value;
|
|
128
|
+
const subject = parseSubject(raw.subject);
|
|
129
|
+
if (!subject.ok)
|
|
130
|
+
return subject;
|
|
131
|
+
const type = parseActorType(front.actor, "actor");
|
|
132
|
+
if (!type.ok)
|
|
133
|
+
return type;
|
|
134
|
+
const participants = parseParticipants(raw.participants, front.participants);
|
|
135
|
+
if (!participants.ok)
|
|
136
|
+
return participants;
|
|
137
|
+
const fields = parseActorFields(front.actor, "actor");
|
|
138
|
+
if (!fields.ok)
|
|
139
|
+
return fields;
|
|
140
|
+
const execution = parseExecution(raw.execution);
|
|
141
|
+
if (!execution.ok)
|
|
142
|
+
return execution;
|
|
143
|
+
const caps = parseCaps(raw.caps);
|
|
144
|
+
if (!caps.ok)
|
|
145
|
+
return caps;
|
|
165
146
|
if (isRecord(raw.policies) &&
|
|
166
147
|
raw.policies.mediaPermission !== undefined &&
|
|
167
148
|
raw.policies.mediaPermission !== "prompt" &&
|
|
168
149
|
raw.policies.mediaPermission !== "granted") {
|
|
169
150
|
return invalid("`policies.mediaPermission` must be `prompt` (the participant answers the browser's own dialog) or `granted`.");
|
|
170
151
|
}
|
|
171
|
-
const
|
|
172
|
-
if (!
|
|
173
|
-
return
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (
|
|
178
|
-
return reviewResult;
|
|
179
|
-
if (reviewResult.value)
|
|
180
|
-
config.review = reviewResult.value;
|
|
181
|
-
if (raw.defaults !== undefined && isRecord(raw.defaults) && raw.defaults.open !== undefined) {
|
|
152
|
+
const policies = parsePolicies(raw.policies);
|
|
153
|
+
if (!policies.ok)
|
|
154
|
+
return policies;
|
|
155
|
+
const review = parseReview(raw.review);
|
|
156
|
+
if (!review.ok)
|
|
157
|
+
return review;
|
|
158
|
+
if (isRecord(raw.defaults) && raw.defaults.open !== undefined) {
|
|
182
159
|
if (typeof raw.defaults.open !== "boolean")
|
|
183
160
|
return invalid("`defaults.open` must be true or false.");
|
|
184
161
|
}
|
|
185
162
|
const defaults = parseDefaults(raw.defaults);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
163
|
+
const comms = parseComms(raw.comms);
|
|
164
|
+
if (!comms.ok)
|
|
165
|
+
return comms;
|
|
166
|
+
const mode = raw.mode === "dry-run" || raw.mode === "live" ? raw.mode : undefined;
|
|
167
|
+
const { surfaces } = front;
|
|
168
|
+
const scenario = str(raw.scenario);
|
|
169
|
+
return {
|
|
170
|
+
ok: true,
|
|
171
|
+
value: {
|
|
172
|
+
schema: STUDY_SCHEMA,
|
|
173
|
+
id,
|
|
174
|
+
...optionalStr("title", raw.title),
|
|
175
|
+
...optionalStr("description", raw.description),
|
|
176
|
+
route: front.route,
|
|
177
|
+
...(mode === undefined ? {} : { mode }),
|
|
178
|
+
subject: subject.value,
|
|
179
|
+
actor: { type: type.value, ...fields.value },
|
|
180
|
+
...(participants.value === undefined ? {} : { participants: participants.value }),
|
|
181
|
+
...(surfaces === undefined ? {} : { surfaces }),
|
|
182
|
+
...(caps.value === undefined ? {} : { caps: caps.value }),
|
|
183
|
+
...(execution.value === undefined ? {} : { execution: execution.value }),
|
|
184
|
+
...(scenario === undefined ? {} : { scenario }),
|
|
185
|
+
...(policies.value === undefined ? {} : { policies: policies.value }),
|
|
186
|
+
...(review.value === undefined ? {} : { review: review.value }),
|
|
187
|
+
...(defaults === undefined ? {} : { defaults }),
|
|
188
|
+
...(comms.value === undefined ? {} : { comms: comms.value }),
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
// `participants` in the form the file wrote it. readStudyFront has checked the form against the
|
|
193
|
+
// route and expanded each list entry that has a count; the entries parse here, each named by the
|
|
194
|
+
// `participants` entry it came from. An instruction that is not a non-empty string is dropped, as
|
|
195
|
+
// the v2 parser drops it.
|
|
196
|
+
function parseParticipants(raw, read) {
|
|
197
|
+
if (read.entries !== undefined) {
|
|
198
|
+
const { source } = read;
|
|
199
|
+
return parseParticipantEntries(read.entries, {
|
|
200
|
+
list: "participants",
|
|
201
|
+
entry: (index) => `participants[${source[index] ?? index}]`,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
if (typeof raw === "number")
|
|
205
|
+
return { ok: true, value: read.count };
|
|
206
|
+
if (!isRecord(raw))
|
|
207
|
+
return { ok: true, value: undefined };
|
|
208
|
+
const instruction = str(read.instruction);
|
|
209
|
+
return {
|
|
210
|
+
ok: true,
|
|
211
|
+
value: {
|
|
212
|
+
...(read.count === undefined ? {} : { count: read.count }),
|
|
213
|
+
...(instruction === undefined ? {} : { instruction }),
|
|
214
|
+
},
|
|
215
|
+
};
|
|
194
216
|
}
|
|
195
217
|
/**
|
|
196
218
|
* The checks that read more than one section of a parsed config, and the defaults they fill:
|
|
197
219
|
* shared-world `execution.concurrency`, `comms.email.recipients` and the local browser's defaults.
|
|
198
|
-
* parseV2 runs them once every section has parsed. parse/study.ts runs them on its v2 view of a v3
|
|
199
|
-
* study.
|
|
200
220
|
*/
|
|
201
|
-
|
|
221
|
+
function checkStudyConfig(config) {
|
|
202
222
|
const smtpReason = smtpValidationReason(config);
|
|
203
223
|
if (smtpReason)
|
|
204
224
|
return invalid(smtpReason);
|
|
@@ -208,9 +228,6 @@ export function checkStudyConfig(config) {
|
|
|
208
228
|
const outputLimitReason = outputTokenLimitValidationReason(config);
|
|
209
229
|
if (outputLimitReason)
|
|
210
230
|
return invalid(outputLimitReason);
|
|
211
|
-
const scenarioCapsReason = scenarioCapsValidationReason(config);
|
|
212
|
-
if (scenarioCapsReason)
|
|
213
|
-
return invalid(scenarioCapsReason);
|
|
214
231
|
// All-parallel default: a multi-participant study that does not declare execution.concurrency
|
|
215
232
|
// runs every participant at once; the declared field is a cap the author chose, never a mode.
|
|
216
233
|
// Independent computer-use participants resolve that default from the final participant count
|
|
@@ -218,7 +235,7 @@ export function checkStudyConfig(config) {
|
|
|
218
235
|
// world's roster is fixed, so its default is filled here for the envelopes and warnings that read
|
|
219
236
|
// the parsed config.
|
|
220
237
|
{
|
|
221
|
-
const participantCount =
|
|
238
|
+
const participantCount = participantList(config)?.length ?? declaredParticipantCount(config) ?? 1;
|
|
222
239
|
if (participantCount > 1 &&
|
|
223
240
|
config.execution?.concurrency === undefined &&
|
|
224
241
|
isSharedWorldComposition(config)) {
|
package/dist/study/discover.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ActorPersonaRef } from "../actors/contract.js";
|
|
2
|
-
import {
|
|
2
|
+
import type { StudyConfig } from "./types.js";
|
|
3
3
|
declare const STUDY_LIST_SCHEMA = "humanish.study-list.v1";
|
|
4
4
|
declare const STUDY_SHOW_SCHEMA = "humanish.study-show.v1";
|
|
5
5
|
type StudyOrigin = "committed" | "ignored" | "explicit";
|
package/dist/study/discover.js
CHANGED
|
@@ -5,7 +5,7 @@ import { lstat, open, readdir, realpath } from "node:fs/promises";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { parse } from "yaml";
|
|
7
7
|
import { parseStudy } from "./config.js";
|
|
8
|
-
import { V2_SCHEMA } from "./
|
|
8
|
+
import { V2_SCHEMA } from "./migrate/v2.js";
|
|
9
9
|
import { assertPreparedSelectedOutputDirectory, assertSafeOutputPathSegment, prepareSelectedOutputDirectory, readContainedRegularFile, } from "../run/contained-output.js";
|
|
10
10
|
import { isNodeError } from "../run/type-guards.js";
|
|
11
11
|
import { studyFileCandidates, STUDY_DIRECTORIES } from "./files.js";
|