evals-lab 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -33,7 +33,7 @@
33
33
  // and that is a measurement.
34
34
 
35
35
  import yaml from "./js-yaml.mjs";
36
-
36
+
37
37
  import { evaluate, asText, templateProblem, WdlError, } from "./flows/wdl.mjs";
38
38
  import { stepsOf as flowStepsOf, actionsByName, readerFor, readsOf, } from "./flows/record.mjs";
39
39
  import { flowApi, } from "./flows/flowApi.mjs";
@@ -41,7 +41,7 @@ import registerMetrics from "./metrics/builtin.mjs";
41
41
  import list, { ECHO, CASE_MODES, legacyTagsOut, legacyTagsModifiers, cleaningFilters, preambleFilters, CLEANING_MODIFIERS,
42
42
  splitItems, tidy, tryExamples, itemRulesProblems, dropBy, rejectBy, matcher,
43
43
  FILTER_TYPES, FILTER_SEPARATORS, FILTER_MODIFIER, applyFilter, applyFilterSet, tryFilter, splitSample, filterProblems, filterFromItemRule,
44
- } from "./kinds/list.mjs";
44
+ filterV4, V3_TEXT_TYPES, } from "./kinds/list.mjs";
45
45
 
46
46
  // ---- The types -------------------------------------------------------------
47
47
  // docs/pipeline-model.md as types, and the shapes of the registries every
