@orkestrel/study 0.0.1 → 0.0.2

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.
@@ -1,670 +1,34 @@
1
1
  import { createRequire, isBuiltin, registerHooks, stripTypeScriptTypes } from "node:module";
2
- import { arrayOf, attempt, canonicalStringify, cloneJSONValue, holds, isArray, isError, isFiniteNumber, isInstance, isInteger, isJSONValue, isNonEmptyString, isObject, isRecord, isString, literalOf, parseJSON } from "@orkestrel/contract";
2
+ import { STUDY_CAUSES, STUDY_LAYERS, STUDY_ROLES, StudyControl, StudyDeadline, StudyError, StudySeries, buildStudyAgentDispatch, buildStudyAgentParts, buildStudySchedule, computeStudyScores, failStudyTicket, failureToStudyError, isStudyAgentGauge, isStudyBounds, isStudyCause, isStudyDefinition, isStudyDigest, isStudyError, isStudyGoal, isStudyJudgment, isStudyName, isStudyProduct, isStudyProfile, isStudyRole, isStudyRubric, isStudyScenario, isStudyStop, isStudyTimestamp, isStudyUsage, matchesStudyKeys, parseStudyDefinition, parseStudyGrading, parseStudyLabeling, parseStudyProfile, parseStudyReading, parseStudyRubric, parseStudyScenario, projectStudyAgentRequest, readStudyFailure, studyStopToJSON, translateStudyAgentError, validateStudyAgentUsage, validateStudyInputs } from "../core/index.js";
3
+ import { arrayOf, attempt, canonicalStringify, cloneJSONValue, holds, isArray, isError, isInstance, isInteger, isJSONValue, isNonEmptyString, isObject, isRecord, isString, parseJSON } from "@orkestrel/contract";
3
4
  import { createHash, randomUUID } from "node:crypto";
