@catheadowl/dsh-eval 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Overlay (cordis patch) serialization — the ONLY hand-rolled YAML emitter
3
+ * in the package. Both the behavior runner (buildOverlayYaml) and the dsh
4
+ * review adapter (overlayDisableRows for its tool-less overlay) generate
5
+ * per-run overlay files; sharing one emitter keeps quoting rules and
6
+ * row-patch syntax (`- id: <row>` / `disabled: true`) identical everywhere.
7
+ *
8
+ * YAML strategy: JSON double-quoted strings are valid YAML scalars, so the
9
+ * emitters lean on JSON.stringify — zero dependencies, identical quoting
10
+ * across scalar and array leaves.
11
+ */
12
+
13
+ import { join } from 'node:path'
14
+ import { fileURLToPath, pathToFileURL } from 'node:url'
15
+
16
+ /** This framework's root directory (the eval package dir). */
17
+ const FRAMEWORK_ROOT = fileURLToPath(new URL('..', import.meta.url))
18
+
19
+ /** The scripted mock adapter plugin, referenced from generated overlays. */
20
+ const MOCK_ADAPTER_PATH = join(FRAMEWORK_ROOT, 'src', 'mock', 'mock-adapter.mjs')
21
+
22
+ /** The multi-turn driver plugin, mounted when a case declares `followups`. */
23
+ const MULTI_TURN_DRIVER_PATH = join(FRAMEWORK_ROOT, 'src', 'driver', 'multi-turn-driver.mjs')
24
+
25
+ /** JSON double-quoted strings are valid YAML scalars — enough for this emitter. */
26
+ function yamlScalar(value) {
27
+ if (typeof value === 'boolean' || typeof value === 'number') return String(value)
28
+ return JSON.stringify(String(value))
29
+ }
30
+
31
+ /**
32
+ * One rowConfig leaf: a scalar, or an array of scalars emitted as a YAML flow
33
+ * sequence (a JSON array is valid YAML flow syntax, and keeps quoting rules
34
+ * identical to `yamlScalar`).
35
+ */
36
+ function yamlConfigValue(value) {
37
+ if (Array.isArray(value)) return JSON.stringify(value.map(item => typeof item === 'string' ? item : String(item)))
38
+ return yamlScalar(value)
39
+ }
40
+
41
+ /**
42
+ * Serialize `- id: <row> / disabled: true` patches for every row id — the
43
+ * cross-layer row-disable mechanism shared by case `disableRows` and the
44
+ * review adapter's host-tool lockdown.
45
+ * @param {string[]} rowIds - loader row ids to disable.
46
+ * @returns {string} overlay entries (each line-terminated).
47
+ */
48
+ export function overlayDisableRows(rowIds) {
49
+ return rowIds.map(rowId => `- id: ${yamlScalar(rowId)}\n disabled: true\n`).join('')
50
+ }
51
+
52
+ /**
53
+ * Serialize the per-run overlay patch list for one eval case.
54
+ * @param {object} parts - overlay ingredients (see runEvalCase):
55
+ * `sessionsRoot` (required), optional `persona`, `disableRows`,
56
+ * `rowConfig`, and `mock` (mount the scripted adapter + re-point the
57
+ * default model).
58
+ * @returns {string} the overlay file text.
59
+ */
60
+ export function buildOverlayYaml(parts) {
61
+ const lines = []
62
+ lines.push('- id: session-persistence-jsonl')
63
+ lines.push(' config:')
64
+ lines.push(` root: ${yamlScalar(parts.sessionsRoot)}`)
65
+ lines.push(' packChunks: false')
66
+ lines.push(' compression: none')
67
+ if (parts.persona !== undefined) {
68
+ lines.push('- id: system-prompt')
69
+ lines.push(' config:')
70
+ lines.push(` persona: ${yamlScalar(parts.persona)}`)
71
+ }
72
+ if (parts.disableRows !== undefined && parts.disableRows.length > 0) {
73
+ lines.push(overlayDisableRows(parts.disableRows).trimEnd())
74
+ }
75
+ for (const [rowId, config] of Object.entries(parts.rowConfig ?? {})) {
76
+ // Whole-replace semantics: these config keys REPLACE the row's config
77
+ // (cordis patch layer), so the emitter adds to a fresh `- id:` entry —
78
+ // restating keys is the declaring case's responsibility.
79
+ lines.push(`- id: ${yamlScalar(rowId)}`)
80
+ lines.push(' config:')
81
+ for (const [key, value] of Object.entries(config)) {
82
+ lines.push(` ${key}: ${yamlConfigValue(value)}`)
83
+ }
84
+ }
85
+ if (parts.mock) {
86
+ lines.push('- id: agent-default-model')
87
+ lines.push(' config:')
88
+ lines.push(' provider: eval-mock')
89
+ lines.push(' model: eval-mock')
90
+ // The title generator also calls the default provider and would consume
91
+ // script steps; deterministic runs own every model call themselves.
92
+ lines.push('- id: session-title-llm')
93
+ lines.push(' disabled: true')
94
+ lines.push('- insert:')
95
+ lines.push(' - id: eval-mock-llm')
96
+ lines.push(` name: ${yamlScalar(pathToFileURL(MOCK_ADAPTER_PATH).href)}`)
97
+ }
98
+ if (parts.followups !== undefined) {
99
+ // Cross-turn driving replaces the one-shot headless runner: it exits at
100
+ // the FIRST idle and aborts every in-process background subagent at
101
+ // teardown, so fire-and-forget children need the driver's longer lifetime.
102
+ lines.push('- id: headless-runner')
103
+ lines.push(' disabled: true')
104
+ lines.push('- insert:')
105
+ lines.push(' - id: eval-multi-turn-driver')
106
+ lines.push(` name: ${yamlScalar(pathToFileURL(MULTI_TURN_DRIVER_PATH).href)}`)
107
+ }
108
+ return `${lines.join('\n')}\n`
109
+ }
package/src/report.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Machine-readable run reports for the behavior CLI (EVAL-007).
2
+ * Machine-readable run reports for the behavior CLI.
3
3
  *
4
4
  * One report covers ONE `dsh-eval run` invocation: the selection summary,
5
5
  * per-case outcomes, and the invocation's environment anchors (profile /
@@ -68,7 +68,20 @@ export function renderReviewReport(parts) {
68
68
  if (attempt.ok) {
69
69
  lines.push(`### run ${attempt.index} — ok`)
70
70
  lines.push('')
71
- lines.push('```text', attempt.result.stdout.trimEnd(), '```')
71
+ // The answer, not the raw final message: adapters derive an
72
+ // answer that survives tail interactions (gate splices); stdout
73
+ // is the fallback for executors without trace-derived answers.
74
+ lines.push('```text', (attempt.result?.answer ?? attempt.result?.stdout ?? '').trimEnd(), '```')
75
+ // Traceability: the grader can always recover the full conversation
76
+ // from the persisted per-run artifacts. The divergence note matches
77
+ // the bin's write predicate exactly (both fields defined and
78
+ // differing) — a stdout-less executor writes no run-N.stdout.txt,
79
+ // so it must not be cited either.
80
+ const diverged = attempt.result?.answer !== undefined
81
+ && attempt.result?.stdout !== undefined
82
+ && attempt.result.answer !== attempt.result.stdout
83
+ lines.push('')
84
+ lines.push(`- transcript: \`run-${attempt.index}.stderr.txt\`${diverged ? ` (answer differs from the final message — see \`run-${attempt.index}.stdout.txt\`)` : ''}`)
72
85
  } else {
73
86
  lines.push(`### run ${attempt.index} — FAIL`)
74
87
  lines.push('')