@@ -141,16 +141,18 @@ import list, { ECHO, CASE_MODES, legacyTagsOut, legacyTagsModifiers, cleaningFil
141
141
 
142
142
 
143
143
 
144
- /** The filter sets a job cleans and gates its reply by (#339, #405): each a
144
+ /** The filter sets a job cleans and gates its reply by (#339, #615): each a
145
145
  link to a Library filter set (followed latest or pinned) or a private one
146
- held inline, run at the step's `stage` (#432: a set has no stage of its
147
- own; the step runs its filters of that stage). A job may hold one `filters` step of
148
- each stage -- a `text` one run on the raw reply before it is read into
149
- items (Pre-format), an `items` one run on the items after (Post-format). */
146
+ held inline, one list in run order. `format` is where the job's Response
147
+ Format reads the reply, when it reads items: before `filterSets[format]`
148
+ -- the filters above it run on the raw text, those below on each item.
149
+ Absent, a linked set's own `format` marks it (the first that has one),
150
+ or else the format reads after them all. A format that reads text reads
151
+ after them all whatever it says. A job holds one `filters` step. */
150
152
 
151
153
 
152
-
153
154
 
155
+
154
156
 
155
157
 
156
158
  /** A job's steps: its Content stage, then its Responses (§16). */
@@ -358,32 +360,20 @@ import list, { ECHO, CASE_MODES, legacyTagsOut, legacyTagsModifiers, cleaningFil
358
360
  * at. `filters` are tried in order -- each drop-item narrows the items the
359
361
  * next reads, and the first reject-reply that fires gates the answer.
360
362
  */
361
- /** Which side of Response Format a filter step sits on (#405): `text`
362
- before it, on the raw reply (Pre-format), `items` after it (Post-format).
363
- It also names what a filter reads: the reply's text (rewrite-text /
364
- reject-text) or the items (drop-item, rewrite-item, keep-first,
365
- reject-reply). A text filter runs at either stage, an item filter only
366
- Post-format after a kind that reads items (#557, `stageReads`); a set
367
- belongs to no stage (#432), and runs its share in each it is linked in. */
368
-
369
-
370
- /** The stage a filter type's scope belongs to: text filters read the raw
371
- reply; item, list and answer filters read the items it is read into. */
372
- export const stageOfScope = (scope ) => (scope === "text" ? "text" : "items");
373
-
374
- /** What job's [stage] filters read when it answers as [kind] (#557):
375
- Pre-format the text; Post-format the items after a kind that reads
376
- items, and the text after any other. A filter runs there when its
377
- `stageOfScope` is this. */
378
- export const stageReads = (stage , kind ) =>
379
- stage === "items" && OUTPUT_KINDS[kind ?? ""]?.offers?.includes("items") ? "items" : "text";
380
-
381
- /** A filter stage as the page names it: before the reply is formatted, or after. */
382
- export const FILTER_STAGE_LABEL = { text: "Pre-format", items: "Post-format" };
363
+ /** Whether a job answering as [kind] reads its reply into items, so that
364
+ where its Response Format sits among its filters matters (#615): the
365
+ filters above it run on the raw text, those below on each item. A format
366
+ that reads text reads after every filter. */
367
+ export const formatReadsItems = (kind ) =>
368
+ !!OUTPUT_KINDS[kind ?? ""]?.offers?.includes("items");
383
369
 
384
370
 
385
371
 
386
372
 
373
+
374
+
375
+
376
+
387
377
 
388
378
 
389
379
  /** A Library filter set by reference; a run's copy says the version it used:
@@ -571,14 +561,10 @@ export const FILTER_STAGE_LABEL = { text: "Pre-form
571
561
 
572
562
 
573
563
 
564
+
574
565
 
575
-
576
-
566
+
577
567
 
578
-
579
-
580
-
581
-
582
568
 
583
569
 
584
570
 
@@ -668,17 +654,16 @@ export function kept(text , max = KEPT_CHARS) {
668
654
 
669
655
 
670
656
 
671
-
672
-
657
+
673
658
 
674
659
 
675
660
 
676
661
 
677
662
 
678
- /** Which filter discarded a job's reply, or emptied it, and where (#435). */
663
+ /** Which filter discarded a job's reply, or emptied it, and where (#435). A
664
+ run kept before #615 also says the stage it ran in, which nothing reads. */
679
665
 
680
666
 
681
-
682
667
 
683
668
 
684
669
 
@@ -713,7 +698,6 @@ export function kept(text , max = KEPT_CHARS) {
713
698
 
714
699
 
715
700
 
716
-
717
701
 
718
702
 
719
703
 
@@ -1247,10 +1231,10 @@ const platformOf = (src
1247
1231
 
1248
1232
 
1249
1233
 
1250
-
1251
-
1252
-
1253
-
1234
+
1235
+
1236
+
1237
+
1254
1238
 
1255
1239
 
1256
1240
 
@@ -1549,24 +1533,42 @@ function upgradeDatasetBody (body ) {
1549
1533
  `{ version, filters }` body #334 introduces. A later format adds a step to
1550
1534
  `upgradeFilterSetBody`, as `upgradeDatasetBody` grew, with server.py's
1551
1535
  `upgrade_filter_set_body` its twin. */
1552
- const FILTER_SET_VERSION = 3 ;
1553
-
1554
- /** [body] as this version of a filter set (3), from any earlier one. Version 2
1555
- (#405) named the stage a set shaped; version 3 (#432) names none -- each
1556
- filter's type decides where it runs -- so a version-1 or -2 body loses its
1557
- `stage` and reads as this one. Anything else comes back untouched for
1558
- `filterSetBodyProblems` to refuse. Pure: the same body gives the same
1559
- answer, and server.py's upgrade_filter_set_body is its twin. */
1536
+ const FILTER_SET_VERSION = 4 ;
1537
+
1538
+ /** [body] as this version of a filter set (4), from any earlier one. Version 2
1539
+ (#405) named the stage a set shaped, which version 3 (#432) dropped: each
1540
+ filter's type decided where it ran. Version 4 (#615) has seven verbs that
1541
+ run wherever they sit, around a Response Format a set may straddle: each
1542
+ filter becomes the verb that reads a reply as it did (`filterV4`), and a
1543
+ set that held both text and item filters puts its text ones first, in
1544
+ their order, then its `format` marker, then its item ones -- what it ran
1545
+ linked both before and after the format. Anything else comes back
1546
+ untouched for `filterSetBodyProblems` to refuse. Pure: the same body gives
1547
+ the same answer, and server.py's upgrade_filter_set_body is its twin. */
1560
1548
  function upgradeFilterSetBody (body ) {
1561
1549
  if (!isObj(body)) return body;
1562
1550
  if (body.version === FILTER_SET_VERSION) return body;
1563
- if (body.version === 1 || body.version === 2) {
1551
+ if (body.version === 1 || body.version === 2 || body.version === 3) {
1564
1552
  const { stage: _stage, ...rest } = body;
1565
- return { ...rest, version: FILTER_SET_VERSION } ;
1553
+ if (!Array.isArray(rest.filters)) return { ...rest, version: FILTER_SET_VERSION } ;
1554
+ const isText = (f ) => isObj(f) && V3_TEXT_TYPES.includes(f.type );
1555
+ const text = rest.filters.filter(isText), items = rest.filters.filter((f ) => !isText(f));
1556
+ return { ...rest, version: FILTER_SET_VERSION, filters: [...text, ...items].map(filterV4),
1557
+ ...(text.length && items.length ? { format: text.length } : {}) } ;
1566
1558
  }
1567
1559
  return body;
1568
1560
  }
1569
1561
 
1562
+ /** A version-1 to -3 private filter set (#615) as a version-4 one that runs
1563
+ on the side of the format its link sits: each filter its verb, in its
1564
+ order, and no marker -- a private set held one stage's filters. A current
1565
+ body comes back as it was. */
1566
+ function ownBodyV4(own ) {
1567
+ if (!isObj(own) || ![1, 2, 3].includes(own.version )) return own;
1568
+ const { stage: _stage, ...rest } = own;
1569
+ return { ...rest, version: FILTER_SET_VERSION, ...(Array.isArray(rest.filters) ? { filters: rest.filters.map(filterV4) } : {}) };
1570
+ }
1571
+
1570
1572
  /** A filter set with no filters, at this version: what New filter set starts
1571
1573
  from, and what a lab with none holds. */
1572
1574
  function blankFilterSetBody() {
@@ -1581,7 +1583,16 @@ function filterSetBodyProblems(body , at = "the filter set") {
1581
1583
  if (body.version !== FILTER_SET_VERSION) {
1582
1584
  return [`${at} is version ${JSON.stringify(body.version)}, and the lab reads version ${FILTER_SET_VERSION}`];
1583
1585
  }
1584
- return filterProblems(body.filters, at);
1586
+ return [...filterProblems(body.filters, at),
1587
+ ...formatProblems(body.format, Array.isArray(body.filters) ? body.filters.length : 0, at)];
1588
+ }
1589
+
1590
+ /** Why [format] is not a place among [n] entries the Response Format can
1591
+ read at (#615): a whole number from 0 to [n], or none. */
1592
+ function formatProblems(format , n , at ) {
1593
+ if (format == null) return [];
1594
+ return Number.isInteger(format) && (format ) >= 0 && (format ) <= n
1595
+ ? [] : [`${at}: Response Format has to sit among its filters`];
1585
1596
  }
1586
1597
 
1587
1598
  /** Why [link] is not a filter-set link a run's Responses step may carry, one
@@ -3324,30 +3335,20 @@ async function runPipeline(stages , dataUrl , cal
3324
3335
  // and that answer is graded as though the job had worked.
3325
3336
  const handsOn = i < stages.length - 1;
3326
3337
  const against = res.readAgainst ?? echo;
3327
- // The Pre-format (text) filters shape the raw reply before the kind
3328
- // reads it (#405): a Rewrite text reshapes it, a Reject text ends the job.
3338
+ // The filters above the format shape the raw reply before the kind
3339
+ // reads it (#405, #615): they reshape it, or a Reject ends the job.
3329
3340
  const pre = applyFilterSet(st.before || [], { body: res.raw ?? "", items: [], instruction: against }, "text");
3330
- const by = (filter , stage , rejected ) => ({ filter, stage, job: i + 1, rejected });
3331
- if (pre.reject != null) return result({ error: named(`discarded: ${pre.reject}`), filteredBy: by(pre.reject, "text", true) });
3341
+ const by = (filter , rejected ) => ({ filter, job: i + 1, rejected });
3342
+ if (pre.reject != null) return result({ error: named(`discarded: ${pre.reject}`), filteredBy: by(pre.reject, true) });
3332
3343
  if (pre.rewrote) transcript[transcript.length - 1] .filtered = { got: pre.body, by: pre.rewrote };
3333
3344
  const read = reader.read(pre.body, { echo: against, parsing: opts.parsing, settings: st.settings });
3334
3345
  if (read.error) return result({ error: named(read.error) });
3335
3346
  const mod = applyModifiers(mods[i], kind, read.value, { instruction: against, body: read.body ?? res.raw ?? "" });
3336
3347
  if (mod.reject) {
3337
- return result({ error: named(`discarded: ${mod.reject}`), ...(mod.by ? { filteredBy: by(mod.by, "items", true) } : {}) });
3338
- }
3339
- // A kind that reads no items runs its Post-format filters on its text,
3340
- // once read (#557); what they leave is what is graded.
3341
- if (st.after?.length && typeof mod.value === "string") {
3342
- const post = applyFilterSet(st.after, { body: mod.value, items: [], instruction: against }, "text");
3343
- if (post.reject != null) return result({ error: named(`discarded: ${post.reject}`), filteredBy: by(post.reject, "items", true) });
3344
- if (post.rewrote) {
3345
- transcript[transcript.length - 1] .filtered = { got: post.body, by: [...(pre.rewrote || []), ...post.rewrote] };
3346
- mod.value = post.body;
3347
- }
3348
+ return result({ error: named(`discarded: ${mod.reject}`), ...(mod.by ? { filteredBy: by(mod.by, true) } : {}) });
3348
3349
  }
3349
3350
  // A filter that dropped the last item: what a zero result is explained by.
3350
- const emptied = mod.by && reader.terms && !reader.terms(mod.value).length ? { filteredBy: by(mod.by, "items", false) } : {};
3351
+ const emptied = mod.by && reader.terms && !reader.terms(mod.value).length ? { filteredBy: by(mod.by, false) } : {};
3351
3352
  const nothing = handsOn ? reader.handsOn?.(mod.value) : null;
3352
3353
  if (nothing) return result({ error: named(nothing), ...emptied });
3353
3354
  const value = mod.value;
@@ -3377,8 +3378,8 @@ function readList(reply , out , instruction =
3377
3378
  const reader = OUTPUT_KINDS[out.kind];
3378
3379
  if (!reader) return { items: [], dropped: [], echoed: 0, error: `"${out.kind}" is not an output kind this lab has` };
3379
3380
  const { kind, modifiers, before, ...settings } = out;
3380
- // The Pre-format (text) filters shape the raw reply before the kind
3381
- // reads it (#405); a Reject text ends the job.
3381
+ // The filters above the format shape the raw reply before the kind reads
3382
+ // it (#405, #615); a Reject ends the job.
3382
3383
  const pre = applyFilterSet(before || [], { body: reply, items: [], instruction }, "text");
3383
3384
  if (pre.reject != null) return { items: [], dropped: [], echoed: 0, error: `discarded: ${pre.reject}` };
3384
3385
  const read = reader.read(pre.body, { echo: instruction, settings });
@@ -3456,7 +3457,11 @@ function applyModifiers (list , kind ,
3456
3457
  // 16: a target names an AI Connection and, optionally, a Connection Profile,
3457
3458
  // where it named one Target profile; a run's table is keyed by the pair
3458
3459
  // (answerKey) -- the Target profile split in the store as it opens (#445).
3459
- const PIPELINE_VERSION = 18 ;
3460
+ // 17: a request reads by its job's HTTP API; 18: a saved-mode Source content
3461
+ // pins one response (#596).
3462
+ // 19: a job's filters are one list, run in order around its Response Format,
3463
+ // and filters are seven verbs (#615).
3464
+ const PIPELINE_VERSION = 19 ;
3460
3465
 
3461
3466
  // Plain objects, so an entry is added by assignment and a reader never needs
3462
3467
  // to know which registered it.
@@ -3965,7 +3970,7 @@ SOURCE_TYPES.dynamic = {
3965
3970
  };
3966
3971
 
3967
3972
  // HTTP GET (docs/dynamic-sources.md § 1): the safest pull provider. It fetches
3968
- // a URL through the relay server-side -- Fetch now keeps a response, Live
3973
+ // a URL through the relay server-side -- Get response keeps a response, Live
3969
3974
  // data fetches it again at submit -- so the page drives only the
3970
3975
  // create dialog and the Call labels; the fetch, redaction and token-refusal are
3971
3976
  // the server's (its SOURCE_PROVIDERS mirror in server.py).
@@ -3974,6 +3979,7 @@ DYNAMIC_PROVIDERS["http-get"] = { // provider: the HTTP GET adapter's own id
3974
3979
  label: "HTTP GET",
3975
3980
  icon: "square-code",
3976
3981
  noun: "response",
3982
+ produceIcon: "cloud-sync",
3977
3983
  tier: "pull",
3978
3984
  config: ["url"],
3979
3985
  configProblems(config){
@@ -4031,7 +4037,7 @@ DYNAMIC_PROVIDERS["api"] = { // provider: the API call adapter's own id
4031
4037
  // or an Upload… by hand, formalised as a provider. The lab fetches and executes
4032
4038
  // nothing: a Supplied item is named by the id the caller gives it (its file
4033
4039
  // name), which is what joins it to a case. It has no config and no server-side
4034
- // production, so no Fetch now and no live data; its Calls are the items handed in,
4040
+ // production, so no Get response and no live data; its Calls are the items handed in,
4035
4041
  // redacted on the way in like any other (server.py's take_dynamic). A hosted run
4036
4042
  // grades those Calls; a solely-Supplied Source with none is run only via the CLI.
4037
4043
  DYNAMIC_PROVIDERS["supplied"] = { // provider: the Supplied adapter's own id
@@ -4061,7 +4067,7 @@ DYNAMIC_PROVIDERS["file"] = { // provider: the File on disk adapter's own id
4061
4067
  label: "File on disk",
4062
4068
  icon: "file",
4063
4069
  noun: "file",
4064
- produceLabel: "Read now",
4070
+ produceIcon: "file-input",
4065
4071
  tier: "local",
4066
4072
  config: ["path"],
4067
4073
  configProblems(config){
@@ -4085,7 +4091,7 @@ DYNAMIC_PROVIDERS["command"] = { // provider: the Command adapter's own id
4085
4091
  label: "Command",
4086
4092
  icon: "terminal",
4087
4093
  noun: "call",
4088
- produceLabel: "Run now",
4094
+ produceIcon: "square-terminal",
4089
4095
  tier: "local",
4090
4096
  config: ["command", "args"],
4091
4097
  configProblems(config){
@@ -5163,23 +5169,20 @@ STEP_TYPES.modifier = {
5163
5169
  };
5164
5170
 
5165
5171
  // A filters step: the filter sets a job cleans and gates its reply by, each a
5166
- // link or a private set (#339, #405), at the step's `stage`. A job may hold
5167
- // one of each stage -- a `text` one Pre-format, an `items` one Post-format -- so
5168
- // the type is `many`; two of a stage is the job's to refuse. A set belongs to
5169
- // no stage (#432): linked here, it runs the filters whose type fits the step's
5170
- // stage and skips the rest. A private set's body is the core's to judge
5172
+ // link or a private set (#339), in run order around the job's Response Format
5173
+ // (#615, `format`). A private set's body is the core's to judge
5171
5174
  // (filterSetBodyProblems); a link to a Library set is the server's to resolve,
5172
5175
  // so only its shape is checked here.
5173
5176
  STEP_TYPES.filters = {
5174
- label: "Filter sets", slot: "responses", rank: 2, many: true,
5177
+ label: "Filter sets", slot: "responses", rank: 2,
5175
5178
  description: "Cleans and gates the reply with linked or private filter sets.",
5176
5179
  in: "value", out: "value",
5177
5180
  apply: "runPipeline",
5178
- fields: ["type", "stage", "filterSets"],
5181
+ fields: ["type", "filterSets", "format"],
5179
5182
  validate(step, _ctx, bad, at){
5180
- if (step.stage !== "text" && step.stage !== "items") bad.push(`${at}: a filters step shapes the text or the items stage`);
5181
5183
  if (!Array.isArray(step.filterSets)) return void bad.push(`${at}: filterSets has to be a list`);
5182
5184
  for (const link of step.filterSets ) bad.push(...filterSetLinkProblems(link, at));
5185
+ bad.push(...formatProblems(step.format, step.filterSets.length, at));
5183
5186
  },
5184
5187
  };
5185
5188
 
@@ -5410,79 +5413,112 @@ function withOut(job , out ) {
5410
5413
  }
5411
5414
 
5412
5415
  /** [job] reading a reply as `legacyTagsOut` builds it, on the current model
5413
- (#405): its output a List, a Pre-format text set that reads the reply as
5414
- the old `preamble` did, and a Post-format items set that cleans and gates
5415
- it. The filters are `legacyTagsOut`'s, placed as private sets -- what the
5416
- version-15 upgrade builds for a legacy list job, for a test to build
5417
- directly. */
5416
+ (#405, #615): its output a List, and one filters list -- a private set
5417
+ above the format that reads the reply as the old `preamble` did, then the
5418
+ format, then a private set that cleans and gates its items. The filters
5419
+ are `legacyTagsOut`'s -- what the upgrade builds for a legacy list job, for
5420
+ a test to build directly. */
5418
5421
  function legacyTagsJob(job , rules , mode = "LOWER") {
5419
5422
  const legacy = legacyTagsOut(rules, mode);
5420
5423
  const items = ((legacy.modifiers[0]?.filters ) || []);
5421
- let j = withOut(job, { kind: "list", modifiers: [], parse: "csv" });
5422
- if (legacy.before.length) j = withFilterSets(j, [{ id: `filters-${j.id}-before`, set: null, pin: null, own: { version: FILTER_SET_VERSION, filters: legacy.before } }], "text");
5423
- if (items.length) j = withFilterSets(j, [{ id: `filters-${j.id}`, set: null, pin: null, own: { version: FILTER_SET_VERSION, filters: items } }], "items");
5424
- return j;
5425
- }
5426
-
5427
- /** A job's filters step for [stage], when it holds one (#405). */
5428
- const filterStepOf = (job , stage ) =>
5429
- (job?.steps || []).find((st) => isObj(st) && st.type === "filters" && st.stage === stage);
5430
-
5431
- /** The filter-set links a job's Responses stage carries for [stage], in
5432
- document order (#339, #405): read from that stage's filters step, or none.
5433
- Defaults to the Post-format (items) stage, the one that was there before. */
5434
- function filterSetsOf(job , stage = "items") {
5435
- return (filterStepOf(job, stage)?.filterSets ?? []).slice();
5436
- }
5437
-
5438
- /** [job] with its [stage] filter-set links set to [links]: that stage's
5439
- filters step added, replaced, or taken away when none are left. The other
5440
- stage's step is untouched. */
5441
- function withFilterSets(job , links , stage = "items") {
5442
- const steps = job.steps.filter(st => !(st.type === "filters" && st.stage === stage));
5443
- const next = links.length ? [...steps, { type: "filters" , stage, filterSets: [...links] }] : steps;
5444
- return { ...job, steps: ordered(next) };
5445
- }
5446
-
5447
- /** The filters a link resolves to, or none: a private (`own`) link its own
5448
- body, a Library link the body [resolve] gives (the worker's kept bodies),
5449
- and a Library link with no body nothing. */
5450
- const linkFilters = (link , resolve ) =>
5451
- (link.own ?? (link.set && resolve ? resolve(link.set) : null))?.filters || [];
5452
-
5453
- /** A job's Post-format (items) Responses as an ordered modifier list for a
5454
- run (#339): each modifier step as it is, and each items filter-set link as a
5455
- synthetic modifier that applies the set's filters where the link sits. The
5456
- Pre-format (text) filters are not modifiers -- `beforeFilters` gives
5457
- them, and a run applies them to the raw reply before it is read. [resolve]
5458
- gives a Library link's body. */
5424
+ const j = withOut(job, { kind: "list", modifiers: [], parse: "csv" });
5425
+ const above = legacy.before.length
5426
+ ? [{ id: `filters-${j.id}-before`, set: null, pin: null, own: { version: FILTER_SET_VERSION, filters: legacy.before } }] : [];
5427
+ const below = items.length
5428
+ ? [{ id: `filters-${j.id}`, set: null, pin: null, own: { version: FILTER_SET_VERSION, filters: items } }] : [];
5429
+ return withFilterSets(j, [...above, ...below], above.length);
5430
+ }
5431
+
5432
+ /** A job's filters step, when it holds one. */
5433
+ const filterStepOf = (job ) =>
5434
+ (job?.steps || []).find((st) => isObj(st) && st.type === "filters");
5435
+
5436
+ /** The filter-set links a job's Responses carries, in run order (#339,
5437
+ #615): read from its filters step, or none. */
5438
+ function filterSetsOf(job ) {
5439
+ return (filterStepOf(job)?.filterSets ?? []).slice();
5440
+ }
5441
+
5442
+ /** Where job's own Response Format sits among its links (#615): before
5443
+ `filterSetsOf(job)[n]`, or undefined where it holds no place of its own --
5444
+ a straddling set's marker, or the end, is where it reads. */
5445
+ function filterFormatOf(job ) {
5446
+ const at = filterStepOf(job)?.format;
5447
+ return Number.isInteger(at) ? at : undefined;
5448
+ }
5449
+
5450
+ /** [job] with its filter-set links set to [links], and its own Response
5451
+ Format at [format] among them (none: undefined). The filters step is
5452
+ added, replaced, or taken away when no link is left. */
5453
+ function withFilterSets(job , links , format ) {
5454
+ const steps = job.steps.filter(st => st.type !== "filters");
5455
+ const step = { type: "filters", filterSets: [...links], ...(format != null ? { format } : {}) };
5456
+ return { ...job, steps: ordered(links.length ? [...steps, step] : steps) };
5457
+ }
5458
+
5459
+ /** The body a link resolves to, or none: a private (`own`) link its own body,
5460
+ a Library link the body [resolve] gives (the worker's kept bodies). */
5461
+ const linkBody = (link , resolve ) =>
5462
+ link.own ?? (link.set && resolve ? resolve(link.set) ?? null : null);
5463
+
5464
+ /** Where job's Response Format reads among its links (#615), as the index
5465
+ of the link it reads in and the place among that link's filters: its own
5466
+ place (`format`) when it has one, else the first linked set whose body
5467
+ straddles the format (its `format`), else after every link. A format that
5468
+ reads text reads after every link. [resolve] gives a Library link's body. */
5469
+ function formatPlace(job , resolve ) {
5470
+ const links = filterSetsOf(job);
5471
+ if (!formatReadsItems(outOf(job)?.kind)) return { link: links.length, at: 0 };
5472
+ const own = filterFormatOf(job);
5473
+ if (own != null) return { link: Math.min(own, links.length), at: 0 };
5474
+ for (let i = 0; i < links.length; i++) {
5475
+ const at = linkBody(links[i] , resolve)?.format;
5476
+ if (Number.isInteger(at)) return { link: i, at: at };
5477
+ }
5478
+ return { link: links.length, at: 0 };
5479
+ }
5480
+
5481
+ /** A job's filters on either side of its Response Format (#615): `above`,
5482
+ run on the raw reply before it is read, and `below`, each link's share
5483
+ run on the items after it -- one list a link, so a filter that empties a
5484
+ list is named as a link's always was. */
5485
+ function filterSides(job , resolve ) {
5486
+ const links = filterSetsOf(job);
5487
+ const place = formatPlace(job, resolve);
5488
+ const above = [], below = [];
5489
+ links.forEach((link, i) => {
5490
+ const filters = linkBody(link, resolve)?.filters || [];
5491
+ if (i < place.link) above.push(...filters);
5492
+ else if (i > place.link) below.push(filters);
5493
+ else { above.push(...filters.slice(0, place.at)); below.push(filters.slice(place.at)); }
5494
+ });
5495
+ return { above, below };
5496
+ }
5497
+
5498
+ /** A job's Responses below its Response Format as an ordered modifier list
5499
+ for a run (#339, #615): each modifier step as it is, and each link's share
5500
+ below the format as a synthetic modifier that applies its filters to the
5501
+ items where the filters step sits. The filters above the format are not
5502
+ modifiers -- `beforeFilters` gives them, and a run applies them to the raw
5503
+ reply before it is read. [resolve] gives a Library link's body. */
5459
5504
  function stageModifiers(job , resolve ) {
5460
5505
  const out = [];
5461
5506
  for (const st of job?.steps || []) {
5462
5507
  if (!isObj(st)) continue;
5463
5508
  if (st.type === "modifier") out.push(st.modifier);
5464
- else if (st.type === "filters" && st.stage === "items") {
5465
- for (const link of (st.filterSets ) || []) out.push({ type: FILTER_MODIFIER, filters: linkFilters(link, resolve) });
5509
+ else if (st.type === "filters") {
5510
+ for (const filters of filterSides(job, resolve).below) out.push({ type: FILTER_MODIFIER, filters });
5466
5511
  }
5467
5512
  }
5468
5513
  return out;
5469
5514
  }
5470
5515
 
5471
- /** A job's Post-format filters for a run when its kind reads no items
5472
- (#557), in document order: every link's on its `items` filters step, which
5473
- a run applies to the text after the kind reads it. A kind that reads items
5474
- has none here -- `stageModifiers` runs its Post-format links. */
5475
- function afterFilters(job , resolve ) {
5476
- if (stageReads("items", outOf(job)?.kind) !== "text") return [];
5477
- return (filterStepOf(job, "items")?.filterSets || []).flatMap(link => linkFilters(link, resolve));
5478
- }
5479
-
5480
- /** A job's Pre-format (text) filters for a run (#405), in document order:
5481
- the filters of every link on its `text` filters step, which a run applies to
5482
- the raw reply before the kind reads it into items. [resolve] gives a Library
5516
+ /** A job's filters above its Response Format, for a run (#405, #615), in
5517
+ run order: a run applies them to the raw reply before the kind reads it.
5518
+ Every filter of a job whose format reads text. [resolve] gives a Library
5483
5519
  link's body, as `stageModifiers` does. */
5484
5520
  function beforeFilters(job , resolve ) {
5485
- return (filterStepOf(job, "text")?.filterSets || []).flatMap(link => linkFilters(link, resolve));
5521
+ return filterSides(job, resolve).above;
5486
5522
  }
5487
5523
 
5488
5524
  /** A new id: random, so one minted in one browser never collides with one
@@ -5528,11 +5564,6 @@ STEP_TYPES.job = {
5528
5564
  }
5529
5565
  const twice = steps.find((st, x) => !STEP_TYPES[st.type] .many && steps.findIndex(o => o.type === st.type) !== x);
5530
5566
  if (twice) return void bad.push(`${at} has two ${STEP_TYPES[twice.type] .label ?? twice.type} steps`);
5531
- // A `filters` step is `many`, but only one per stage: a Pre-format and
5532
- // a Post-format set at most (#405).
5533
- const twoFilters = steps.find((st, x) => st.type === "filters"
5534
- && steps.findIndex(o => o.type === "filters" && o.stage === st.stage) !== x);
5535
- if (twoFilters) return void bad.push(`${at} has two ${twoFilters.stage === "text" ? "Pre-format" : "Post-format"} filter steps`);
5536
5567
  if (steps.some((st, x) => x > 0 && placeOf(st) < placeOf(steps[x - 1]))) {
5537
5568
  return void bad.push(`${at}: steps are its Content, then its Responses, each in order`);
5538
5569
  }
@@ -5920,9 +5951,9 @@ function groupsOf(next ) {
5920
5951
  /** [doc] with each private group's body read as this version of a group
5921
5952
  body (`upgradeDatasetBody`): a version-8 one's `source` goes (#396); and
5922
5953
  each private filter set's as this version of a filter-set body
5923
- (`upgradeFilterSetBody`): a version-2 one's `stage` goes (#432). A body
5924
- carries its own version, so this runs on a current pipeline too; one whose
5925
- bodies are all current comes back as it was. */
5954
+ (`ownBodyV4`): its filters the verbs that read a reply as they did (#615).
5955
+ A body carries its own version, so this runs on a current pipeline too;
5956
+ one whose bodies are all current comes back as it was. */
5926
5957
  function ownBodiesNow(doc ) {
5927
5958
  const stale = (t ) => isObj(t) && isObj(t.own) && t.own.version !== DATASET_BODY_VERSION;
5928
5959
  const staleSet = (l ) => isObj(l) && isObj(l.own) && l.own.version !== FILTER_SET_VERSION;
@@ -5934,7 +5965,7 @@ function ownBodiesNow(doc ) {
5934
5965
  }
5935
5966
  if (Array.isArray(out.jobs) && out.jobs.some(staleJob)) {
5936
5967
  out = { ...out, jobs: out.jobs.map((j ) => (!staleJob(j) ? j : { ...j, steps: j.steps.map((st ) => (!staleStep(st) ? st
5937
- : { ...st, filterSets: st.filterSets.map((l ) => (staleSet(l) ? { ...l, own: upgradeFilterSetBody(l.own) } : l)) })) })) };
5968
+ : { ...st, filterSets: st.filterSets.map((l ) => (staleSet(l) ? { ...l, own: ownBodyV4(l.own) } : l)) })) })) };
5938
5969
  }
5939
5970
  return out;
5940
5971
  }
@@ -5952,10 +5983,14 @@ function upgradePipeline (doc , ctx = {}) {
5952
5983
  }
5953
5984
  return localSteps(ownBodiesNow(withoutLeftoverPreamble(out)), ctx) ;
5954
5985
  }
5955
- if (!isObj(doc) || ![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17].includes(doc.version )) return doc;
5986
+ if (!isObj(doc) || ![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18].includes(doc.version )) return doc;
5987
+ // Every version reads as 18 first, then as 19: a job's filters are one list
5988
+ // around its Response Format (oneFilterList).
5989
+ const nineteen = (d ) => localSteps(ownBodiesNow(oneFilterList(d)), ctx) ;
5990
+ if (doc.version === 18) return nineteen(withoutLeftoverPreamble(cutRefs(clone(doc) )));
5956
5991
  // Every version reads as 17 first, then as 18: a saved-mode Source content
5957
5992
  // pins one response (savedPinned).
5958
- const eighteen = (d ) => localSteps(ownBodiesNow(savedPinned(d, ctx)), ctx) ;
5993
+ const eighteen = (d ) => nineteen(savedPinned(d, ctx));
5959
5994
  if (doc.version === 17) return eighteen(cutRefs(clone(doc) ));
5960
5995
  // Every version reads as 16 first, then as 17: its requests read by their
5961
5996
  // job's HTTP API (requestsOnJobApi).
@@ -6026,14 +6061,16 @@ function filtersFromCleaning(next ) {
6026
6061
  if (isCleaning(st)) buffer = [...buffer, ...cleaningFilters((st ).modifier )];
6027
6062
  else if (isFilters(st)) {
6028
6063
  for (const link of ((st ).filterSets ) || []) {
6029
- if (link.own) buffer = [...buffer, ...(link.own.filters || [])];
6064
+ if (link.own) buffer = [...buffer, ...((ownBodyV4(link.own) ).filters || [])];
6030
6065
  else { flush(); links.push(link); }
6031
6066
  }
6032
6067
  }
6033
6068
  }
6034
6069
  flush();
6035
- const itemsStep = links.length ? { type: "filters", stage: "items", filterSets: links } : null;
6036
- const textStep = preamble
6070
+ // Version 18's two filters steps, a stage each, which oneFilterList reads
6071
+ // on the way to 19.
6072
+ const itemsStep = links.length ? { type: "filters", stage: "items", filterSets: links } : null;
6073
+ const textStep = preamble
6037
6074
  ? { type: "filters", stage: "text", filterSets: [{ id: `filters-${jid}-before`, set: null, pin: null, own: { version: FILTER_SET_VERSION, filters: preambleFilters() } }] }
6038
6075
  : null;
6039
6076
 
@@ -6052,6 +6089,48 @@ function filtersFromCleaning(next ) {
6052
6089
  return next;
6053
6090
  }
6054
6091
 
6092
+ /** Version 18 to 19 (#615): each job's Pre-format and Post-format filters
6093
+ steps become one, in run order -- the Pre-format links, then the
6094
+ Post-format ones -- and where the job's format reads items, its Response
6095
+ Format sits between them (`format`), so every filter runs where it did. A
6096
+ Library set linked on both steps is linked once, between them, and its
6097
+ body (now straddling the format, `upgradeFilterSetBody`) says where the
6098
+ format reads: the job holds no place of its own. A job whose format reads
6099
+ text runs every filter on its text, so it holds no place either. Each
6100
+ private set's body is read as version 4 (`ownBodyV4`). Every version
6101
+ before 19 ends here. */
6102
+ function oneFilterList(next ) {
6103
+ next.version = PIPELINE_VERSION;
6104
+ if (!Array.isArray(next.jobs)) return next;
6105
+ const isFilters = (st ) => isObj(st) && st.type === "filters";
6106
+ next.jobs = next.jobs.map((job ) => {
6107
+ if (!isObj(job) || !Array.isArray(job.steps) || !job.steps.some(isFilters)) return job;
6108
+ const steps = job.steps ;
6109
+ const linksOf = (stage ) => steps.filter(st => isFilters(st) && (st.stage ?? "items") === stage)
6110
+ .flatMap(st => (Array.isArray(st.filterSets) ? st.filterSets : []) )
6111
+ .map(l => (isObj(l) && l.own ? { ...l, own: ownBodyV4(l.own) } : l));
6112
+ const pre = linksOf("text"), post = linksOf("items");
6113
+ const setId = (l ) => (isObj(l) && isObj(l.set) ? l.set.id : null);
6114
+ const both = pre.find(l => setId(l) && post.some(o => setId(o) === setId(l) && o.pin === l.pin));
6115
+ let links , format ;
6116
+ if (!formatReadsItems(outOf(job )?.kind)) { links = [...pre, ...post]; format = undefined; }
6117
+ else if (both) {
6118
+ links = [...pre.filter(l => l !== both), both, ...post.filter(o => !(setId(o) === setId(both) && o.pin === both.pin))];
6119
+ format = undefined;
6120
+ } else { links = [...pre, ...post]; format = post.length ? pre.length : undefined; }
6121
+ const out = [];
6122
+ let placed = false;
6123
+ for (const st of steps) {
6124
+ if (!isFilters(st)) { out.push(st); continue; }
6125
+ if (placed || !links.length) continue;
6126
+ out.push({ type: "filters", filterSets: links, ...(format != null ? { format } : {}) });
6127
+ placed = true;
6128
+ }
6129
+ return { ...job, steps: out };
6130
+ });
6131
+ return next;
6132
+ }
6133
+
6055
6134
  /** [st] without its List `preamble` setting, when it is a Read Reply holding
6056
6135
  one; [st] itself otherwise. */
6057
6136
  function withoutPreamble(st ) {
@@ -7057,13 +7136,13 @@ function stagesFor(run , i , resolveFilters
7057
7136
  {
7058
7137
  const stages = run.jobs.map((ch, k) => {
7059
7138
  const { kind, modifiers: _mods, ...settings } = outOf(ch);
7060
- // The modifiers a run applies, with each Post-format filter set a
7061
- // synthetic modifier where its link sits (#339), and the Pre-format
7062
- // (text) filters applied to the raw reply before it is read (#405);
7139
+ // The modifiers a run applies, with the filters below the format a
7140
+ // synthetic modifier a link where the filters step sits (#339), and those
7141
+ // above it applied to the raw reply before it is read (#405, #615);
7063
7142
  // without a resolver, a Library link reads nothing, a private one its own.
7064
7143
  return { text: targetStepOf(run, i, k) .prompt, kind, withImage: sendsImage(ch),
7065
7144
  verbatim: !!targetEntryOf(run, i, k)?.verbatim, before: beforeFilters(ch, resolveFilters),
7066
- after: afterFilters(ch, resolveFilters), modifiers: stageModifiers(ch, resolveFilters), settings };
7145
+ modifiers: stageModifiers(ch, resolveFilters), settings };
7067
7146
  });
7068
7147
  // Each named by the connection whose key it is sent with.
7069
7148
  const asked = (key , held ) => {
@@ -7478,7 +7557,7 @@ CONNECTION_FIELDS, SETTING_KEYS, OPTION_FIELDS, jobLabel, scenarioLabel, jobWord
7478
7557
  contentOf, withContent, replyOf,
7479
7558
  targetsOf, targetLetter, targetLabel, targetStepOf, targetEntryOf, jobTargetType, targetAnswerOf, targetAnswerKey, recordedTargetOf, withTarget,
7480
7559
  tokensOf, withTokens, sendsImage, withImage, flowStepOf, requestStepOf, readAsOf, outOf, withOut, slotEntry, SLOTS,
7481
- filterSetsOf, withFilterSets, stageModifiers, beforeFilters, afterFilters, gradedText,
7560
+ filterSetsOf, filterFormatOf, withFilterSets, formatPlace, stageModifiers, beforeFilters, gradedText,
7482
7561
  blankPipeline, upgradePipeline, fatProfileRef, newId, newEval, newLink, validatePipeline, resolvePipeline, pipelineOfRun, stagesFor, profileIds, answerKeys,
7483
7562
  scenarioProfile, lastKind, lastModifier, modifierSummary, itemScores, scenarioEvals, scenarioPasses, groupPass, overallVerdict, passVerdict, isSkipped, failedScore,
7484
7563
  evalLabel, evalsOf, evalsDataset, casesRef, groupOf, groupOfMetrics, pipelineWarnings, itemsFound, previewOf, isWholeRun, EVAL_FIELDS,