4
5
  import { basename, dirname, extname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
5
- import { MAX_TIMEOUT_MS, Timeout } from "@orkestrel/timeout";
6
6
  import { closeSync, constants, existsSync, fstatSync, lstatSync, openSync, opendirSync, readFileSync, readSync, realpathSync } from "node:fs";
7
7
  import { fileURLToPath, pathToFileURL } from "node:url";
8
8
  import { lstat, mkdir, mkdtemp, open, readdir, rm } from "node:fs/promises";
9
- import { Budget, isBudgetError } from "@orkestrel/budget";
10
- import { Abort, linkSignal } from "@orkestrel/abort";
11
9
  import { execFile, execFileSync, fork, spawn } from "node:child_process";
12
10
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
13
11
  import { isToolCall } from "@orkestrel/tool";
14
- //#region src/core/constants.ts
15
- /**
16
- * Keys the own self-identity every study error stamps on itself.
17
- * @remarks
18
- * The global symbol registry lets every package, ESM, CommonJS and captured copy read the same
19
- * key. The registry makes the key discoverable; it is a recognition mechanism, not an
20
- * unforgeable provenance marker.
21
- */
22
- var STUDY_ERROR_BRAND = Symbol.for("@orkestrel/study.error");
23
- /** Restricts identifiers to portable path-safe names. */
24
- var STUDY_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
25
- /** Lists every measured request role in report order. */
26
- var STUDY_ROLES = Object.freeze([
27
- "agent",
28
- "judge",
29
- "summary",
30
- "metadata",
31
- "tool"
32
- ]);
33
- /** Lists every ceiling a run can stop at in report order. */
34
- var STUDY_LAYERS = Object.freeze([
35
- "scope",
36
- "window",
37
- "custody",
38
- "meter",
39
- "budget",
40
- "model"
41
- ]);
42
- /** Lists every operational ending in report order. */
43
- var STUDY_CAUSES = Object.freeze([
44
- "capacity",
45
- "infrastructure",
46
- "deadline",
47
- "interrupted",
48
- "content"
49
- ]);
50
- //#endregion
51
- //#region src/core/helpers.ts
52
- /**
53
- * Copies the own entries of a record that hold a value, in their original order.
54
- * @param value - The record whose undefined entries are absent declarations
55
- * @returns A new record without the undefined entries
56
- * @example
57
- * ```ts
58
- * omitStudyAbsent({ context: 512, recap: undefined }) // { context: 512 }
59
- * ```
60
- */
61
- function omitStudyAbsent(value) {
62
- return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== void 0));
63
- }
64
- /**
65
- * Omits every undefined optional declaration so the profile has one JSON form.
66
- * @param profile - The validated profile whose optional leaves may be undefined
67
- * @returns The profile fields with absent qualification, settings, provider, judge and recap removed
68
- * @example
69
- * ```ts
70
- * normalizeStudyProfile(profile) // no `recap` key when the room declares none
71
- * ```
72
- */
73
- function normalizeStudyProfile(profile) {
74
- const { provider, qualification, settings, room, ...required } = profile;
75
- return {
76
- ...required,
77
- room: omitStudyAbsent(room),
78
- ...provider === void 0 ? {} : { provider: {
79
- ...omitStudyAbsent(provider),
80
- digest: omitStudyAbsent(provider.digest)
81
- } },
82
- ...qualification === void 0 ? {} : { qualification },
83
- ...settings === void 0 ? {} : { settings }
84
- };
85
- }
86
- /**
87
- * Projects a stop onto JSON with every unreported reading as null.
88
- * @param stop - The ceiling that stopped a run
89
- * @returns The stop's layer, id, and readings, with an undefined `max` or `consumed` as null
90
- * @example
91
- * ```ts
92
- * studyStopToJSON({ layer: 'model', id: 'num_predict', max: 2048, consumed: undefined })
93
- * // { layer: 'model', id: 'num_predict', max: 2048, consumed: null }
94
- * ```
95
- */
96
- function studyStopToJSON(stop) {
97
- return {
98
- layer: stop.layer,
99
- id: stop.id,
100
- max: stop.max ?? null,
101
- consumed: stop.consumed ?? null
102
- };
103
- }
104
- /**
105
- * Matches exact own enumerable data keys without evaluating accessors.
106
- * @param value - The candidate record
107
- * @param required - Required keys
108
- * @param optional - Allowed optional keys
109
- * @returns True if the record has exactly the allowed data keys; false otherwise
110
- * @example Check exact keys
12
+ import { isBudgetError } from "@orkestrel/budget";
13
+ //#region src/server/helpers.ts
14
+ /** Reports whether a caught value is a Node error carrying the given system code.
15
+ * @remarks Requires an `Error` from this realm. Structured cloning, such as a worker's `postMessage`,
16
+ * drops a Node error's system code, so the cloned error does not match.
17
+ * @param value - The caught value.
18
+ * @param code - The system error code to match.
19
+ * @returns True if the value is an Error from this realm carrying the code; false otherwise.
20
+ * @example Match a system error
111
21
  * ```ts
112
- * import { matchesStudyKeys } from '@orkestrel/study'
22
+ * import { matchesSystemError } from '@orkestrel/study/server'
113
23
  *
114
- * matchesStudyKeys({ id: 'one' }, ['id']) // true
115
- * ```
116
- */
117
- function matchesStudyKeys(value, required, optional = []) {
118
- return holds(() => {
119
- if (!isRecord(value)) return false;
120
- const keys = Reflect.ownKeys(value);
121
- return required.every((key) => Object.hasOwn(value, key)) && keys.every((key) => {
122
- if (typeof key !== "string" || !required.includes(key) && !optional.includes(key)) return false;
123
- const descriptor = Object.getOwnPropertyDescriptor(value, key);
124
- return descriptor !== void 0 && descriptor.enumerable === true && Object.hasOwn(descriptor, "value");
125
- });
126
- });
127
- }
128
- /**
129
- * Builds a bounded schedule from explicit repetition rows and declared scenarios.
130
- * @param definition - The frozen population declaration
131
- * @param scenarios - The complete scenario declarations
132
- * @returns Owned trials in repetition, scenario and row-arm order
133
- * @throws StudyError - Thrown when the population or scenario set is invalid
134
- * @example
135
- * ```ts
136
- * buildStudySchedule(definition, scenarios)
137
- * ```
138
- */
139
- function buildStudySchedule(definition, scenarios) {
140
- if (!isStudyDefinition(definition) || !scenarios.every(isStudyScenario)) throw new StudyError("CONFIG", "Invalid study population");
141
- const ids = new Set(scenarios.map((scenario) => scenario.id));
142
- if (ids.size !== scenarios.length || ids.size !== definition.scenarios.length || !definition.scenarios.every((id) => ids.has(id))) throw new StudyError("CONFIG", "Scenario declarations must match the population exactly");
143
- return Object.freeze([...definition.order.entries()].flatMap(([repetition, row]) => [...definition.scenarios.entries()].flatMap(([scenario, id]) => row.map((arm) => Object.freeze({
144
- id: `r${repetition + 1}-s${scenario + 1}-a${definition.arms.indexOf(arm) + 1}`,
145
- scenario: id,
146
- arm,
147
- repetition
148
- })))));
149
- }
150
- /**
151
- * Validates grading coverage and request room against the declared study inputs.
152
- * @param definition - The population declaration
153
- * @param scenarios - The complete scenario declarations
154
- * @param rubric - The independently held grading declaration
155
- * @param profile - The execution bounds and model room
156
- * @returns Nothing when every cross-input invariant holds
157
- * @throws StudyError - Thrown when any input or cross-reference is invalid
158
- * @example
159
- * ```ts
160
- * validateStudyInputs(definition, scenarios, rubric, profile)
161
- * ```
162
- */
163
- function validateStudyInputs(definition, scenarios, rubric, profile) {
164
- buildStudySchedule(definition, scenarios);
165
- if (!isStudyRubric(rubric) || !isStudyProfile(profile)) throw new StudyError("CONFIG", "Invalid rubric or profile");
166
- if (profile.bounds.request.tokens > profile.room.context - profile.room.safety || profile.room.output > profile.bounds.request.tokens) throw new StudyError("CONFIG", "Request allowance does not fit the model room");
167
- for (const criterion of rubric.criteria) {
168
- const scenario = scenarios.find((entry) => entry.id === criterion.scenario);
169
- if (scenario === void 0 || !scenario.goals.some((goal) => goal.id === criterion.goal)) throw new StudyError("CONFIG", "Criterion references an undeclared goal");
170
- if (definition.purpose === "mechanism" && criterion.method === "semantic") throw new StudyError("CONFIG", "Mechanism studies cannot establish semantic quality");
171
- }
172
- const covered = new Set(rubric.criteria.map((criterion) => canonicalStringify([criterion.scenario, criterion.goal]) ?? ""));
173
- for (const scenario of scenarios) for (const goal of scenario.goals) if (!covered.has(canonicalStringify([scenario.id, goal.id]) ?? "")) throw new StudyError("CONFIG", "Every declared goal needs a criterion");
174
- }
175
- /**
176
- * Computes arm intervals over the complete declared criterion horizon.
177
- * @param manifest - The sealed population and schedule
178
- * @param rubric - The criterion declaration
179
- * @param grading - Judgments bound to the complete scheduled attempt set
180
- * @returns Whole-horizon scores in declared arm order
181
- * @throws StudyError - Thrown when schedule, attempts or judgments are incompatible
182
- * @example
183
- * ```ts
184
- * computeStudyScores(manifest, rubric, grading)
185
- * ```
186
- */
187
- function computeStudyScores(manifest, rubric, grading) {
188
- if (!isStudyRubric(rubric) || !isStudyGrading(grading)) throw new StudyError("EVIDENCE", "Invalid rubric or grading");
189
- const expected = buildStudySchedule(manifest.definition, manifest.scenarios);
190
- if (canonicalStringify(expected) !== canonicalStringify(manifest.schedule)) throw new StudyError("EVIDENCE", "Manifest schedule differs from the declared population");
191
- const trials = new Map(expected.map((trial) => [trial.id, trial]));
192
- if (grading.attempts.length !== expected.length || !grading.attempts.every((id) => trials.has(id))) throw new StudyError("EVIDENCE", "Grading attempts differ from the declared schedule");
193
- const criteria = new Map(rubric.criteria.map((criterion) => [criterion.id, criterion]));
194
- for (const scenario of manifest.scenarios) for (const goal of scenario.goals) if (!rubric.criteria.some((criterion) => criterion.scenario === scenario.id && criterion.goal === goal.id)) throw new StudyError("EVIDENCE", "Rubric omits a declared goal");
195
- for (const criterion of rubric.criteria) if (!manifest.scenarios.some((scenario) => scenario.id === criterion.scenario && scenario.goals.some((goal) => goal.id === criterion.goal)) || manifest.definition.purpose === "mechanism" && criterion.method === "semantic") throw new StudyError("EVIDENCE", "Criterion is incompatible with the study");
196
- const keyed = grading.judgments.map((judgment) => ({
197
- judgment,
198
- key: canonicalStringify([judgment.trial, judgment.criterion]) ?? ""
199
- }));
200
- const firsts = new Map([...keyed.entries()].reverse().map(([index, { key }]) => [key, index]));
201
- for (const [index, { judgment, key }] of keyed.entries()) {
202
- const trial = trials.get(judgment.trial);
203
- const criterion = criteria.get(judgment.criterion);
204
- if (trial === void 0 || criterion === void 0 || criterion.scenario !== trial.scenario || firsts.get(key) !== index) throw new StudyError("EVIDENCE", "Duplicate or foreign trial criterion judgment");
205
- if (criterion.method === "semantic" ? judgment.method !== "human" && judgment.method !== "model" : judgment.method !== criterion.method) throw new StudyError("EVIDENCE", "Judgment method differs from the criterion");
206
- }
207
- const judgments = new Map(keyed.map(({ key, judgment }) => [key, judgment]));
208
- return Object.freeze(manifest.definition.arms.map((arm) => {
209
- const pairs = expected.filter((trial) => trial.arm === arm).flatMap((trial) => rubric.criteria.filter((criterion) => criterion.scenario === trial.scenario).map((criterion) => ({
210
- criterion,
211
- judgment: judgments.get(canonicalStringify([trial.id, criterion.id]) ?? "")
212
- })));
213
- const refuted = pairs.filter(({ judgment }) => judgment?.verdict === "unsuccessful");
214
- const total = pairs.length;
215
- const achieved = pairs.filter(({ judgment }) => judgment?.verdict === "achieved").length;
216
- const unsuccessful = refuted.length;
217
- const critical = refuted.filter(({ criterion }) => criterion.critical).length;
218
- const unknown = total - achieved - unsuccessful;
219
- return Object.freeze({
220
- arm,
221
- total,
222
- achieved,
223
- unsuccessful,
224
- unknown,
225
- critical,
226
- lower: achieved / total,
227
- upper: (achieved + unknown) / total,
228
- coverage: (achieved + unsuccessful) / total
229
- });
230
- }));
231
- }
232
- //#endregion
233
- //#region src/core/validators.ts
234
- /** Checks whether a value names a measured request role. */
235
- var isStudyRole = literalOf(STUDY_ROLES);
236
- /** Checks whether a value names an operational ending. */
237
- var isStudyCause = literalOf(STUDY_CAUSES);
238
- /** Checks whether a value names a ceiling a run can stop at. */
239
- var isStudyLayer = literalOf(STUDY_LAYERS);
240
- /**
241
- * Checks whether a value is the exact record of a ceiling that stopped a run.
242
- * @remarks
243
- * The guard accepts exact own data keys: `layer` and `id` are required and `max` and `consumed` are
244
- * optional. `max` and `consumed` are each undefined or a finite number. Finite foreign readings,
245
- * including negative and fractional values, retain the source's measurement unchanged.
246
- * @param value - The candidate stop
247
- * @returns True if the layer is known, the id is nonempty, and every reading is absent or finite; false otherwise
248
- * @example
249
- * ```ts
250
- * isStudyStop({ layer: 'scope', id: 'Goal tokens', max: 100, consumed: 120 }) // true
251
- * ```
252
- */
253
- function isStudyStop(value) {
254
- return holds(() => isRecord(value) && matchesStudyKeys(value, ["layer", "id"], ["max", "consumed"]) && isStudyLayer(value.layer) && isNonEmptyString(value.id) && ["max", "consumed"].every((key) => value[key] === void 0 || isFiniteNumber(value[key])));
255
- }
256
- /**
257
- * Checks whether an identifier is portable and bounded.
258
- * @param value - The candidate identifier
259
- * @returns True if it is a portable study identifier; false otherwise
260
- * @example
261
- * ```ts
262
- * isStudyName('memory') // true
263
- * ```
264
- */
265
- function isStudyName(value) {
266
- return typeof value === "string" && value.length <= 128 && STUDY_NAME.test(value);
267
- }
268
- /**
269
- * Checks whether all allowances are positive safe integers within timer bounds.
270
- * @param value - The candidate limit
271
- * @returns True if it defines finite positive allowances; false otherwise
272
- * @example
273
- * ```ts
274
- * isStudyLimit({ duration: 1000, calls: 1, tokens: 100, bytes: 1024 }) // true
275
- * ```
276
- */
277
- function isStudyLimit(value) {
278
- return holds(() => {
279
- if (!isRecord(value) || !matchesStudyKeys(value, [
280
- "duration",
281
- "calls",
282
- "tokens",
283
- "bytes"
284
- ])) return false;
285
- return [
286
- "duration",
287
- "calls",
288
- "tokens",
289
- "bytes"
290
- ].every((key) => typeof value[key] === "number" && Number.isSafeInteger(value[key]) && value[key] > 0) && typeof value.duration === "number" && value.duration <= MAX_TIMEOUT_MS;
291
- });
292
- }
293
- /**
294
- * Checks whether nested scopes admit no larger allowance than their parent.
295
- * @param value - The candidate bounds
296
- * @returns True if every nested allowance is ordered; false otherwise
297
- * @example
298
- * ```ts
299
- * isStudyBounds({}) // false
300
- * ```
301
- */
302
- function isStudyBounds(value) {
303
- return holds(() => {
304
- if (!isRecord(value) || !matchesStudyKeys(value, [
305
- "request",
306
- "goal",
307
- "trial",
308
- "series"
309
- ]) || !isStudyLimit(value.request) || !isStudyLimit(value.goal) || !isStudyLimit(value.trial) || !isStudyLimit(value.series)) return false;
310
- const limits = [
311
- value.request,
312
- value.goal,
313
- value.trial,
314
- value.series
315
- ];
316
- for (const [index, limit] of limits.entries()) {
317
- const next = limits[index + 1];
318
- if (next === void 0) continue;
319
- if (limit.duration > next.duration || limit.calls > next.calls || limit.tokens > next.tokens || limit.bytes > next.bytes) return false;
320
- }
321
- return value.request.calls === 1;
322
- });
323
- }
324
- /**
325
- * Checks whether a goal has a bounded identifier and exact JSON input.
326
- * @param value - The candidate goal
327
- * @returns True if the goal is valid; false otherwise
328
- * @example
329
- * ```ts
330
- * isStudyGoal({ id: 'write', input: { text: 'hello' } }) // true
331
- * ```
332
- */
333
- function isStudyGoal(value) {
334
- return holds(() => {
335
- return isRecord(value) && matchesStudyKeys(value, ["id", "input"]) && isStudyName(value.id) && isJSONValue(value.input);
336
- });
337
- }
338
- /**
339
- * Checks whether a scenario declares a nonempty horizon with unique goal ids.
340
- * @param value - The candidate scenario
341
- * @returns True if the scenario has a valid horizon; false otherwise
342
- * @example
343
- * ```ts
344
- * isStudyScenario({ id: 'shift', goals: [{ id: 'handoff', input: 'Prepare handoff' }] }) // true
345
- * ```
346
- */
347
- function isStudyScenario(value) {
348
- return holds(() => {
349
- if (!isRecord(value) || !matchesStudyKeys(value, ["id", "goals"]) || !isStudyName(value.id) || !isArray(value.goals)) return false;
350
- return value.goals.length > 0 && value.goals.length <= 4096 && value.goals.every(isStudyGoal) && new Set(value.goals.map((goal) => goal.id)).size === value.goals.length && isJSONValue(value.goals);
351
- });
352
- }
353
- /**
354
- * Checks whether a study freezes a complete arm order for every repetition.
355
- * @param value - The candidate definition
356
- * @returns True if its population and repetition orders are valid; false otherwise
357
- * @example
358
- * ```ts
359
- * isStudyDefinition({ name: 'tools' }) // false
360
- * ```
361
- */
362
- function isStudyDefinition(value) {
363
- return holds(() => {
364
- if (!isRecord(value) || !isJSONValue(value) || !matchesStudyKeys(value, [
365
- "name",
366
- "version",
367
- "question",
368
- "purpose",
369
- "driver",
370
- "arms",
371
- "scenarios",
372
- "order",
373
- "retries",
374
- "rubric"
375
- ]) || !isStudyName(value.name) || !isStudyName(value.driver) || !isNonEmptyString(value.version) || !isNonEmptyString(value.question) || value.purpose !== "mechanism" && value.purpose !== "quality" && value.purpose !== "capacity" || !isArray(value.arms) || value.arms.length === 0 || !value.arms.every(isStudyName) || new Set(value.arms).size !== value.arms.length || !isArray(value.scenarios) || value.scenarios.length === 0 || !value.scenarios.every(isStudyName) || new Set(value.scenarios).size !== value.scenarios.length || !isArray(value.order) || !isJSONValue(value.order) || value.order.length === 0 || value.retries !== 0 || !isNonEmptyString(value.rubric)) return false;
376
- const arms = value.arms;
377
- if (arms.length * value.scenarios.length * value.order.length > 4096) return false;
378
- return value.order.every((order) => isArray(order) && order.length === arms.length && new Set(order).size === arms.length && order.every((arm) => typeof arm === "string" && arms.includes(arm)));
379
- });
380
- }
381
- /**
382
- * Checks whether generation room leaves positive context for input.
383
- * @param value - The candidate room
384
- * @returns True if input, output and safety allowances fit and any declared recap fits; false otherwise
385
- * @example
386
- * ```ts
387
- * isStudyRoom({ context: 20480, output: 8192, safety: 512, recap: 2048, think: true }) // true
388
- * isStudyRoom({ context: 20480, output: 8192, safety: 512, think: true }) // true
389
- * ```
390
- */
391
- function isStudyRoom(value) {
392
- return holds(() => {
393
- if (!isRecord(value) || !matchesStudyKeys(value, [
394
- "context",
395
- "output",
396
- "safety",
397
- "think"
398
- ], ["recap"]) || typeof value.think !== "boolean") return false;
399
- if (![
400
- "context",
401
- "output",
402
- "safety",
403
- ...value.recap === void 0 ? [] : ["recap"]
404
- ].every((key) => typeof value[key] === "number" && Number.isSafeInteger(value[key]) && value[key] > 0)) return false;
405
- return typeof value.context === "number" && typeof value.output === "number" && typeof value.safety === "number" && value.output + value.safety < value.context && (value.recap === void 0 || typeof value.recap === "number" && value.recap + value.safety < value.context);
406
- });
407
- }
408
- /**
409
- * Checks whether a provider declaration names artifacts, an HTTP origin and models.
410
- * @param value - The candidate provider declaration
411
- * @returns True if the declaration has a supported origin, nonempty names, and a judge declared with its digest or not at all; false otherwise
412
- * @example
413
- * ```ts
414
- * isStudyProvider({ entry: 'dist/index.js' }) // false
415
- * ```
416
- */
417
- function isStudyProvider(value) {
418
- return holds(() => {
419
- if (!isRecord(value) || !matchesStudyKeys(value, [
420
- "entry",
421
- "url",
422
- "model",
423
- "digest"
424
- ], ["judge"]) || ![
425
- "entry",
426
- "url",
427
- "model"
428
- ].every((key) => isNonEmptyString(value[key])) || typeof value.url !== "string" || !isRecord(value.digest) || !matchesStudyKeys(value.digest, ["model"], ["judge"]) || !isStudyDigest(value.digest.model) || value.judge === void 0 !== (value.digest.judge === void 0) || value.judge !== void 0 && (!isNonEmptyString(value.judge) || !isStudyDigest(value.digest.judge))) return false;
429
- const url = new URL(value.url);
430
- return ["http:", "https:"].includes(url.protocol) && url.username === "" && url.password === "" && url.search === "" && url.hash === "" && url.pathname === "/";
431
- });
432
- }
433
- /**
434
- * Checks whether a profile carries finite bounds and an optional explicit provider.
435
- * @param value - The candidate profile
436
- * @returns True if the profile is structurally valid; false otherwise
437
- * @example
438
- * ```ts
439
- * isStudyProfile({ name: 'local' }) // false
440
- * ```
441
- */
442
- function isStudyProfile(value) {
443
- return holds(() => {
444
- return isRecord(value) && matchesStudyKeys(value, [
445
- "name",
446
- "agent",
447
- "subject",
448
- "roots",
449
- "bounds",
450
- "room"
451
- ], [
452
- "provider",
453
- "qualification",
454
- "settings"
455
- ]) && isStudyName(value.name) && isNonEmptyString(value.agent) && isArray(value.roots) && isJSONValue(value.roots) && value.roots.length > 0 && value.roots.every((root) => typeof root === "string" && root.trim().length > 0) && new Set(value.roots).size === value.roots.length && isNonEmptyString(value.subject) && isStudyBounds(value.bounds) && isStudyRoom(value.room) && (value.provider === void 0 || isStudyProvider(value.provider)) && (value.qualification === void 0 || isNonEmptyString(value.qualification)) && (value.settings === void 0 || isRecord(value.settings) && isJSONValue(value.settings));
456
- });
457
- }
458
- /**
459
- * Checks whether a byte identity is a lowercase SHA-256 digest.
460
- * @param value - The candidate identity
461
- * @returns True if it has exactly 64 hexadecimal digits; false otherwise
462
- * @example
463
- * ```ts
464
- * isStudyDigest('a'.repeat(64)) // true
465
- * ```
466
- */
467
- function isStudyDigest(value) {
468
- return typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
469
- }
470
- /**
471
- * Checks whether a timestamp is a canonical UTC millisecond instant.
472
- * @param value - The candidate timestamp
473
- * @returns True if it is a valid canonical timestamp; false otherwise
474
- * @example
475
- * ```ts
476
- * isStudyTimestamp('2026-10-14T12:00:00.000Z') // true
477
- * ```
478
- */
479
- function isStudyTimestamp(value) {
480
- return holds(() => {
481
- return typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value) && new Date(value).toISOString() === value;
482
- });
483
- }
484
- /**
485
- * Checks whether request usage contains nonnegative safe measurements.
486
- * @param value - The candidate usage
487
- * @returns True if the role and every known measurement are valid; false otherwise
488
- * @example
489
- * ```ts
490
- * isStudyUsage({ role: 'tool', calls: 0, bytes: 0, duration: 0 }) // true
491
- * ```
492
- */
493
- function isStudyUsage(value) {
494
- return holds(() => {
495
- return isRecord(value) && matchesStudyKeys(value, [
496
- "role",
497
- "calls",
498
- "duration"
499
- ], ["tokens", "bytes"]) && isStudyRole(value.role) && ["calls", "duration"].every((key) => typeof value[key] === "number" && Number.isSafeInteger(value[key]) && value[key] >= 0) && (value.bytes === void 0 || typeof value.bytes === "number" && Number.isSafeInteger(value.bytes) && value.bytes >= 0) && (value.tokens === void 0 || typeof value.tokens === "number" && Number.isSafeInteger(value.tokens) && value.tokens >= 0);
500
- });
501
- }
502
- /**
503
- * Checks a retained public product without awarding task achievement.
504
- * @param value - The candidate product
505
- * @returns True if the product carries JSON output and partial status; false otherwise
506
- * @example
507
- * ```ts
508
- * isStudyProduct({ value: { answer: 1 }, partial: true }) // true
509
- * ```
510
- */
511
- function isStudyProduct(value) {
512
- return holds(() => isRecord(value) && typeof value.partial === "boolean" && isJSONValue(value.value));
513
- }
514
- /**
515
- * Checks whether a criterion declares exact grading inputs and references.
516
- * @param value - The candidate criterion
517
- * @returns True if its structural grading declaration is valid; false otherwise
518
- * @example
519
- * ```ts
520
- * isStudyCriterion({ id: 'saved', scenario: 'shift', goal: 'write', method: 'state', critical: true, assertion: true }) // true
521
- * ```
522
- */
523
- function isStudyCriterion(value) {
524
- return holds(() => {
525
- return isRecord(value) && matchesStudyKeys(value, [
526
- "id",
527
- "scenario",
528
- "goal",
529
- "method",
530
- "critical",
531
- "assertion"
532
- ]) && isStudyName(value.id) && isStudyName(value.scenario) && isStudyName(value.goal) && (value.method === "state" || value.method === "trace" || value.method === "semantic") && typeof value.critical === "boolean" && isJSONValue(value.assertion);
533
- });
534
- }
535
- /**
536
- * Checks whether a rubric declares nonempty criteria with unique identities.
537
- * @param value - The candidate rubric
538
- * @returns True if its version and criteria are valid; false otherwise
539
- * @example
540
- * ```ts
541
- * isStudyRubric({ version: '1', criteria: [] }) // false
542
- * ```
543
- */
544
- function isStudyRubric(value) {
545
- return holds(() => {
546
- return isRecord(value) && matchesStudyKeys(value, ["version", "criteria"]) && typeof value.version === "string" && value.version.trim().length > 0 && isArray(value.criteria) && value.criteria.length > 0 && value.criteria.every(isStudyCriterion) && new Set(value.criteria.map((criterion) => criterion.id)).size === value.criteria.length && isJSONValue(value.criteria);
547
- });
548
- }
549
- /**
550
- * Checks whether a judgment carries a supported verdict and evidence references.
551
- * @param value - The candidate judgment
552
- * @returns True if the judgment is attributable and structurally valid; false otherwise
553
- * @example
554
- * ```ts
555
- * isStudyJudgment({ trial: 'r1-s1-a1', criterion: 'saved', verdict: 'unknown', method: 'state', reason: 'No evidence', references: [] }) // true
556
- * ```
557
- */
558
- function isStudyJudgment(value) {
559
- return holds(() => {
560
- return isRecord(value) && matchesStudyKeys(value, [
561
- "trial",
562
- "criterion",
563
- "verdict",
564
- "method",
565
- "reason",
566
- "references"
567
- ]) && isStudyName(value.trial) && isStudyName(value.criterion) && (value.verdict === "achieved" || value.verdict === "unsuccessful" || value.verdict === "unknown") && (value.method === "state" || value.method === "trace" || value.method === "human" || value.method === "model") && typeof value.reason === "string" && value.reason.trim().length > 0 && isArray(value.references) && isJSONValue(value.references) && value.references.every((reference) => typeof reference === "string" && reference.trim().length > 0) && new Set(value.references).size === value.references.length && (value.verdict === "unknown" || value.references.length > 0);
568
- });
569
- }
570
- /**
571
- * Checks whether imported semantic labels have accountable packet provenance.
572
- * @param value - The candidate labels
573
- * @returns True if provenance, usage and semantic judgments are valid; false otherwise
574
- * @example
575
- * ```ts
576
- * isStudyLabeling({}) // false
577
- * ```
578
- */
579
- function isStudyLabeling(value) {
580
- return holds(() => {
581
- return isRecord(value) && matchesStudyKeys(value, [
582
- "packet",
583
- "labeler",
584
- "version",
585
- "created",
586
- "usage",
587
- "judgments"
588
- ]) && isStudyDigest(value.packet) && typeof value.labeler === "string" && value.labeler.trim().length > 0 && typeof value.version === "string" && value.version.trim().length > 0 && isStudyTimestamp(value.created) && isArray(value.usage) && Reflect.ownKeys(value.usage).length === value.usage.length + 1 && Array.from(value.usage).every(isStudyUsage) && isArray(value.judgments) && value.judgments.every(isStudyJudgment) && value.judgments.every((judgment) => judgment.method === "human" || judgment.method === "model") && new Set(value.judgments.map((judgment) => canonicalStringify([judgment.trial, judgment.criterion]))).size === value.judgments.length && isJSONValue(value.judgments);
589
- });
590
- }
591
- /**
592
- * Checks whether a value names a supported study error code.
593
- * @param value - The candidate code
594
- * @returns True if it is a declared study error code; false otherwise
595
- * @example
596
- * ```ts
597
- * isStudyErrorCode('CONFIG') // true
598
- * ```
599
- */
600
- function isStudyErrorCode(value) {
601
- return value === "CONFIG" || value === "EVIDENCE" || value === "CUSTODY" || value === "CAPACITY" || value === "FAULT" || value === "DEADLINE";
602
- }
603
- /**
604
- * Checks whether grading binds accountable judgments to unique attempt identities.
605
- * @param value - The candidate grading
606
- * @returns True if its identities, attempts and judgments are valid; false otherwise
607
- * @example
608
- * ```ts
609
- * isStudyGrading({}) // false
610
- * ```
611
- */
612
- function isStudyGrading(value) {
613
- return holds(() => {
614
- return isRecord(value) && matchesStudyKeys(value, [
615
- "result",
616
- "rubric",
617
- "grader",
618
- "instrument",
619
- "attempts",
620
- "judgments"
621
- ], ["labels", "packet"]) && isStudyDigest(value.result) && isStudyDigest(value.rubric) && isStudyDigest(value.instrument) && typeof value.grader === "string" && value.grader.trim().length > 0 && (value.labels === void 0 || isStudyDigest(value.labels)) && (value.packet === void 0 || isStudyDigest(value.packet)) && isArray(value.attempts) && isJSONValue(value.attempts) && value.attempts.every(isStudyName) && new Set(value.attempts).size === value.attempts.length && isArray(value.judgments) && value.judgments.every(isStudyJudgment) && new Set(value.judgments.map((judgment) => canonicalStringify([judgment.trial, judgment.criterion]))).size === value.judgments.length && isJSONValue(value.judgments);
622
- });
623
- }
624
- //#endregion
625
- //#region src/core/errors.ts
626
- /** Reports a study contract or operational failure with a stable code. */
627
- var StudyError = class extends Error {
628
- code;
629
- failure;
630
- /**
631
- * Records the failing boundary and optional operational attribution.
632
- * @param code - The machine-readable boundary
633
- * @param message - The specific failure
634
- * @param failure - The optional operational reading
635
- */
636
- constructor(code, message, failure) {
637
- super(message);
638
- Object.defineProperty(this, STUDY_ERROR_BRAND, {
639
- value: this,
640
- configurable: true
641
- });
642
- this.name = "StudyError";
643
- this.code = code;
644
- this.failure = failure;
645
- }
646
- };
647
- /**
648
- * Narrows a caught value to an attributable study error.
649
- * @remarks
650
- * Recognition combines the native `Error` base, a subclass prototype, the fixed name, a declared
651
- * study code, and an own {@link STUDY_ERROR_BRAND} descriptor whose value is the error itself.
652
- * The global key is recognized across package, ESM, CommonJS and captured module copies; it is
653
- * not unforgeable provenance. A transparent proxy is refused because its forwarded descriptor
654
- * stores the target, and an unbranded copy, an inherited brand, or a property-only lookalike
655
- * stays outside the type. Hostile property access returns false instead of throwing.
656
- * @param value - The caught value
657
- * @returns True if the value is a study error; false otherwise
658
- * @example
659
- * ```ts
660
- * isStudyError(new StudyError('CONFIG', 'missing definition')) // true
24
+ * const error = Object.assign(new Error('Missing declaration'), { code: 'ENOENT' })
25
+ * matchesSystemError(error, 'ENOENT') // true
26
+ * matchesSystemError({ code: 'ENOENT' }, 'ENOENT') // false
661
27
  * ```
662
28
  */
663
- function isStudyError(value) {
664
- return isError(value) && holds(() => Object.getPrototypeOf(value) !== Error.prototype && value.name === "StudyError" && Object.getOwnPropertyDescriptor(value, STUDY_ERROR_BRAND)?.value === value && "code" in value && isStudyErrorCode(value.code));
29
+ function matchesSystemError(value, code) {
30
+ return holds(() => isError(value) && "code" in value && value.code === code);
665
31
  }
666
- //#endregion
667
- //#region src/server/helpers.ts
668
32
  /** Checks strict lexical containment under a selected directory.
669
33
  * @param root - The canonical directory.
670
34
  * @param path - The candidate absolute file.
@@ -2076,153 +1440,6 @@ var StudyResultStore = class {
2076
1440
  }
2077
1441
  };
2078
1442
  //#endregion
2079
- //#region src/core/parsers.ts
2080
- /**
2081
- * Parses a foreign reading into a number a stop can retain.
2082
- * @param value - The unknown reading a source reports
2083
- * @returns The value when it is a finite number; undefined otherwise
2084
- * @example
2085
- * ```ts
2086
- * parseStudyReading(2048) // 2048
2087
- * parseStudyReading(Number.NaN) // undefined
2088
- * ```
2089
- */
2090
- function parseStudyReading(value) {
2091
- return isFiniteNumber(value) ? value : void 0;
2092
- }
2093
- /**
2094
- * Parses a definition into an independent frozen validated snapshot.
2095
- * @param value - The unknown configuration
2096
- * @returns The owned study definition
2097
- * @throws StudyError - Thrown when configuration is invalid or cannot be owned
2098
- * @example
2099
- * ```ts
2100
- * parseStudyDefinition(configuration)
2101
- * ```
2102
- */
2103
- function parseStudyDefinition(value) {
2104
- try {
2105
- if (!isStudyDefinition(value)) throw new StudyError("CONFIG", "Invalid study definition");
2106
- const snapshot = cloneJSONValue(value);
2107
- if (isStudyDefinition(snapshot)) return snapshot;
2108
- } catch {
2109
- throw new StudyError("CONFIG", "Invalid or unreadable study definition");
2110
- }
2111
- throw new StudyError("CONFIG", "Invalid study definition snapshot");
2112
- }
2113
- /**
2114
- * Parses a scenario into an independent frozen validated snapshot.
2115
- * @param value - The unknown configuration
2116
- * @returns The owned scenario and goal inputs
2117
- * @throws StudyError - Thrown when configuration is invalid or cannot be owned
2118
- * @example
2119
- * ```ts
2120
- * parseStudyScenario({ id: 'shift', goals: [{ id: 'write', input: {} }] })
2121
- * ```
2122
- */
2123
- function parseStudyScenario(value) {
2124
- try {
2125
- if (!isStudyScenario(value)) throw new StudyError("CONFIG", "Invalid study scenario");
2126
- const snapshot = cloneJSONValue(value);
2127
- if (isStudyScenario(snapshot)) return snapshot;
2128
- } catch {
2129
- throw new StudyError("CONFIG", "Invalid or unreadable study scenario");
2130
- }
2131
- throw new StudyError("CONFIG", "Invalid study scenario snapshot");
2132
- }
2133
- /**
2134
- * Parses a profile into an independent frozen validated snapshot.
2135
- * @param value - The unknown configuration
2136
- * @returns The owned profile with absent optional settings preserved
2137
- * @throws StudyError - Thrown when configuration is invalid or cannot be owned
2138
- * @example
2139
- * ```ts
2140
- * parseStudyProfile(configuration)
2141
- * ```
2142
- */
2143
- function parseStudyProfile(value) {
2144
- try {
2145
- if (!isStudyProfile(value)) throw new StudyError("CONFIG", "Invalid study profile");
2146
- const snapshot = cloneJSONValue(normalizeStudyProfile(value));
2147
- if (isStudyProfile(snapshot)) return snapshot;
2148
- } catch {
2149
- throw new StudyError("CONFIG", "Invalid or unreadable study profile");
2150
- }
2151
- throw new StudyError("CONFIG", "Invalid study profile snapshot");
2152
- }
2153
- /**
2154
- * Parses a rubric into an independent frozen validated snapshot.
2155
- * @param value - The unknown grading declaration
2156
- * @returns The owned rubric and assertion inputs
2157
- * @throws StudyError - Thrown when configuration is invalid or cannot be owned
2158
- * @example
2159
- * ```ts
2160
- * parseStudyRubric(configuration)
2161
- * ```
2162
- */
2163
- function parseStudyRubric(value) {
2164
- try {
2165
- if (!isStudyRubric(value)) throw new StudyError("CONFIG", "Invalid study rubric");
2166
- const snapshot = cloneJSONValue(value);
2167
- if (isStudyRubric(snapshot)) return snapshot;
2168
- } catch {
2169
- throw new StudyError("CONFIG", "Invalid or unreadable study rubric");
2170
- }
2171
- throw new StudyError("CONFIG", "Invalid study rubric snapshot");
2172
- }
2173
- /**
2174
- * Parses grading into an independent frozen validated evidence snapshot.
2175
- * @param value - The unknown grading evidence
2176
- * @returns The owned grading identity and judgments
2177
- * @throws StudyError - Thrown when evidence is invalid or cannot be owned
2178
- * @example
2179
- * ```ts
2180
- * parseStudyGrading(evidence)
2181
- * ```
2182
- */
2183
- function parseStudyGrading(value) {
2184
- try {
2185
- if (!isStudyGrading(value)) throw new StudyError("EVIDENCE", "Invalid study grading");
2186
- const { labels, packet, ...required } = value;
2187
- const snapshot = cloneJSONValue({
2188
- ...required,
2189
- ...labels === void 0 ? {} : { labels },
2190
- ...packet === void 0 ? {} : { packet }
2191
- });
2192
- if (isStudyGrading(snapshot)) return snapshot;
2193
- } catch {
2194
- throw new StudyError("EVIDENCE", "Invalid or unreadable study grading");
2195
- }
2196
- throw new StudyError("EVIDENCE", "Invalid study grading snapshot");
2197
- }
2198
- /**
2199
- * Parses semantic labels into an independent frozen validated evidence snapshot.
2200
- * @param value - The unknown imported labels
2201
- * @returns The owned packet provenance, usage and judgments
2202
- * @throws StudyError - Thrown when evidence is invalid or cannot be owned
2203
- * @example
2204
- * ```ts
2205
- * parseStudyLabeling(evidence)
2206
- * ```
2207
- */
2208
- function parseStudyLabeling(value) {
2209
- try {
2210
- if (!isStudyLabeling(value)) throw new StudyError("EVIDENCE", "Invalid study labels");
2211
- const snapshot = cloneJSONValue({
2212
- ...value,
2213
- usage: value.usage.map(({ tokens, bytes, ...usage }) => ({
2214
- ...usage,
2215
- ...tokens === void 0 ? {} : { tokens },
2216
- ...bytes === void 0 ? {} : { bytes }
2217
- }))
2218
- });
2219
- if (isStudyLabeling(snapshot)) return snapshot;
2220
- } catch {
2221
- throw new StudyError("EVIDENCE", "Invalid or unreadable study labels");
2222
- }
2223
- throw new StudyError("EVIDENCE", "Invalid study labels snapshot");
2224
- }
2225
- //#endregion
2226
1443
  //#region src/server/stores/StudyGradingStore.ts
2227
1444
  /** Retains immutable deterministic grading versions independently of execution bytes. */
2228
1445
  var StudyGradingStore = class {
@@ -2290,7 +1507,7 @@ var StudyGradingStore = class {
2290
1507
  try {
2291
1508
  await lstat(directory);
2292
1509
  } catch (error) {
2293
- if (error instanceof Error && "code" in error && error.code === "ENOENT") return void 0;
1510
+ if (matchesSystemError(error, "ENOENT")) return void 0;
2294
1511
  throw error;
2295
1512
  }
2296
1513
  await checkStudyDirectory(directory);
@@ -2331,14 +1548,14 @@ var StudyGradingStore = class {
2331
1548
  try {
2332
1549
  await mkdir(root);
2333
1550
  } catch (error) {
2334
- if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
1551
+ if (!matchesSystemError(error, "EEXIST")) throw error;
2335
1552
  }
2336
1553
  await checkStudyDirectory(root);
2337
1554
  const directory = join(root, id);
2338
1555
  try {
2339
1556
  await mkdir(directory);
2340
1557
  } catch (error) {
2341
- if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
1558
+ if (!matchesSystemError(error, "EEXIST")) throw error;
2342
1559
  const existing = await this.read(id);
2343
1560
  if (existing === void 0 || canonicalStringify(existing) !== canonicalStringify(grading) || hashStudyBytes(await readStudyBytes(join(directory, "grading.json"), 16777216)) !== id) throw new StudyError("CUSTODY", "Existing grading version is incompatible");
2344
1561
  return id;
@@ -2390,6 +1607,7 @@ study grade <result> --rubric <path>
2390
1607
  study label <result> --grading <sha256> --labels <path>
2391
1608
  study report <result> --grading <sha256>
2392
1609
  study --help
1610
+ study --version
2393
1611
 
2394
1612
  Profiles default to local. Flags require values and may appear only once.
2395
1613
  Results default to tmp/studies/<result>; scratch also uses tmp/studies.
@@ -2405,6 +1623,81 @@ Exit codes: 0 success, 64 invalid input, 124 deadline, 130 interruption, 1 opera
2405
1623
  `;
2406
1624
  //#endregion
2407
1625
  //#region src/server/commands/helpers.ts
1626
+ /** Reads one declaration file and refuses a missing or linked path with its root-relative path.
1627
+ * @param root - The consumer root used to describe a missing or linked declaration.
1628
+ * @param path - The already resolved and contained declaration path.
1629
+ * @returns The exact declaration bytes under the custody metadata allowance.
1630
+ * @throws StudyError - Thrown with code `CONFIG` when the path is missing or linked; other custody refusals retain their codes.
1631
+ * @example Read declaration bytes
1632
+ * ```ts
1633
+ * import { readStudyDeclarationBytes } from '@orkestrel/study/server'
1634
+ *
1635
+ * declare const root: string
1636
+ * declare const path: string
1637
+ * await readStudyDeclarationBytes(root, path)
1638
+ * ```
1639
+ */
1640
+ async function readStudyDeclarationBytes(root, path) {
1641
+ try {
1642
+ const absolute = resolve(path);
1643
+ const origin = parse(absolute).root;
1644
+ const names = relative(origin, absolute).split(sep).filter(Boolean);
1645
+ for (const [index] of names.entries()) {
1646
+ const candidate = join(origin, ...names.slice(0, index + 1));
1647
+ const stat = await lstat(candidate);
1648
+ if (stat.isSymbolicLink()) throw new StudyError("CONFIG", `Declaration path is linked: ${relative(root, candidate).split(sep).join("/")}`);
1649
+ if (!stat.isDirectory()) break;
1650
+ }
1651
+ return await readStudyBytes(path, STUDY_CUSTODY_BYTES);
1652
+ } catch (error) {
1653
+ if (matchesSystemError(error, "ENOENT")) throw new StudyError("CONFIG", `Declaration is missing: ${relative(root, path).split(sep).join("/")}`);
1654
+ throw error;
1655
+ }
1656
+ }
1657
+ /** Reads a study declaration, its driver's scenarios, and its rubric as the command resolves them.
1658
+ * @param root - The consumer root containing the studies directory.
1659
+ * @param study - The selected study name.
1660
+ * @param signal - Optional cancellation checked between scenario reads.
1661
+ * @returns The parsed declaration, driver bundle, ordered scenarios, fixtures, and selected rubric.
1662
+ * @throws StudyError - Thrown when declarations are missing, invalid, or incompatible.
1663
+ * @example Read a study declaration
1664
+ * ```ts
1665
+ * import type { StudyDeclaration } from '@orkestrel/study/server'
1666
+ * import { readStudyDeclaration } from '@orkestrel/study/server'
1667
+ *
1668
+ * declare const root: string
1669
+ * const declaration: StudyDeclaration = await readStudyDeclaration(root, 'assessment')
1670
+ * ```
1671
+ */
1672
+ async function readStudyDeclaration(root, study, signal) {
1673
+ const directory = resolveStudyPath(join(root, "studies"), study);
1674
+ const definition = parseStudyDefinition(decodeStudyJSON(await readStudyDeclarationBytes(root, resolveStudyPath(directory, "study.json"))));
1675
+ if (definition.name !== study) throw new StudyError("CONFIG", "Study name differs from its selected directory");
1676
+ const bundle = resolveStudyPath(join(root, "studies"), definition.driver);
1677
+ const steps = [];
1678
+ for (const id of definition.scenarios) {
1679
+ signal?.throwIfAborted();
1680
+ const path = `scenarios/${id}.json`;
1681
+ const bytes = await readStudyDeclarationBytes(root, resolveStudyPath(bundle, path));
1682
+ const scenario = parseStudyScenario(decodeStudyJSON(bytes));
1683
+ if (scenario.id !== id) throw new StudyError("CONFIG", "Scenario identity is incompatible");
1684
+ steps.push({
1685
+ scenario,
1686
+ fixture: {
1687
+ path,
1688
+ digest: hashStudyBytes(bytes)
1689
+ }
1690
+ });
1691
+ }
1692
+ const rubric = parseStudyRubric(decodeStudyJSON(await readStudyDeclarationBytes(root, resolveStudyPath(directory, definition.rubric))));
1693
+ return {
1694
+ bundle,
1695
+ definition,
1696
+ scenarios: Object.freeze(steps.map((step) => step.scenario)),
1697
+ fixtures: Object.freeze(steps.map((step) => step.fixture)),
1698
+ rubric
1699
+ };
1700
+ }
2408
1701
  /**
2409
1702
  * Resolves a portable relative artifact path strictly inside its selected directory.
2410
1703
  * @param directory - The selected custody or study directory
@@ -2616,1083 +1909,167 @@ function computeStudyReport(result, rubric, grading) {
2616
1909
  const identity = result.manifest.identity;
2617
1910
  return {
2618
1911
  command: "report",
2619
- result: result.manifest.id,
2620
- seal: result.seal.id,
2621
- identity: {
2622
- agent: identity.agent,
2623
- subject: identity.subject,
2624
- instrument: identity.instrument,
2625
- fixtures: identity.fixtures,
2626
- profile: identity.profile,
2627
- ...identity.provider === void 0 ? {} : { provider: identity.provider }
2628
- },
2629
- purpose: result.manifest.definition.purpose,
2630
- population: "Complete declared trial and criterion horizon",
2631
- uncertainty: repetitions.length === 1 ? "One repetition is descriptive and does not establish replicated capacity or quality." : "Repetitions are retained separately; intervals represent unresolved grading, not statistical confidence.",
2632
- scores: scores.map((score) => ({ ...score })),
2633
- repetitions,
2634
- operations: {
2635
- trials: {
2636
- scheduled: result.manifest.schedule.length,
2637
- terminal: result.attempts.length,
2638
- ...computeStudyPartition(result.attempts),
2639
- completed: result.attempts.filter((attempt) => attempt.failure === void 0 && attempt.observations.every((observation) => completed.has(observation))).length,
2640
- faults: STUDY_CAUSES.map((cause) => ({
2641
- cause,
2642
- ...computeStudyPartition(result.attempts.filter((attempt) => attempt.failure?.cause === cause))
2643
- })),
2644
- attributions: computeStudyAttributions(result.attempts.flatMap((attempt) => attempt.failure === void 0 ? [] : [{
2645
- trial: attempt.trial.id,
2646
- goal: void 0,
2647
- failure: attempt.failure,
2648
- executed: attempt.executed
2649
- }]))
2650
- },
2651
- goals: {
2652
- scheduled: observations.length,
2653
- ...computeStudyPartition(observations),
2654
- products: observations.filter((observation) => observation.product !== void 0).length,
2655
- partial: observations.filter((observation) => observation.product?.partial === true).length,
2656
- completed: observations.filter((observation) => completed.has(observation)).length,
2657
- faults: STUDY_CAUSES.map((cause) => ({
2658
- cause,
2659
- ...computeStudyPartition(observations.filter((observation) => observation.failure?.cause === cause))
2660
- })),
2661
- attributions: computeStudyAttributions(result.attempts.flatMap((attempt) => attempt.observations.flatMap((observation) => observation.failure === void 0 ? [] : [{
2662
- trial: attempt.trial.id,
2663
- goal: observation.goal,
2664
- failure: observation.failure,
2665
- executed: observation.executed
2666
- }])))
2667
- }
2668
- },
2669
- costs: {
2670
- duration: result.attempts.reduce((total, attempt) => total + attempt.duration, 0),
2671
- roles: STUDY_ROLES.map((role) => {
2672
- const charges = usage.filter((entry) => entry.role === role);
2673
- return {
2674
- role,
2675
- measurements: charges.length,
2676
- calls: charges.reduce((total, entry) => total + entry.calls, 0),
2677
- duration: charges.reduce((total, entry) => total + entry.duration, 0),
2678
- tokens: {
2679
- known: charges.reduce((total, entry) => total + (entry.tokens ?? 0), 0),
2680
- unknown: charges.filter((entry) => entry.tokens === void 0).length
2681
- },
2682
- bytes: {
2683
- known: charges.reduce((total, entry) => total + (entry.bytes ?? 0), 0),
2684
- unknown: charges.filter((entry) => entry.bytes === void 0).length
2685
- }
2686
- };
2687
- })
2688
- }
2689
- };
2690
- }
2691
- //#endregion
2692
- //#region src/server/commands/parsers.ts
2693
- /** Parses public command tokens with operation-specific grammar diagnostics.
2694
- * @param arguments_ - Exact command tokens, after the CLI help boundary.
2695
- * @returns The independent operation request.
2696
- * @throws StudyError when a token, required flag, or portable path is invalid.
2697
- * @example Parse a study command
2698
- * ```ts
2699
- * import { parseStudyArguments } from '@orkestrel/study/server'
2700
- *
2701
- * parseStudyArguments(['check', 'files', '--profile', 'local'])
2702
- * ```
2703
- */
2704
- function parseStudyArguments(arguments_) {
2705
- const command = arguments_[0];
2706
- const target = arguments_[1];
2707
- const usage = "study check <study> [--profile <name>]\nstudy execute <study> [--profile <name>] [--out <path>] [--expected <sha256>]\nstudy grade <result> --rubric <path>\nstudy label <result> --grading <sha256> --labels <path>\nstudy report <result> --grading <sha256>\nstudy --help\n\nProfiles default to local. Flags require values and may appear only once.\nResults default to tmp/studies/<result>; scratch also uses tmp/studies.\nA result is its id under tmp/studies, a root-relative path, or an absolute path.\nRelative paths, including --out, must stay inside the consumer working directory.\nOutput directories must be new; existing results are never overwritten.\n--expected pins the fingerprint returned by check. --grading selects an exact grading.\n--rubric selects independent criteria. --labels imports caller-supplied semantic labels.\nSuccess writes one JSON document; next contains suggested argv, never executed automatically.\ncheck, grade, label and report have a fixed allowance of 60 seconds.\nexecute checks first, then uses the declared series duration plus 15 seconds for cleanup.\nExit codes: 0 success, 64 invalid input, 124 deadline, 130 interruption, 1 operational failure.\n".split("\n").find((line) => line.startsWith("study " + command + " ")) ?? "Expected study check, execute, grade, label, or report; use study --help.";
2708
- if (![
2709
- "check",
2710
- "execute",
2711
- "grade",
2712
- "label",
2713
- "report"
2714
- ].includes(command ?? "")) throw new StudyError("CONFIG", "Unknown or missing command " + JSON.stringify(command ?? "<command>") + ". " + usage);
2715
- if (target === void 0 || target === "" || target.startsWith("-")) throw new StudyError("CONFIG", "Missing <" + (command === "check" || command === "execute" ? "study" : "result") + "> before " + JSON.stringify(target ?? "<end>") + ". " + usage);
2716
- const allowed = command === "check" ? ["--profile"] : command === "execute" ? [
2717
- "--profile",
2718
- "--out",
2719
- "--expected"
2720
- ] : command === "grade" ? ["--rubric"] : command === "label" ? ["--grading", "--labels"] : ["--grading"];
2721
- const tokens = arguments_.slice(2);
2722
- const pairs = [...tokens.entries()].flatMap(([index, flag]) => index % 2 === 0 ? [{
2723
- flag,
2724
- value: tokens[index + 1]
2725
- }] : []);
2726
- const firsts = new Map(pairs.map(({ flag }, index) => [flag, index]).reverse());
2727
- for (const [index, { flag, value }] of pairs.entries()) {
2728
- if (flag === void 0 || !allowed.includes(flag)) throw new StudyError("CONFIG", JSON.stringify(flag ?? "<end>") + " is not a " + command + " flag; accepts " + allowed.join(", ") + ". " + usage);
2729
- if (firsts.get(flag) !== index) throw new StudyError("CONFIG", flag + " is duplicate. " + usage);
2730
- if (value === void 0 || value === "" || value.startsWith("-")) throw new StudyError("CONFIG", flag + " requires a value before " + JSON.stringify(value ?? "<end>") + ". " + usage);
2731
- }
2732
- const flags = new Map(pairs.flatMap(({ flag, value }) => flag === void 0 || value === void 0 ? [] : [[flag, value]]));
2733
- if (command === "check" || command === "execute") {
2734
- if (!isStudyCustodyName(target)) throw new StudyError("CONFIG", "Invalid study token " + JSON.stringify(target) + "; use a portable study name. " + usage);
2735
- const profile = flags.get("--profile") ?? "local";
2736
- const out = flags.get("--out");
2737
- const expected = flags.get("--expected");
2738
- if (!isStudyCustodyName(profile)) throw new StudyError("CONFIG", "--profile " + JSON.stringify(profile) + " must be a portable name. " + usage);
2739
- if (out !== void 0 && !isAbsolute(out)) try {
2740
- resolveStudyPath(resolve("."), out);
2741
- } catch {
2742
- throw new StudyError("CONFIG", "--out " + JSON.stringify(out) + " must be a portable contained path and cannot escape the consumer root. " + usage);
2743
- }
2744
- if (expected !== void 0 && !isStudyDigest(expected)) throw new StudyError("CONFIG", "--expected " + JSON.stringify(expected) + " must be a SHA-256 fingerprint. " + usage);
2745
- return {
2746
- command,
2747
- target,
2748
- profile,
2749
- out,
2750
- expected
2751
- };
2752
- }
2753
- if (command === "grade") {
2754
- const rubric = flags.get("--rubric");
2755
- if (rubric === void 0) throw new StudyError("CONFIG", "Missing --rubric. " + usage);
2756
- return {
2757
- command,
2758
- target,
2759
- rubric
2760
- };
2761
- }
2762
- const grading = flags.get("--grading");
2763
- if (!isStudyDigest(grading)) throw new StudyError("CONFIG", "--grading " + JSON.stringify(grading ?? "<missing>") + " must be an exact SHA-256 grading identity. " + usage);
2764
- if (command === "label") {
2765
- const labels = flags.get("--labels");
2766
- if (labels === void 0) throw new StudyError("CONFIG", "Missing --labels. " + usage);
2767
- return {
2768
- command,
2769
- target,
2770
- grading,
2771
- labels
2772
- };
2773
- }
2774
- return {
2775
- command: "report",
2776
- target,
2777
- grading
2778
- };
2779
- }
2780
- //#endregion
2781
- //#region src/core/engines/helpers.ts
2782
- /** Converts an operational reading to its throwable boundary.
2783
- * @param failure - The retained operational reading.
2784
- * @returns An error preserving the reading's cause and responsibility.
2785
- */
2786
- function failureToStudyError(failure) {
2787
- return new StudyError(failure.cause === "capacity" ? "CAPACITY" : failure.cause === "deadline" ? "DEADLINE" : "FAULT", failure.message, failure);
2788
- }
2789
- /** Reads an operational error without inventing a task verdict.
2790
- * @remarks
2791
- * A branded `BudgetError` from any copy of the budget package reads as a `budget` stop, because the
2792
- * error alone does not prove which budget object raised it; the Agent record names a `meter` stop.
2793
- * An unbranded lookalike reads as an infrastructure failure.
2794
- * @param error - The caught error.
2795
- * @returns Existing attribution, the capacity a branded budget error names, the cause a bare
2796
- * capacity or deadline code names, or an infrastructure failure.
2797
- */
2798
- function readStudyFailure(error) {
2799
- if (isStudyError(error)) {
2800
- const { failure, code, message } = error;
2801
- if (failure !== void 0) return failure;
2802
- if (code === "CAPACITY") return {
2803
- cause: "capacity",
2804
- message,
2805
- role: void 0,
2806
- stop: void 0
2807
- };
2808
- if (code === "DEADLINE") return {
2809
- cause: "deadline",
2810
- message,
2811
- role: void 0,
2812
- stop: void 0
2813
- };
2814
- }
2815
- if (isBudgetError(error)) {
2816
- const reading = attempt(() => {
2817
- const context = error.context;
2818
- return {
2819
- id: context.id,
2820
- max: context.max,
2821
- consumed: context.consumed,
2822
- message: error.message
2823
- };
2824
- });
2825
- if (reading.success) return {
2826
- cause: "capacity",
2827
- message: reading.value.message,
2828
- role: void 0,
2829
- stop: {
2830
- layer: "budget",
2831
- id: isNonEmptyString(reading.value.id) ? reading.value.id : "Agent budget",
2832
- max: parseStudyReading(reading.value.max),
2833
- consumed: parseStudyReading(reading.value.consumed)
2834
- }
2835
- };
2836
- }
2837
- return {
2838
- cause: "infrastructure",
2839
- message: isError(error) ? error.message : "Study operation rejected without an Error",
2840
- role: void 0,
2841
- stop: void 0
2842
- };
2843
- }
2844
- /**
2845
- * Computes the stop of a request whose actual usage crossed the ceiling it was admitted under.
2846
- * @param charge - The request's admitted ceilings
2847
- * @param usage - The request's measured usage
2848
- * @returns The `scope` stop for the first quantity, tokens then bytes, whose known usage exceeds its admitted ceiling; undefined when none does
2849
- * @example
2850
- * ```ts
2851
- * computeStudyAdmittedStop({ role: 'agent', tokens: 10, bytes: 100 }, usage) // { layer: 'scope', id: 'Admitted tokens', max: 10, consumed: 12 }
2852
- * ```
2853
- */
2854
- function computeStudyAdmittedStop(charge, usage) {
2855
- if (usage.tokens !== void 0 && usage.tokens > charge.tokens) return {
2856
- layer: "scope",
2857
- id: "Admitted tokens",
2858
- max: charge.tokens,
2859
- consumed: usage.tokens
2860
- };
2861
- if (usage.bytes !== void 0 && usage.bytes > charge.bytes) return {
2862
- layer: "scope",
2863
- id: "Admitted bytes",
2864
- max: charge.bytes,
2865
- consumed: usage.bytes
2866
- };
2867
- }
2868
- /**
2869
- * Computes the stop of a product the output allowances cannot hold.
2870
- * @param bytes - The encoded size of the product's value
2871
- * @param limit - The goal's byte allowance
2872
- * @param outputs - The trial and series output budgets, in the order they refuse
2873
- * @returns The `Goal bytes` stop when the goal allowance refuses, else the stop of the first budget whose remaining room is below the product, with its tally including the refused product; undefined when every allowance holds it
2874
- * @example
2875
- * ```ts
2876
- * computeStudyOutputStop(2048, 1024, []) // { layer: 'scope', id: 'Goal bytes', max: 1024, consumed: 2048 }
2877
- * ```
2878
- */
2879
- function computeStudyOutputStop(bytes, limit, outputs) {
2880
- if (bytes > limit) return {
2881
- layer: "scope",
2882
- id: "Goal bytes",
2883
- max: limit,
2884
- consumed: bytes
2885
- };
2886
- const refusal = outputs.find((budget) => bytes > budget.remaining);
2887
- return refusal === void 0 ? void 0 : {
2888
- layer: "scope",
2889
- id: refusal.id,
2890
- max: refusal.max,
2891
- consumed: refusal.consumed + bytes
2892
- };
2893
- }
2894
- /**
2895
- * Records an error that escaped admitted work as the failure of its ticket.
2896
- * @param ticket - The admitted request whose work escaped
2897
- * @param error - The caught value, read through `readStudyFailure`
2898
- * @param role - The responsibility that replaces any role the reading carries; Default: the carried role, else the ticket's admitted role
2899
- * @remarks
2900
- * A ticket that already settled or whose signal aborted is left untouched, because its owner
2901
- * already holds the failure. A remote ticket's aborted signal does not mean it settled locally,
2902
- * so the parent's ownership is respected rather than inferred as local settlement. A refusal
2903
- * thrown by the ticket's own settlement propagates.
2904
- * @throws Thrown when the ticket's settlement refuses the failure.
2905
- * @example Fail an escaped request
2906
- * ```ts
2907
- * import type { StudyTicket } from '@orkestrel/study'
2908
- * import { failStudyTicket } from '@orkestrel/study'
2909
- *
2910
- * declare const ticket: StudyTicket
2911
- *
2912
- * failStudyTicket(ticket, new Error('Provider refused'), 'tool')
2913
- * ```
2914
- */
2915
- function failStudyTicket(ticket, error, role) {
2916
- if (ticket.settled || ticket.signal.aborted) return;
2917
- const failure = readStudyFailure(error);
2918
- ticket.fail(role === void 0 ? failure : {
2919
- ...failure,
2920
- role
2921
- });
2922
- }
2923
- //#endregion
2924
- //#region src/core/engines/StudyControl.ts
2925
- /** Retains the first operational failure while bounding asynchronous waits. */
2926
- var StudyControl = class {
2927
- #abort;
2928
- #listeners = new Abort();
2929
- #failure;
2930
- /** Links cancellation to the caller without assigning it a task verdict.
2931
- * @param signal - Optional parent cancellation.
2932
- */
2933
- constructor(signal) {
2934
- this.#abort = new Abort(signal === void 0 ? void 0 : { signal });
2935
- if (this.#abort.aborted) this.#interrupt();
2936
- else this.#abort.signal.addEventListener("abort", () => this.#interrupt(), {
2937
- once: true,
2938
- signal: this.#listeners.signal
2939
- });
2940
- }
2941
- get signal() {
2942
- return this.#abort.signal;
2943
- }
2944
- get failure() {
2945
- return this.#failure;
2946
- }
2947
- /** Latches an operational failure and cancels all linked work.
2948
- * @param failure - The first attribution to preserve.
2949
- */
2950
- fail(failure) {
2951
- if (this.#failure !== void 0) return;
2952
- this.#failure = Object.freeze({
2953
- ...failure,
2954
- stop: failure.stop === void 0 ? void 0 : Object.freeze({ ...failure.stop })
2955
- });
2956
- this.#abort.abort(failureToStudyError(this.#failure));
2957
- }
2958
- /** Races work against cancellation and removes its listener after either ending.
2959
- * @param work - Already-started work whose rejection remains observed after cancellation.
2960
- * @returns The work's result if it settles before cancellation.
2961
- * @throws Thrown with the retained operational attribution when cancelled.
2962
- */
2963
- wait(work) {
2964
- const listeners = new Abort();
2965
- return new Promise((resolve, reject) => {
2966
- Promise.resolve(work).then(resolve, reject);
2967
- if (this.#abort.aborted) reject(this.#reason());
2968
- else this.#abort.signal.addEventListener("abort", () => reject(this.#reason()), {
2969
- once: true,
2970
- signal: listeners.signal
2971
- });
2972
- }).finally(() => listeners.abort());
2973
- }
2974
- /** Removes the lifecycle listener without changing the retained failure. */
2975
- close() {
2976
- this.#listeners.abort();
2977
- }
2978
- #reason() {
2979
- return failureToStudyError(this.#failure ?? {
2980
- cause: "interrupted",
2981
- message: "Study interrupted",
2982
- role: void 0,
2983
- stop: void 0
2984
- });
2985
- }
2986
- #interrupt() {
2987
- if (this.#failure === void 0) this.fail({
2988
- cause: "interrupted",
2989
- message: "Study interrupted by parent cancellation",
2990
- role: void 0,
2991
- stop: void 0
2992
- });
2993
- }
2994
- };
2995
- //#endregion
2996
- //#region src/core/engines/StudyScope.ts
2997
- /** Reserves finite request ceilings around cumulative actual budgets and one deadline. */
2998
- var StudyScope = class {
2999
- #options;
3000
- #calls;
3001
- #tokens;
3002
- #bytes;
3003
- #timeout;
3004
- #listeners = new Abort();
3005
- #pending = /* @__PURE__ */ new Map();
3006
- #started = performance.now();
3007
- #closed = false;
3008
- /** Starts a timed scope with independent counters.
3009
- * @param options - Finite limit, shared failure owner, and diagnostic label.
3010
- */
3011
- constructor(options) {
3012
- if (!isStudyLimit(options.limit)) throw new StudyError("CONFIG", "Invalid study scope limit");
3013
- this.#options = {
3014
- ...options,
3015
- limit: { ...options.limit }
3016
- };
3017
- this.#calls = new Budget({
3018
- max: options.limit.calls,
3019
- consumer: (value) => value
3020
- });
3021
- this.#tokens = new Budget({
3022
- max: options.limit.tokens,
3023
- consumer: (value) => value
3024
- });
3025
- this.#bytes = new Budget({
3026
- max: options.limit.bytes,
3027
- consumer: (value) => value
3028
- });
3029
- this.#timeout = new Timeout({ ms: options.limit.duration });
3030
- this.#timeout.signal.addEventListener("abort", () => this.#expire(), {
3031
- once: true,
3032
- signal: this.#listeners.signal
3033
- });
3034
- this.#timeout.start();
3035
- }
3036
- /** Checks cumulative consumption and every pending ceiling before admission.
3037
- * @param charge - One prospective request's ceilings.
3038
- * @throws Thrown when this scope cannot admit the request.
3039
- */
3040
- check(charge) {
3041
- this.#checkTime();
3042
- if (this.#closed) throw new StudyError("FAULT", "Study scope is closed");
3043
- const prior = this.#options.control.failure;
3044
- if (prior !== void 0) throw failureToStudyError(prior);
3045
- if (charge === void 0) return;
3046
- const pending = [...this.#pending.values()];
3047
- const tokens = pending.reduce((total, entry) => total + entry.tokens, 0);
3048
- const bytes = pending.reduce((total, entry) => total + entry.bytes, 0);
3049
- const { label, limit } = this.#options;
3050
- const refusal = [
3051
- {
3052
- id: `${label} calls`,
3053
- refused: this.#calls.remaining - this.#pending.size < 1,
3054
- max: limit.calls,
3055
- consumed: this.#calls.consumed + this.#pending.size + 1
3056
- },
3057
- {
3058
- id: `${label} tokens`,
3059
- refused: this.#tokens.remaining - tokens < charge.tokens,
3060
- max: limit.tokens,
3061
- consumed: this.#tokens.consumed + tokens + charge.tokens
3062
- },
3063
- {
3064
- id: `${label} bytes`,
3065
- refused: this.#bytes.remaining - bytes < charge.bytes,
3066
- max: limit.bytes,
3067
- consumed: this.#bytes.consumed + bytes + charge.bytes
3068
- }
3069
- ].find((entry) => entry.refused);
3070
- if (refusal !== void 0) {
3071
- const failure = {
3072
- cause: "capacity",
3073
- message: `${label} allowance cannot admit the request`,
3074
- role: charge.role,
3075
- stop: {
3076
- layer: "scope",
3077
- id: refusal.id,
3078
- max: refusal.max,
3079
- consumed: refusal.consumed
3080
- }
3081
- };
3082
- this.#options.control.fail(failure);
3083
- throw failureToStudyError(failure);
3084
- }
3085
- }
3086
- /** Reserves one request after the enclosing meter checked all scopes.
3087
- * @param id - The request's unique reservation identity.
3088
- * @param charge - Its admitted ceilings.
3089
- */
3090
- reserve(id, charge) {
3091
- if (this.#pending.has(id)) throw new StudyError("FAULT", "Duplicate study reservation");
3092
- this.#pending.set(id, { ...charge });
3093
- }
3094
- /** Commits known charges while retaining ceilings for missing measurements.
3095
- * @param id - The pending reservation identity.
3096
- * @param usage - The request's retained public measurements.
3097
- */
3098
- settle(id, usage) {
3099
- const charge = this.#pending.get(id);
3100
- if (charge === void 0) throw new StudyError("FAULT", "Study reservation is not pending");
3101
- this.#pending.delete(id);
3102
- this.#calls.consume(1);
3103
- this.#tokens.consume(Math.min(charge.tokens, usage.tokens ?? charge.tokens));
3104
- this.#bytes.consume(Math.min(charge.bytes, usage.bytes ?? charge.bytes));
3105
- }
3106
- /** Checks deadline attribution before clearing the timer. */
3107
- close() {
3108
- if (this.#closed) return;
3109
- this.#checkTime();
3110
- this.#closed = true;
3111
- this.#listeners.abort();
3112
- this.#timeout.clear();
3113
- }
3114
- #checkTime() {
3115
- if (!this.#closed && (this.#timeout.expired || performance.now() - this.#started >= this.#options.limit.duration)) this.#expire();
3116
- }
3117
- #expire() {
3118
- this.#options.control.fail({
3119
- cause: "deadline",
3120
- message: `${this.#options.label} deadline expired`,
3121
- role: void 0,
3122
- stop: {
3123
- layer: "scope",
3124
- id: `${this.#options.label} duration`,
3125
- max: this.#options.limit.duration,
3126
- consumed: Math.ceil(performance.now() - this.#started)
3127
- }
3128
- });
3129
- }
3130
- };
3131
- //#endregion
3132
- //#region src/core/engines/StudyDeadline.ts
3133
- /** Bounds each independent operation with a fresh failure owner and deadline. */
3134
- var StudyDeadline = class {
3135
- #options;
3136
- /** Retains the deadline without starting a timer.
3137
- * @param options - Finite duration in milliseconds and diagnostic label.
3138
- */
3139
- constructor(options) {
3140
- this.#options = { ...options };
3141
- }
3142
- /** Waits for work under a fresh failure owner and releases its timer and listener.
3143
- * @param work - Already-started work whose late rejection remains observed.
3144
- * @returns The work's result when it settles before the deadline.
3145
- * @throws Thrown for an invalid duration, the work's own rejection, or the labelled deadline.
3146
- */
3147
- async wait(work) {
3148
- const control = new StudyControl();
3149
- const scope = this.#open(control, work);
3150
- try {
3151
- const result = await control.wait(work);
3152
- scope.check();
3153
- return result;
3154
- } finally {
3155
- scope.close();
3156
- control.close();
3157
- }
3158
- }
3159
- #open(control, work) {
3160
- try {
3161
- return new StudyScope({
3162
- limit: {
3163
- duration: this.#options.duration,
3164
- calls: 1,
3165
- tokens: 1,
3166
- bytes: 1
3167
- },
3168
- control,
3169
- label: this.#options.label
3170
- });
3171
- } catch (error) {
3172
- Promise.resolve(work).catch(() => void 0);
3173
- control.close();
3174
- throw error;
3175
- }
3176
- }
3177
- };
3178
- //#endregion
3179
- //#region src/core/engines/StudyRequest.ts
3180
- /** Settles an admitted request once and propagates operational failure to its owner. */
3181
- var StudyRequest = class {
3182
- #options;
3183
- #timeout;
3184
- #signal;
3185
- #listeners = new Abort();
3186
- #started = performance.now();
3187
- #settled = false;
3188
- /** Starts the request deadline after its enclosing scopes reserve capacity.
3189
- * @param options - Admitted ceilings, request deadline, and settlement callback.
3190
- */
3191
- constructor(options) {
3192
- this.#options = {
3193
- ...options,
3194
- charge: { ...options.charge }
3195
- };
3196
- this.#timeout = new Timeout({ ms: options.duration });
3197
- this.#signal = linkSignal(this.#timeout.signal, options.control.signal);
3198
- this.#signal.addEventListener("abort", () => this.#abort(), {
3199
- once: true,
3200
- signal: this.#listeners.signal
3201
- });
3202
- this.#timeout.start();
3203
- if (this.#signal.aborted) this.#abort();
3204
- }
3205
- get signal() {
3206
- return this.#signal;
3207
- }
3208
- get settled() {
3209
- return this.#settled;
3210
- }
3211
- /** Retains actual measurements and refuses invalid or excessive charges.
3212
- * @param usage - Measurements attributed to exactly this request.
3213
- * @throws Thrown when settlement is late, repeated, invalid, or beyond admission.
3214
- */
3215
- finish(usage) {
3216
- this.#checkPending();
3217
- if (!isStudyUsage(usage) || usage.role !== this.#options.charge.role || usage.calls !== 1) {
3218
- const failure = {
3219
- cause: "infrastructure",
3220
- message: "Request usage is invalid or misattributed",
3221
- role: this.#options.charge.role,
3222
- stop: void 0
3223
- };
3224
- this.fail(failure);
3225
- throw failureToStudyError(failure);
3226
- }
3227
- const elapsed = performance.now() - this.#started;
3228
- const admitted = computeStudyAdmittedStop(this.#options.charge, usage);
3229
- const failure = this.#timeout.expired || elapsed >= this.#options.duration || usage.duration > this.#options.duration ? {
3230
- cause: "deadline",
3231
- message: "Request deadline expired",
3232
- role: usage.role,
3233
- stop: {
3234
- layer: "scope",
3235
- id: "Request duration",
3236
- max: this.#options.duration,
3237
- consumed: Math.ceil(Math.max(elapsed, usage.duration))
3238
- }
3239
- } : admitted !== void 0 ? {
3240
- cause: "capacity",
3241
- message: "Request usage exceeded its admitted ceiling",
3242
- role: usage.role,
3243
- stop: admitted
3244
- } : void 0;
3245
- this.#complete(usage);
3246
- if (failure !== void 0) {
3247
- this.#options.control.fail(failure);
3248
- throw failureToStudyError(failure);
3249
- }
3250
- }
3251
- /** Retains unknown cost conservatively and latches the first operational failure.
3252
- * @param failure - The operational request failure.
3253
- * @throws Thrown when this request was already settled.
3254
- */
3255
- fail(failure) {
3256
- this.#checkPending();
3257
- this.#complete({
3258
- role: this.#options.charge.role,
3259
- calls: 1,
3260
- tokens: void 0,
3261
- bytes: void 0,
3262
- duration: Math.ceil(performance.now() - this.#started)
3263
- });
3264
- this.#options.control.fail({
3265
- ...failure,
3266
- role: failure.role ?? this.#options.charge.role
3267
- });
3268
- }
3269
- #complete(usage) {
3270
- this.#settled = true;
3271
- this.#listeners.abort();
3272
- this.#timeout.clear();
3273
- this.#options.settle(Object.freeze({ ...usage }));
3274
- }
3275
- #checkPending() {
3276
- if (this.#settled) {
3277
- this.#options.control.fail({
3278
- cause: "infrastructure",
3279
- message: "Request settlement is late or repeated",
3280
- role: this.#options.charge.role,
3281
- stop: void 0
3282
- });
3283
- throw new StudyError("FAULT", "Request settlement is late or repeated", this.#options.control.failure);
3284
- }
3285
- }
3286
- #abort() {
3287
- if (this.#settled) return;
3288
- const expired = this.#timeout.expired;
3289
- this.fail(this.#options.control.failure ?? {
3290
- cause: expired ? "deadline" : "interrupted",
3291
- message: expired ? "Request deadline expired" : "Request interrupted",
3292
- role: this.#options.charge.role,
3293
- stop: expired ? {
3294
- layer: "scope",
3295
- id: "Request duration",
3296
- max: this.#options.duration,
3297
- consumed: Math.ceil(performance.now() - this.#started)
3298
- } : void 0
3299
- });
3300
- }
3301
- };
3302
- //#endregion
3303
- //#region src/core/engines/StudyMeter.ts
3304
- /** Admits request ceilings across every enclosing scope before dispatch. */
3305
- var StudyMeter = class {
3306
- #options;
3307
- #pending = /* @__PURE__ */ new Set();
3308
- #usage = [];
3309
- #closed = false;
3310
- /** Holds the goal's shared admission scopes.
3311
- * @param options - Request bound, enclosing scopes, and shared failure owner.
3312
- */
3313
- constructor(options) {
3314
- if (!isStudyLimit(options.request) || options.request.calls !== 1 || options.scopes.length === 0) throw new StudyError("CONFIG", "Invalid request admission bounds");
3315
- this.#options = {
3316
- ...options,
3317
- request: { ...options.request },
3318
- scopes: [...options.scopes]
3319
- };
3320
- }
3321
- get signal() {
3322
- return this.#options.control.signal;
3323
- }
3324
- get usage() {
3325
- return Object.freeze([...this.#usage]);
3326
- }
3327
- /** Reserves one request atomically across all enclosing allowances.
3328
- * @param charge - Safe integer token and byte ceilings with the request's role.
3329
- * @returns A request ticket that must settle before the goal completes.
3330
- * @throws Thrown before dispatch if the request exceeds available capacity.
3331
- */
3332
- admit(charge) {
3333
- if (this.#closed) throw new StudyError("FAULT", "Study meter is closed");
3334
- if (this.#options.control.failure !== void 0) throw failureToStudyError(this.#options.control.failure);
3335
- if (!isStudyRole(charge.role) || !Number.isSafeInteger(charge.tokens) || charge.tokens < 0 || !Number.isSafeInteger(charge.bytes) || charge.bytes < 0) {
3336
- const failure = {
3337
- cause: "infrastructure",
3338
- message: "Request admission requires safe nonnegative ceilings",
3339
- role: void 0,
3340
- stop: void 0
3341
- };
3342
- this.#options.control.fail(failure);
3343
- throw failureToStudyError(failure);
3344
- }
3345
- const { request } = this.#options;
3346
- if (charge.tokens > request.tokens || charge.bytes > request.bytes) {
3347
- const tokens = charge.tokens > request.tokens;
3348
- const failure = {
3349
- cause: "capacity",
3350
- message: "Request ceiling exceeds the request allowance",
3351
- role: charge.role,
3352
- stop: {
3353
- layer: "scope",
3354
- id: tokens ? "Request tokens" : "Request bytes",
3355
- max: tokens ? request.tokens : request.bytes,
3356
- consumed: tokens ? charge.tokens : charge.bytes
3357
- }
3358
- };
3359
- this.#options.control.fail(failure);
3360
- throw failureToStudyError(failure);
3361
- }
3362
- for (const scope of this.#options.scopes) scope.check(charge);
3363
- const id = Symbol("request");
3364
- for (const scope of this.#options.scopes) scope.reserve(id, charge);
3365
- this.#pending.add(id);
3366
- return new StudyRequest({
3367
- charge,
3368
- duration: this.#options.request.duration,
3369
- control: this.#options.control,
3370
- settle: (usage) => {
3371
- for (const scope of this.#options.scopes) scope.settle(id, usage);
3372
- this.#pending.delete(id);
3373
- this.#usage = [...this.#usage, usage];
3374
- }
3375
- });
3376
- }
3377
- /** Refuses unfinished requests before closing a goal's admission boundary. */
3378
- close() {
3379
- if (this.#closed) return;
3380
- this.#closed = true;
3381
- if (this.#pending.size > 0) this.#options.control.fail({
3382
- cause: "infrastructure",
3383
- message: "Goal completed with unsettled requests",
3384
- role: void 0,
3385
- stop: void 0
3386
- });
3387
- }
3388
- };
3389
- //#endregion
3390
- //#region src/core/engines/StudyTrialLease.ts
3391
- /** Holds one trial's evidence custody, admission scope and driver session from opening to cleanup. */
3392
- var StudyTrialLease = class {
3393
- #trial;
3394
- #options;
3395
- #control;
3396
- #series;
3397
- #deadline;
3398
- #scope;
3399
- #evidence;
3400
- #opening;
3401
- #session;
3402
- /** Binds one scheduled trial to the series' owned inputs, failure owner, series scope and cleanup deadline.
3403
- * @param trial - The scheduled trial.
3404
- * @param options - The series' owned manifest, driver and store.
3405
- * @param control - The series failure owner.
3406
- * @param series - The series admission scope.
3407
- * @param deadline - The custody and cleanup deadline.
3408
- */
3409
- constructor(trial, options, control, series, deadline) {
3410
- this.#trial = trial;
3411
- this.#options = options;
3412
- this.#control = control;
3413
- this.#series = series;
3414
- this.#deadline = deadline;
3415
- }
3416
- /** Reports the trial scope once opened; undefined when the trial inherited a failure. */
3417
- get scope() {
3418
- return this.#scope;
3419
- }
3420
- /** Reports the trial's evidence sink once custody started. */
3421
- get evidence() {
3422
- return this.#evidence;
3423
- }
3424
- /** Reports the driver session once it opened. */
3425
- get session() {
3426
- return this.#session;
3427
- }
3428
- /** Starts evidence custody and, unless a failure is latched, opens the trial scope and driver session.
3429
- * @returns Nothing after the session opened, or after custody alone started under a latched failure.
3430
- * @throws StudyError - Thrown with code `CUSTODY` when custody refuses the trial; thrown for a series or trial limit or an opening failure.
3431
- */
3432
- async open() {
3433
- const control = this.#control;
3434
- if (control.failure !== void 0) {
3435
- this.#evidence = await this.#start();
3436
- return;
3437
- }
3438
- this.#series.check();
3439
- const scope = new StudyScope({
3440
- limit: this.#options.manifest.bounds.trial,
3441
- control,
3442
- label: "Trial"
3443
- });
3444
- this.#scope = scope;
3445
- this.#evidence = await this.#start();
3446
- this.#series.check();
3447
- scope.check();
3448
- const opening = this.#launch();
3449
- this.#opening = opening;
3450
- this.#session = await control.wait(opening);
3451
- }
3452
- /** Closes the opened session, or a session that opened late, then the trial scope.
3453
- * @returns The cleanup failure, or undefined when cleanup succeeded or repeated the latched failure.
3454
- */
3455
- async close() {
3456
- const cleanup = await this.#release();
3457
- this.#scope?.close();
3458
- return cleanup;
3459
- }
3460
- async #release() {
3461
- const session = this.#session;
3462
- const opening = this.#opening;
3463
- if (session !== void 0) try {
3464
- await this.#deadline.wait(session.close());
3465
- return;
3466
- } catch (error) {
3467
- const cleanup = readStudyFailure(error);
3468
- this.#control.fail(cleanup);
3469
- return cleanup;
3470
- }
3471
- if (opening === void 0) return void 0;
3472
- try {
3473
- const late = await this.#deadline.wait(opening);
3474
- await this.#deadline.wait(late.close());
3475
- return;
3476
- } catch (error) {
3477
- const late = readStudyFailure(error);
3478
- return late.message !== this.#control.failure?.message ? late : void 0;
3479
- }
3480
- }
3481
- async #launch() {
3482
- const control = this.#control;
3483
- const session = await this.#options.driver.open(this.#trial, control.signal);
3484
- if (control.failure !== void 0) {
3485
- await this.#deadline.wait(session.close());
3486
- throw failureToStudyError(control.failure);
3487
- }
3488
- return session;
3489
- }
3490
- async #start() {
3491
- const control = this.#control;
3492
- try {
3493
- const pending = this.#options.store.start(this.#trial);
3494
- return control.failure === void 0 ? await control.wait(pending) : await this.#deadline.wait(pending);
3495
- } catch (error) {
3496
- control.fail(readStudyFailure(error));
3497
- const failure = control.failure ?? readStudyFailure(error);
3498
- throw new StudyError("CUSTODY", failure.message, failure);
3499
- }
3500
- }
3501
- };
3502
- //#endregion
3503
- //#region src/core/engines/StudySeries.ts
3504
- /** Executes a frozen serial study schedule with finite admission and sticky failures. */
3505
- var StudySeries = class {
3506
- #options;
3507
- #executed = false;
3508
- /** Owns the declared inputs while retaining the concrete driver and custody store.
3509
- * @param options - Frozen manifest, concrete adapters, and parent cancellation.
3510
- */
3511
- constructor(options) {
3512
- this.#options = {
3513
- ...options,
3514
- manifest: structuredClone(options.manifest)
3515
- };
3516
- }
3517
- /** Runs every declared trial once and retains explicit unexecuted observations after failure.
3518
- * Custody rejection leaves the run unsealed; callers must seal only a fulfilled execution.
3519
- * @returns Ordered terminal attempts without task success labels.
3520
- * @throws Thrown for invalid configuration or failed or timed out custody operations.
3521
- */
3522
- async execute() {
3523
- if (this.#executed) throw new StudyError("CONFIG", "Study series can execute only once");
3524
- this.#executed = true;
3525
- const manifest = this.#options.manifest;
3526
- const expected = buildStudySchedule(manifest.definition, manifest.scenarios);
3527
- if (!isStudyBounds(manifest.bounds) || canonicalStringify(expected) !== canonicalStringify(manifest.schedule)) throw new StudyError("CONFIG", "Study manifest schedule or bounds are invalid");
3528
- const control = new StudyControl(this.#options.signal);
3529
- const scope = new StudyScope({
3530
- limit: manifest.bounds.series,
3531
- control,
3532
- label: "Series"
3533
- });
3534
- const deadline = new StudyDeadline({
3535
- duration: manifest.bounds.request.duration,
3536
- label: "Custody or cleanup"
3537
- });
3538
- const output = new Budget({
3539
- id: "Series output",
3540
- max: manifest.bounds.series.bytes,
3541
- consumer: (bytes) => bytes
3542
- });
3543
- const attempts = [];
3544
- try {
3545
- for (const trial of manifest.schedule) attempts.push(await this.#executeTrial(trial, control, scope, output, deadline));
3546
- } finally {
3547
- scope.close();
3548
- control.close();
3549
- }
3550
- return Object.freeze(attempts);
3551
- }
3552
- async #executeTrial(trial, control, series, output, deadline) {
3553
- const started = performance.now();
3554
- const scenario = this.#options.manifest.scenarios.find((entry) => entry.id === trial.scenario);
3555
- if (scenario === void 0) throw new StudyError("CONFIG", "Scheduled scenario is absent");
3556
- const prior = control.failure;
3557
- const lease = new StudyTrialLease(trial, this.#options, control, series, deadline);
3558
- const executed = await this.#executeGoals(scenario, lease, control, series, output);
3559
- const cleanup = await this.#release(lease, control, series);
3560
- const failure = control.failure;
3561
- const observations = [...executed, ...scenario.goals.slice(executed.length).map((goal) => {
3562
- const unexecuted = failure === void 0 ? {
3563
- cause: "infrastructure",
3564
- message: "Goal unexecuted because the trial did not open",
3565
- role: void 0,
3566
- stop: void 0
3567
- } : {
3568
- ...failure,
3569
- message: `Goal unexecuted: ${failure.message}`
3570
- };
3571
- return Object.freeze({
3572
- goal: goal.id,
3573
- product: void 0,
3574
- failure: unexecuted,
3575
- usage: Object.freeze([]),
3576
- duration: 0,
3577
- executed: false
3578
- });
3579
- })];
3580
- const attempt = Object.freeze({
3581
- trial,
3582
- observations: Object.freeze(observations),
3583
- failure: cleanup !== void 0 && failure !== void 0 && (prior !== void 0 || failure.message !== cleanup.message) ? {
3584
- ...failure,
3585
- message: `${failure.message}; cleanup: ${cleanup.message}`
3586
- } : failure,
3587
- duration: Math.ceil(performance.now() - started),
3588
- executed: prior === void 0
3589
- });
3590
- try {
3591
- await deadline.wait(this.#options.store.finish(attempt));
3592
- if (attempt.failure === void 0) series.check();
3593
- } catch (error) {
3594
- control.fail(readStudyFailure(error));
3595
- const custody = control.failure ?? readStudyFailure(error);
3596
- throw new StudyError("CUSTODY", custody.message, custody);
3597
- }
3598
- return attempt;
3599
- }
3600
- async #executeGoals(scenario, lease, control, series, output) {
3601
- const observations = [];
3602
- try {
3603
- await lease.open();
3604
- const trialOutput = new Budget({
3605
- id: "Trial output",
3606
- max: this.#options.manifest.bounds.trial.bytes,
3607
- consumer: (bytes) => bytes
3608
- });
3609
- for (const goal of scenario.goals) {
3610
- const { session, scope, evidence } = lease;
3611
- if (control.failure !== void 0 || session === void 0 || scope === void 0 || evidence === void 0) break;
3612
- series.check();
3613
- scope.check();
3614
- observations.push(await this.#executeGoal(goal, session, evidence, control, [scope, series], [trialOutput, output]));
3615
- }
3616
- } catch (error) {
3617
- if (isStudyError(error) && error.code === "CUSTODY") {
3618
- await this.#release(lease, control, series);
3619
- throw error;
1912
+ result: result.manifest.id,
1913
+ seal: result.seal.id,
1914
+ identity: {
1915
+ agent: identity.agent,
1916
+ subject: identity.subject,
1917
+ instrument: identity.instrument,
1918
+ fixtures: identity.fixtures,
1919
+ profile: identity.profile,
1920
+ ...identity.provider === void 0 ? {} : { provider: identity.provider }
1921
+ },
1922
+ purpose: result.manifest.definition.purpose,
1923
+ population: "Complete declared trial and criterion horizon",
1924
+ uncertainty: repetitions.length === 1 ? "One repetition is descriptive and does not establish replicated capacity or quality." : "Repetitions are retained separately; intervals represent unresolved grading, not statistical confidence.",
1925
+ scores: scores.map((score) => ({ ...score })),
1926
+ repetitions,
1927
+ operations: {
1928
+ trials: {
1929
+ scheduled: result.manifest.schedule.length,
1930
+ terminal: result.attempts.length,
1931
+ ...computeStudyPartition(result.attempts),
1932
+ completed: result.attempts.filter((attempt) => attempt.failure === void 0 && attempt.observations.every((observation) => completed.has(observation))).length,
1933
+ faults: STUDY_CAUSES.map((cause) => ({
1934
+ cause,
1935
+ ...computeStudyPartition(result.attempts.filter((attempt) => attempt.failure?.cause === cause))
1936
+ })),
1937
+ attributions: computeStudyAttributions(result.attempts.flatMap((attempt) => attempt.failure === void 0 ? [] : [{
1938
+ trial: attempt.trial.id,
1939
+ goal: void 0,
1940
+ failure: attempt.failure,
1941
+ executed: attempt.executed
1942
+ }]))
1943
+ },
1944
+ goals: {
1945
+ scheduled: observations.length,
1946
+ ...computeStudyPartition(observations),
1947
+ products: observations.filter((observation) => observation.product !== void 0).length,
1948
+ partial: observations.filter((observation) => observation.product?.partial === true).length,
1949
+ completed: observations.filter((observation) => completed.has(observation)).length,
1950
+ faults: STUDY_CAUSES.map((cause) => ({
1951
+ cause,
1952
+ ...computeStudyPartition(observations.filter((observation) => observation.failure?.cause === cause))
1953
+ })),
1954
+ attributions: computeStudyAttributions(result.attempts.flatMap((attempt) => attempt.observations.flatMap((observation) => observation.failure === void 0 ? [] : [{
1955
+ trial: attempt.trial.id,
1956
+ goal: observation.goal,
1957
+ failure: observation.failure,
1958
+ executed: observation.executed
1959
+ }])))
3620
1960
  }
3621
- control.fail(readStudyFailure(error));
1961
+ },
1962
+ costs: {
1963
+ duration: result.attempts.reduce((total, attempt) => total + attempt.duration, 0),
1964
+ roles: STUDY_ROLES.map((role) => {
1965
+ const charges = usage.filter((entry) => entry.role === role);
1966
+ return {
1967
+ role,
1968
+ measurements: charges.length,
1969
+ calls: charges.reduce((total, entry) => total + entry.calls, 0),
1970
+ duration: charges.reduce((total, entry) => total + entry.duration, 0),
1971
+ tokens: {
1972
+ known: charges.reduce((total, entry) => total + (entry.tokens ?? 0), 0),
1973
+ unknown: charges.filter((entry) => entry.tokens === void 0).length
1974
+ },
1975
+ bytes: {
1976
+ known: charges.reduce((total, entry) => total + (entry.bytes ?? 0), 0),
1977
+ unknown: charges.filter((entry) => entry.bytes === void 0).length
1978
+ }
1979
+ };
1980
+ })
3622
1981
  }
3623
- return observations;
1982
+ };
1983
+ }
1984
+ //#endregion
1985
+ //#region src/server/commands/parsers.ts
1986
+ /** Parses public command tokens with operation-specific grammar diagnostics.
1987
+ * @param arguments_ - Exact command tokens, after the CLI help boundary.
1988
+ * @returns The independent operation request.
1989
+ * @throws StudyError when a token, required flag, or portable path is invalid.
1990
+ * @example Parse a study command
1991
+ * ```ts
1992
+ * import { parseStudyArguments } from '@orkestrel/study/server'
1993
+ *
1994
+ * parseStudyArguments(['check', 'files', '--profile', 'local'])
1995
+ * ```
1996
+ */
1997
+ function parseStudyArguments(arguments_) {
1998
+ const command = arguments_[0];
1999
+ const target = arguments_[1];
2000
+ const usage = "study check <study> [--profile <name>]\nstudy execute <study> [--profile <name>] [--out <path>] [--expected <sha256>]\nstudy grade <result> --rubric <path>\nstudy label <result> --grading <sha256> --labels <path>\nstudy report <result> --grading <sha256>\nstudy --help\nstudy --version\n\nProfiles default to local. Flags require values and may appear only once.\nResults default to tmp/studies/<result>; scratch also uses tmp/studies.\nA result is its id under tmp/studies, a root-relative path, or an absolute path.\nRelative paths, including --out, must stay inside the consumer working directory.\nOutput directories must be new; existing results are never overwritten.\n--expected pins the fingerprint returned by check. --grading selects an exact grading.\n--rubric selects independent criteria. --labels imports caller-supplied semantic labels.\nSuccess writes one JSON document; next contains suggested argv, never executed automatically.\ncheck, grade, label and report have a fixed allowance of 60 seconds.\nexecute checks first, then uses the declared series duration plus 15 seconds for cleanup.\nExit codes: 0 success, 64 invalid input, 124 deadline, 130 interruption, 1 operational failure.\n".split("\n").find((line) => line.startsWith("study " + command + " ")) ?? "Expected study check, execute, grade, label, or report; use study --help.";
2001
+ if (![
2002
+ "check",
2003
+ "execute",
2004
+ "grade",
2005
+ "label",
2006
+ "report"
2007
+ ].includes(command ?? "")) throw new StudyError("CONFIG", "Unknown or missing command " + JSON.stringify(command ?? "<command>") + ". " + usage);
2008
+ if (target === void 0 || target === "" || target.startsWith("-")) throw new StudyError("CONFIG", "Missing <" + (command === "check" || command === "execute" ? "study" : "result") + "> before " + JSON.stringify(target ?? "<end>") + ". " + usage);
2009
+ const allowed = command === "check" ? ["--profile"] : command === "execute" ? [
2010
+ "--profile",
2011
+ "--out",
2012
+ "--expected"
2013
+ ] : command === "grade" ? ["--rubric"] : command === "label" ? ["--grading", "--labels"] : ["--grading"];
2014
+ const tokens = arguments_.slice(2);
2015
+ const pairs = [...tokens.entries()].flatMap(([index, flag]) => index % 2 === 0 ? [{
2016
+ flag,
2017
+ value: tokens[index + 1]
2018
+ }] : []);
2019
+ const firsts = new Map(pairs.map(({ flag }, index) => [flag, index]).reverse());
2020
+ for (const [index, { flag, value }] of pairs.entries()) {
2021
+ if (flag === void 0 || !allowed.includes(flag)) throw new StudyError("CONFIG", JSON.stringify(flag ?? "<end>") + " is not a " + command + " flag; accepts " + allowed.join(", ") + ". " + usage);
2022
+ if (firsts.get(flag) !== index) throw new StudyError("CONFIG", flag + " is duplicate. " + usage);
2023
+ if (value === void 0 || value === "" || value.startsWith("-")) throw new StudyError("CONFIG", flag + " requires a value before " + JSON.stringify(value ?? "<end>") + ". " + usage);
3624
2024
  }
3625
- async #release(lease, control, series) {
3626
- const cleanup = await lease.close();
3627
- try {
3628
- series.check();
3629
- } catch (error) {
3630
- control.fail(readStudyFailure(error));
2025
+ const flags = new Map(pairs.flatMap(({ flag, value }) => flag === void 0 || value === void 0 ? [] : [[flag, value]]));
2026
+ if (command === "check" || command === "execute") {
2027
+ if (!isStudyCustodyName(target)) throw new StudyError("CONFIG", "Invalid study token " + JSON.stringify(target) + "; use a portable study name. " + usage);
2028
+ const profile = flags.get("--profile") ?? "local";
2029
+ const out = flags.get("--out");
2030
+ const expected = flags.get("--expected");
2031
+ if (!isStudyCustodyName(profile)) throw new StudyError("CONFIG", "--profile " + JSON.stringify(profile) + " must be a portable name. " + usage);
2032
+ if (out !== void 0 && !isAbsolute(out)) try {
2033
+ resolveStudyPath(resolve("."), out);
2034
+ } catch {
2035
+ throw new StudyError("CONFIG", "--out " + JSON.stringify(out) + " must be a portable contained path and cannot escape the consumer root. " + usage);
3631
2036
  }
3632
- return cleanup;
2037
+ if (expected !== void 0 && !isStudyDigest(expected)) throw new StudyError("CONFIG", "--expected " + JSON.stringify(expected) + " must be a SHA-256 fingerprint. " + usage);
2038
+ return {
2039
+ command,
2040
+ target,
2041
+ profile,
2042
+ out,
2043
+ expected
2044
+ };
3633
2045
  }
3634
- async #executeGoal(goal, session, evidence, control, parents, outputs) {
3635
- const started = performance.now();
3636
- const bounds = this.#options.manifest.bounds;
3637
- const scope = new StudyScope({
3638
- limit: bounds.goal,
3639
- control,
3640
- label: "Goal"
3641
- });
3642
- const meter = new StudyMeter({
3643
- request: bounds.request,
3644
- scopes: [scope, ...parents],
3645
- control
3646
- });
3647
- const product = await this.#produce(goal, session, evidence, control, scope, meter, parents, outputs);
3648
- return Object.freeze({
3649
- goal: goal.id,
3650
- product,
3651
- failure: control.failure,
3652
- usage: meter.usage,
3653
- duration: Math.ceil(performance.now() - started),
3654
- executed: true
3655
- });
2046
+ if (command === "grade") {
2047
+ const rubric = flags.get("--rubric");
2048
+ if (rubric === void 0) throw new StudyError("CONFIG", "Missing --rubric. " + usage);
2049
+ return {
2050
+ command,
2051
+ target,
2052
+ rubric
2053
+ };
3656
2054
  }
3657
- async #produce(goal, session, evidence, control, scope, meter, parents, outputs) {
3658
- const bounds = this.#options.manifest.bounds;
3659
- try {
3660
- const value = await control.wait(session.execute(goal, {
3661
- signal: control.signal,
3662
- meter,
3663
- evidence
3664
- }));
3665
- if (!isStudyProduct(value)) throw new StudyError("FAULT", "Driver returned no valid study product");
3666
- const candidate = Object.freeze({
3667
- value: cloneJSONValue(value.value),
3668
- partial: value.partial
3669
- });
3670
- const bytes = new TextEncoder().encode(JSON.stringify(candidate.value)).byteLength;
3671
- const stop = computeStudyOutputStop(bytes, bounds.goal.bytes, outputs);
3672
- const product = stop === void 0 ? candidate : void 0;
3673
- try {
3674
- for (const parent of parents) parent.check();
3675
- scope.check();
3676
- if (stop !== void 0) control.fail({
3677
- cause: "capacity",
3678
- message: "Study product exceeded its output allowance",
3679
- role: void 0,
3680
- stop
3681
- });
3682
- else for (const budget of outputs) budget.consume(bytes);
3683
- } catch (error) {
3684
- control.fail(readStudyFailure(error));
3685
- }
3686
- return product;
3687
- } catch (error) {
3688
- control.fail(readStudyFailure(error));
3689
- return;
3690
- } finally {
3691
- meter.close();
3692
- scope.close();
3693
- }
2055
+ const grading = flags.get("--grading");
2056
+ if (!isStudyDigest(grading)) throw new StudyError("CONFIG", "--grading " + JSON.stringify(grading ?? "<missing>") + " must be an exact SHA-256 grading identity. " + usage);
2057
+ if (command === "label") {
2058
+ const labels = flags.get("--labels");
2059
+ if (labels === void 0) throw new StudyError("CONFIG", "Missing --labels. " + usage);
2060
+ return {
2061
+ command,
2062
+ target,
2063
+ grading,
2064
+ labels
2065
+ };
3694
2066
  }
3695
- };
2067
+ return {
2068
+ command: "report",
2069
+ target,
2070
+ grading
2071
+ };
2072
+ }
3696
2073
  //#endregion
3697
2074
  //#region src/server/processes/constants.ts
3698
2075
  /** Limits each serialized IPC envelope, including its JSON framing. */
@@ -3785,6 +2162,23 @@ function resolveStudyInstrument(entry) {
3785
2162
  if (root === void 0) throw new StudyError("CONFIG", "Cannot locate the study runtime package");
3786
2163
  return root;
3787
2164
  }
2165
+ /** Reads the version of the study package that owns an executable entry.
2166
+ * @param entry - The installed runtime's executable entry.
2167
+ * @returns The non-empty version declared by the owning study package.
2168
+ * @throws StudyError - Thrown when no study package owns the entry or its version is absent or empty.
2169
+ * @example Read the installed version
2170
+ * ```ts
2171
+ * import { readStudyVersion } from '@orkestrel/study/server'
2172
+ *
2173
+ * declare const entry: string
2174
+ * readStudyVersion(entry)
2175
+ * ```
2176
+ */
2177
+ function readStudyVersion(entry) {
2178
+ const value = JSON.parse(readFileSync(join(resolveStudyInstrument(entry), "package.json"), "utf8"));
2179
+ if (!isRecord(value) || typeof value.version !== "string" || value.version.length === 0) throw new StudyError("CONFIG", "Study runtime package declares no version");
2180
+ return value.version;
2181
+ }
3788
2182
  /** Collects a directory and each of its ancestors up to the filesystem root, nearest first.
3789
2183
  * @param directory - The absolute directory to start from.
3790
2184
  * @returns The directory followed by every parent directory, ending at the root.
@@ -3856,14 +2250,14 @@ function terminateStudyChild(child) {
3856
2250
  for (const pid of [...descendants].reverse()) try {
3857
2251
  process.kill(pid, "SIGKILL");
3858
2252
  } catch (error) {
3859
- if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) throw error;
2253
+ if (!matchesSystemError(error, "ESRCH")) throw error;
3860
2254
  }
3861
2255
  }
3862
2256
  } catch (error) {
3863
2257
  try {
3864
2258
  process.kill(child.pid, 0);
3865
2259
  } catch (status) {
3866
- if (status instanceof Error && "code" in status && status.code === "ESRCH") return;
2260
+ if (matchesSystemError(status, "ESRCH")) return;
3867
2261
  throw status;
3868
2262
  }
3869
2263
  if (child.exitCode === null && child.signalCode === null) {
@@ -4631,35 +3025,13 @@ var StudyCommand = class {
4631
3025
  throw new StudyError("CONFIG", "Unsupported study operation");
4632
3026
  }
4633
3027
  async #prepare(request, signal) {
4634
- const directory = resolveStudyPath(join(this.#root, "studies"), request.target);
4635
- const definition = parseStudyDefinition(decodeStudyJSON(await readStudyBytes(resolveStudyPath(directory, "study.json"), STUDY_CUSTODY_BYTES)));
4636
- if (definition.name !== request.target) throw new StudyError("CONFIG", "Study name differs from its selected directory");
4637
- const bundle = resolveStudyPath(join(this.#root, "studies"), definition.driver);
4638
- const steps = [];
4639
- for (const id of definition.scenarios) {
4640
- signal.throwIfAborted();
4641
- const path = `scenarios/${id}.json`;
4642
- const bytes = await readStudyBytes(resolveStudyPath(bundle, path), STUDY_CUSTODY_BYTES);
4643
- const scenario = parseStudyScenario(decodeStudyJSON(bytes));
4644
- if (scenario.id !== id) throw new StudyError("CONFIG", "Scenario identity is incompatible");
4645
- steps.push({
4646
- scenario,
4647
- fixture: {
4648
- path,
4649
- digest: hashStudyBytes(bytes)
4650
- }
4651
- });
4652
- }
4653
- const scenarios = steps.map((step) => step.scenario);
4654
- const fixtures = steps.map((step) => step.fixture);
4655
- const profileBytes = await readStudyBytes(resolveStudyPath(bundle, `profiles/${request.profile}.json`), STUDY_CUSTODY_BYTES);
3028
+ const { bundle, definition, scenarios, fixtures, rubric } = await readStudyDeclaration(this.#root, request.target, signal);
3029
+ const profileBytes = await readStudyDeclarationBytes(this.#root, resolveStudyPath(bundle, `profiles/${request.profile}.json`));
4656
3030
  const source = parseStudyProfile(decodeStudyJSON(profileBytes));
4657
3031
  if (source.name !== request.profile) throw new StudyError("CONFIG", "Profile name differs from its selected file");
4658
3032
  if (source.bounds.series.duration > 864e5) throw new StudyError("CONFIG", "Series duration exceeds the 24-hour host ceiling");
4659
3033
  const profile = resolveStudyProfile(this.#root, source);
4660
- const rubric = parseStudyRubric(decodeStudyJSON(await readStudyBytes(resolveStudyPath(directory, definition.rubric), STUDY_CUSTODY_BYTES)));
4661
3034
  validateStudyInputs(definition, scenarios, rubric, profile);
4662
- if (definition.purpose === "quality" && profile.qualification === void 0) throw new StudyError("CONFIG", "Quality studies require a capacity qualification");
4663
3035
  signal.throwIfAborted();
4664
3036
  const artifacts = new StudyArtifactLoader({
4665
3037
  root: this.#root,
@@ -4685,7 +3057,7 @@ var StudyCommand = class {
4685
3057
  if (validation.value !== true || validation.instrument !== selected.instrument) throw new StudyError("FAULT", "Adapter validation identity differs");
4686
3058
  artifacts.verify();
4687
3059
  return {
4688
- directory: bundle,
3060
+ bundle,
4689
3061
  definition,
4690
3062
  scenarios: Object.freeze(scenarios),
4691
3063
  rubric,
@@ -4757,17 +3129,18 @@ var StudyCommand = class {
4757
3129
  await store.create();
4758
3130
  const scratch = await mkdtemp(join(parent, "scratch-"));
4759
3131
  try {
3132
+ const driver = new StudyProcessDriver({
3133
+ root: this.#root,
3134
+ entry: this.#entry,
3135
+ profile: preparation.profile,
3136
+ identity: preparation.identity,
3137
+ driver: preparation.definition.driver,
3138
+ adapter: join(preparation.bundle, "index.ts"),
3139
+ directory: scratch
3140
+ });
4760
3141
  const attempts = await new StudySeries({
4761
3142
  manifest,
4762
- driver: new StudyProcessDriver({
4763
- root: this.#root,
4764
- entry: this.#entry,
4765
- profile: preparation.profile,
4766
- identity: preparation.identity,
4767
- driver: preparation.definition.driver,
4768
- adapter: join(preparation.directory, "index.ts"),
4769
- directory: scratch
4770
- }),
3143
+ driver,
4771
3144
  store,
4772
3145
  signal
4773
3146
  }).execute();
@@ -4915,14 +3288,17 @@ var StudyCommand = class {
4915
3288
  });
4916
3289
  }
4917
3290
  async #result(target) {
4918
- const result = await readStudyResult(isAbsolute(target) ? resolve(target) : target.includes("/") ? resolveStudyPath(this.#root, target) : resolveStudyPath(join(this.#root, "tmp", "studies"), target));
3291
+ const result = await readStudyResult(isAbsolute(target) ? resolve(target) : target.includes("/") ? resolveStudyPath(this.#root, target) : resolveStudyPath(join(this.#root, "tmp", "studies"), target)).catch((error) => {
3292
+ if (matchesSystemError(error, "ENOENT")) throw new StudyError("CUSTODY", `Selected result is missing: ${target}`);
3293
+ throw error;
3294
+ });
4919
3295
  if (result.status !== "sealed") throw new StudyError("CUSTODY", "Interrupted execution cannot be graded or reported");
4920
3296
  return result;
4921
3297
  }
4922
3298
  async #grading(result, id) {
4923
3299
  const directory = join(result.directory, "grades", id);
4924
3300
  const raw = await readStudyBytes(join(directory, "grading.json"), STUDY_CUSTODY_BYTES).catch((error) => {
4925
- if (error instanceof Error && "code" in error && error.code === "ENOENT") throw new StudyError("CUSTODY", `Selected grading is missing: ${join(directory, "grading.json")}`);
3301
+ if (matchesSystemError(error, "ENOENT")) throw new StudyError("CUSTODY", `Selected grading is missing: ${join(directory, "grading.json")}`);
4926
3302
  throw error;
4927
3303
  });
4928
3304
  if (hashStudyBytes(raw) !== id) throw new StudyError("CUSTODY", "Selected grading differs from its byte identity");
@@ -4957,7 +3333,7 @@ var StudyCommand = class {
4957
3333
  try {
4958
3334
  await mkdir(directory);
4959
3335
  } catch (error) {
4960
- if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
3336
+ if (!matchesSystemError(error, "EEXIST")) throw error;
4961
3337
  }
4962
3338
  return checkStudyDirectory(directory);
4963
3339
  }, checkStudyDirectory(this.#root));
@@ -4969,7 +3345,10 @@ var StudyCommand = class {
4969
3345
  return result.seal.id;
4970
3346
  }
4971
3347
  async #instrument() {
4972
- return hashStudyBytes(await readStudyBytes(this.#entry, STUDY_CUSTODY_BYTES));
3348
+ return computeStudyInstrumentDigest(hashStudyBytes(await readStudyBytes(this.#entry, STUDY_CUSTODY_BYTES)), new ArtifactLoader({
3349
+ entries: [this.#entry],
3350
+ roots: [resolveStudyInstrument(this.#entry)]
3351
+ }).inspect().digest);
4973
3352
  }
4974
3353
  };
4975
3354
  //#endregion
@@ -5520,6 +3899,10 @@ var StudyCLI = class {
5520
3899
  process.stdout.write(STUDY_COMMAND_HELP);
5521
3900
  return 0;
5522
3901
  }
3902
+ if (arguments_.length === 1 && arguments_[0] === "--version") {
3903
+ process.stdout.write(`${readStudyVersion(this.#options.entry)}\n`);
3904
+ return 0;
3905
+ }
5523
3906
  if (arguments_.length === 0) {
5524
3907
  process.stderr.write(STUDY_COMMAND_HELP);
5525
3908
  return 64;
@@ -5548,6 +3931,98 @@ var StudyCLI = class {
5548
3931
  //#endregion
5549
3932
  //#region src/server/agents/helpers.ts
5550
3933
  /**
3934
+ * Reads an Agent tool call into a validated envelope from one read of each foreign field.
3935
+ * @remarks The returned envelope owns its fields; arguments retain their original record identity.
3936
+ * JSON validation and copying belong to the executed-result projection.
3937
+ * @param call - The call received from the admitted Agent
3938
+ * @returns A fresh ToolCall envelope with the original arguments record
3939
+ * @throws StudyError - Thrown with code `FAULT` when a field cannot be read or the envelope is malformed
3940
+ * @example Read an Agent tool call
3941
+ * ```ts
3942
+ * import { readStudyAgentToolCall } from '@orkestrel/study/server'
3943
+ *
3944
+ * const call = readStudyAgentToolCall({ id: 'c1', name: 'write', arguments: { path: 'note.txt' } })
3945
+ * // { id: 'c1', name: 'write', arguments: { path: 'note.txt' } }
3946
+ * ```
3947
+ */
3948
+ function readStudyAgentToolCall(call) {
3949
+ const observed = attempt(() => isRecord(call) ? {
3950
+ id: "id" in call ? call.id : void 0,
3951
+ name: "name" in call ? call.name : void 0,
3952
+ arguments: "arguments" in call ? call.arguments : void 0
3953
+ } : void 0);
3954
+ if (!observed.success || !isToolCall(observed.value)) throw new StudyError("FAULT", "Admitted Agent supplied invalid tool observation");
3955
+ return observed.value;
3956
+ }
3957
+ /**
3958
+ * Builds the JSON evidence form of an Agent tool call and its executed result.
3959
+ * @param call - The call received from the admitted Agent
3960
+ * @param result - The executed tool result
3961
+ * @returns The call identity, owned arguments, success, and owned value or error text
3962
+ * @throws StudyError - Thrown with code `FAULT` when the call, result, arguments, or value is malformed
3963
+ * @example
3964
+ * ```ts
3965
+ * buildStudyAgentToolResult(call, { success: true, value: 'stored' })
3966
+ * ```
3967
+ */
3968
+ function buildStudyAgentToolResult(call, result) {
3969
+ const { id, name, arguments: arguments_ } = readStudyAgentToolCall(call);
3970
+ const returned = attempt(() => {
3971
+ if (!isObject(result)) return void 0;
3972
+ const success = "success" in result ? result.success : void 0;
3973
+ if (typeof success !== "boolean") return void 0;
3974
+ return success ? {
3975
+ success,
3976
+ value: "value" in result ? result.value : void 0
3977
+ } : {
3978
+ success,
3979
+ error: "error" in result ? result.error : void 0
3980
+ };
3981
+ });
3982
+ if (!returned.success || returned.value === void 0) throw new StudyError("FAULT", "Admitted Agent supplied invalid tool result");
3983
+ const argumentsCopy = attempt(() => cloneJSONValue(arguments_));
3984
+ if (!argumentsCopy.success) throw new StudyError("FAULT", "Admitted Agent supplied non-JSON tool arguments");
3985
+ const outcome = returned.value;
3986
+ if (outcome.success) {
3987
+ const valueCopy = attempt(() => cloneJSONValue(outcome.value));
3988
+ if (!valueCopy.success) throw new StudyError("FAULT", "Admitted Agent supplied non-JSON tool value");
3989
+ return {
3990
+ id,
3991
+ name,
3992
+ arguments: argumentsCopy.value,
3993
+ success: true,
3994
+ value: valueCopy.value
3995
+ };
3996
+ }
3997
+ if (typeof outcome.error !== "string") throw new StudyError("FAULT", "Admitted Agent supplied invalid tool error");
3998
+ return {
3999
+ id,
4000
+ name,
4001
+ arguments: argumentsCopy.value,
4002
+ success: false,
4003
+ error: outcome.error
4004
+ };
4005
+ }
4006
+ /**
4007
+ * Builds the JSON evidence form of an Agent tool call the Agent denied before dispatch.
4008
+ * @param call - The call received from the admitted Agent
4009
+ * @param reason - The denial reason, retained only when it is a string
4010
+ * @returns The call identity with the optional string reason
4011
+ * @throws StudyError - Thrown with code `FAULT` when the call is malformed
4012
+ * @example
4013
+ * ```ts
4014
+ * buildStudyAgentToolDenial(call, 'refused')
4015
+ * ```
4016
+ */
4017
+ function buildStudyAgentToolDenial(call, reason) {
4018
+ const { id, name } = readStudyAgentToolCall(call);
4019
+ return {
4020
+ id,
4021
+ name,
4022
+ ...typeof reason === "string" ? { reason } : {}
4023
+ };
4024
+ }
4025
+ /**
5551
4026
  * Builds a request revision by hashing its canonical rendering exactly as rendered.
5552
4027
  * @remarks
5553
4028
  * The request is rendered once with key-sorted canonical JSON and hashed as rendered; no
@@ -5650,94 +4125,6 @@ function isStudyAgentResponse(value) {
5650
4125
  ]) && typeof value.content === "string" && isArray(value.calls) && value.calls.every(isToolCall) && isJSONValue(value.calls) && (value.consume === void 0 || typeof value.consume === "string" && value.consume.length > 0) && (value.abort === void 0 || typeof value.abort === "boolean") && (value.truncated === void 0 || typeof value.truncated === "boolean") && ["tokens", "bytes"].every((key) => value[key] === void 0 || typeof value[key] === "number" && Number.isSafeInteger(value[key]) && value[key] >= 0));
5651
4126
  }
5652
4127
  //#endregion
5653
- //#region src/core/agents/helpers.ts
5654
- /**
5655
- * Prices a runtime estimate through an explicit gauge.
5656
- * @param gauge - The declared scale and overhead
5657
- * @param estimate - The runtime's estimate of one complete request
5658
- * @returns The projected prompt tokens, rounded up
5659
- */
5660
- function projectStudyAgentRequest(gauge, estimate) {
5661
- return Math.ceil(gauge.overhead + gauge.scale * estimate);
5662
- }
5663
- /**
5664
- * Checks that a successful provider call reported a usable actual prompt count.
5665
- * @remarks
5666
- * The check compares nothing with a forecast; room enforcement stays with the transport.
5667
- * @param usage - The usage a successful provider call reported
5668
- * @param role - The responsibility whose request was dispatched; the failure retains it
5669
- * @returns Nothing when the prompt count is finite and positive
5670
- * @throws StudyError - Thrown with code `FAULT` when the prompt count is missing, not finite, or not positive
5671
- */
5672
- function validateStudyAgentUsage(usage, role) {
5673
- const prompt = usage?.prompt;
5674
- if (typeof prompt !== "number" || !Number.isFinite(prompt) || prompt <= 0) {
5675
- const message = "Gauged request returned no usable prompt count";
5676
- throw new StudyError("FAULT", message, {
5677
- cause: "infrastructure",
5678
- message,
5679
- role,
5680
- stop: void 0
5681
- });
5682
- }
5683
- }
5684
- /**
5685
- * Translates only a nominal Agent window refusal into the attributable capacity failure.
5686
- * @remarks
5687
- * The failure's stop is the `window` layer with the id `Agent window`; its `max` is the window and
5688
- * its `consumed` the measured request, each read from the error's context when it is a finite
5689
- * number.
5690
- * @param error - The caught or reported value
5691
- * @param runtime - The admitted Agent namespace owning the nominal guard
5692
- * @returns A capacity StudyError for an Agent `WINDOW` error; every other value with its identity
5693
- */
5694
- function translateStudyAgentError(error, runtime) {
5695
- if (!runtime.isAgentError(error) || error.code !== "WINDOW") return error;
5696
- const context = error.context;
5697
- return new StudyError("CAPACITY", error.message, {
5698
- cause: "capacity",
5699
- message: error.message,
5700
- role: "agent",
5701
- stop: {
5702
- layer: "window",
5703
- id: "Agent window",
5704
- max: parseStudyReading(context?.max),
5705
- consumed: parseStudyReading(context?.consumed)
5706
- }
5707
- });
5708
- }
5709
- /**
5710
- * Projects a request's structural components onto plain JSON without storing a total.
5711
- * @param measure - The runtime's disjoint per-message, tools and schema estimates
5712
- * @returns The same components in request order
5713
- */
5714
- function buildStudyAgentParts(measure) {
5715
- return {
5716
- messages: measure.messages.map((message) => ({
5717
- id: message.id,
5718
- role: message.role,
5719
- content: message.content,
5720
- framing: message.framing,
5721
- calls: message.calls,
5722
- thinking: message.thinking,
5723
- images: message.images
5724
- })),
5725
- tools: measure.tools,
5726
- schema: measure.schema
5727
- };
5728
- }
5729
- /**
5730
- * Builds the evidence join of a wire request to its canonical dispatch.
5731
- * @param dispatch - The measured dispatch, or undefined when the caller measured none
5732
- * @returns The dispatch id and revision, each null when unknown
5733
- */
5734
- function buildStudyAgentDispatch(dispatch) {
5735
- return {
5736
- canonical: dispatch?.id ?? null,
5737
- revision: dispatch?.revision ?? null
5738
- };
5739
- }
5740
- //#endregion
5741
4128
  //#region src/server/agents/StudyAgentTraceGoal.ts
5742
4129
  /** Holds one goal's retained trace rows, its latest admission and its last refused identity. */
5743
4130
  var StudyAgentTraceGoal = class {
@@ -5966,15 +4353,16 @@ var StudyAgentSummary = class {
5966
4353
  constructor(options) {
5967
4354
  const { limit } = options;
5968
4355
  const { scale, overhead } = options.gauge;
4356
+ const gauge = {
4357
+ scale,
4358
+ overhead
4359
+ };
5969
4360
  if (!Number.isSafeInteger(limit) || limit <= 0) throw new StudyError("CONFIG", "Summary limit must be a positive safe integer");
5970
- if (!isFiniteNumber(scale) || scale <= 0 || !isFiniteNumber(overhead) || overhead < 0) throw new StudyError("CONFIG", "Summary gauge scale must be finite and positive and its overhead finite and nonnegative");
4361
+ if (!isStudyAgentGauge(gauge)) throw new StudyError("CONFIG", "Summary gauge scale must be finite and positive and its overhead finite and nonnegative");
5971
4362
  this.#options = {
5972
4363
  ...options,
5973
4364
  limit,
5974
- gauge: {
5975
- scale,
5976
- overhead
5977
- }
4365
+ gauge
5978
4366
  };
5979
4367
  }
5980
4368
  /** Measures one canonical request, refuses it at the limit, and sends the very same array to the provider.
@@ -7276,6 +5664,6 @@ var StudyOllamaTransport = class {
7276
5664
  }
7277
5665
  };
7278
5666
  //#endregion
7279
- export { ARTIFACT_BYTES, ARTIFACT_DEPTH, ARTIFACT_FILES, ARTIFACT_FILE_BYTES, ARTIFACT_ROOTS, ARTIFACT_VISITS, ArtifactError, ArtifactLoader, Resolution, STUDY_COMMAND_DURATION, STUDY_COMMAND_GRADER, STUDY_COMMAND_HELP, STUDY_COMMAND_OPERATION, STUDY_CUSTODY_BYTES, STUDY_DRAIN_DURATION, STUDY_FRAME_BYTES, STUDY_LAUNCH_BYTES, STUDY_LAUNCH_CLEANUP, STUDY_LAUNCH_DURATION, STUDY_LOG_BYTES, STUDY_PACKET_DISCLOSURE, STUDY_PENDING_LIMIT, STUDY_PROCESS_LABEL, STUDY_RESERVED_NAME, STUDY_TASK_DURATION, STUDY_TASK_OPERATION, STUDY_WORKER_OPERATION, StudyAgentScript, StudyAgentSummary, StudyAgentTrace, StudyArtifactLoader, StudyCLI, StudyChannel, StudyCommand, StudyEvidenceSink, StudyGradingStore, StudyLauncher, StudyOllamaTransport, StudyProcessDriver, StudyProcessGoal, StudyProcessSession, StudyRemoteMeter, StudyRemoteTicket, StudyResultStore, StudyTask, StudyTaskWorker, StudyWorker, buildStudyAgentReadings, buildStudyAgentRevision, buildStudyOllamaModels, buildStudyOllamaOptions, buildStudyOllamaRaw, buildStudyOllamaSnapshot, buildStudyOllamaTiming, buildStudyOllamaTransportOptions, buildStudyPacket, buildStudyPacketLinks, buildStudyReference, checkStudyDirectory, collectStudyAncestors, collectStudyEntries, collectStudyEvidence, computeStudyAttributions, computeStudyCommandFingerprint, computeStudyFixtureDigest, computeStudyInstrumentDigest, computeStudyPacketDigest, computeStudyPartition, computeStudyProfileDigest, computeStudyReport, computeStudySealId, containsArtifact, createStudyProcessFailure, decodeStudyJSON, drainStudyChild, encodeStudyJSON, gradeStudyEvidence, hashStudyBytes, isArtifactError, isStudyAdapter, isStudyAgentResponse, isStudyCharge, isStudyCustodyAttempt, isStudyCustodyEvidence, isStudyCustodyFailure, isStudyCustodyFile, isStudyCustodyManifest, isStudyCustodyName, isStudyCustodyObservation, isStudyCustodySeal, isStudyCustodyTrial, isStudyEvidence, isStudyFrame, isStudyIdentity, isStudyProcessFailure, isStudyTrial, iterateArtifactEntries, parseStudyArguments, readArtifact, readArtifactBytes, readStudyAdapter, readStudyAgentMessages, readStudyAgentResult, readStudyBytes, readStudyHandle, readStudyOllamaDigest, readStudyOllamaFailure, readStudyOllamaRequest, readStudyOllamaSupport, readStudyOllamaTerminal, readStudyOllamaTiming, readStudyResult, resolveStudyAgentContent, resolveStudyInstrument, resolveStudyPath, resolveStudyProfile, scanStudyExecution, scanStudyTree, terminateStudyChild, translateStudyPacketLabeling, validateStudyEvidenceBytes, validateStudyOllamaCompletion, validateStudyOllamaInventory, validateStudyOllamaRoles, validateStudyRuntime, valueToWire, writeStudyBytes };
5667
+ export { ARTIFACT_BYTES, ARTIFACT_DEPTH, ARTIFACT_FILES, ARTIFACT_FILE_BYTES, ARTIFACT_ROOTS, ARTIFACT_VISITS, ArtifactError, ArtifactLoader, Resolution, STUDY_COMMAND_DURATION, STUDY_COMMAND_GRADER, STUDY_COMMAND_HELP, STUDY_COMMAND_OPERATION, STUDY_CUSTODY_BYTES, STUDY_DRAIN_DURATION, STUDY_FRAME_BYTES, STUDY_LAUNCH_BYTES, STUDY_LAUNCH_CLEANUP, STUDY_LAUNCH_DURATION, STUDY_LOG_BYTES, STUDY_PACKET_DISCLOSURE, STUDY_PENDING_LIMIT, STUDY_PROCESS_LABEL, STUDY_RESERVED_NAME, STUDY_TASK_DURATION, STUDY_TASK_OPERATION, STUDY_WORKER_OPERATION, StudyAgentScript, StudyAgentSummary, StudyAgentTrace, StudyArtifactLoader, StudyCLI, StudyChannel, StudyCommand, StudyEvidenceSink, StudyGradingStore, StudyLauncher, StudyOllamaTransport, StudyProcessDriver, StudyProcessGoal, StudyProcessSession, StudyRemoteMeter, StudyRemoteTicket, StudyResultStore, StudyTask, StudyTaskWorker, StudyWorker, buildStudyAgentReadings, buildStudyAgentRevision, buildStudyAgentToolDenial, buildStudyAgentToolResult, buildStudyOllamaModels, buildStudyOllamaOptions, buildStudyOllamaRaw, buildStudyOllamaSnapshot, buildStudyOllamaTiming, buildStudyOllamaTransportOptions, buildStudyPacket, buildStudyPacketLinks, buildStudyReference, checkStudyDirectory, collectStudyAncestors, collectStudyEntries, collectStudyEvidence, computeStudyAttributions, computeStudyCommandFingerprint, computeStudyFixtureDigest, computeStudyInstrumentDigest, computeStudyPacketDigest, computeStudyPartition, computeStudyProfileDigest, computeStudyReport, computeStudySealId, containsArtifact, createStudyProcessFailure, decodeStudyJSON, drainStudyChild, encodeStudyJSON, gradeStudyEvidence, hashStudyBytes, isArtifactError, isStudyAdapter, isStudyAgentResponse, isStudyCharge, isStudyCustodyAttempt, isStudyCustodyEvidence, isStudyCustodyFailure, isStudyCustodyFile, isStudyCustodyManifest, isStudyCustodyName, isStudyCustodyObservation, isStudyCustodySeal, isStudyCustodyTrial, isStudyEvidence, isStudyFrame, isStudyIdentity, isStudyProcessFailure, isStudyTrial, iterateArtifactEntries, matchesSystemError, parseStudyArguments, readArtifact, readArtifactBytes, readStudyAdapter, readStudyAgentMessages, readStudyAgentResult, readStudyAgentToolCall, readStudyBytes, readStudyDeclaration, readStudyDeclarationBytes, readStudyHandle, readStudyOllamaDigest, readStudyOllamaFailure, readStudyOllamaRequest, readStudyOllamaSupport, readStudyOllamaTerminal, readStudyOllamaTiming, readStudyResult, readStudyVersion, resolveStudyAgentContent, resolveStudyInstrument, resolveStudyPath, resolveStudyProfile, scanStudyExecution, scanStudyTree, terminateStudyChild, translateStudyPacketLabeling, validateStudyEvidenceBytes, validateStudyOllamaCompletion, validateStudyOllamaInventory, validateStudyOllamaRoles, validateStudyRuntime, valueToWire, writeStudyBytes };
7280
5668
 
7281
5669
  //# sourceMappingURL=index.js.map