@catheadowl/dsh-eval 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +6 -0
- package/README.md +59 -52
- package/README.zh.md +132 -0
- package/bin/dsh-eval.mjs +5 -5
- package/bin/dsh-review.mjs +1 -1
- package/docs/README.md +5 -2
- package/docs/disablerows.md +1 -1
- package/docs/experimental.md +35 -0
- package/docs/host-wiring.md +2 -2
- package/docs/matchers.md +15 -1
- package/docs/review.md +3 -3
- package/docs/rowconfig.md +39 -0
- package/docs/runner-api.md +42 -0
- package/package.json +13 -3
- package/src/adapters/dsh/review.mjs +157 -192
- package/src/cli.mjs +8 -8
- package/src/config.mjs +1 -1
- package/src/discovery.mjs +55 -4
- package/src/experimental.mjs +39 -0
- package/src/index.mjs +34 -49
- package/src/overlay.mjs +96 -0
- package/src/report.mjs +1 -1
- package/src/runner.mjs +216 -373
- package/src/sandbox.mjs +201 -0
- package/src/adapters/dsh/index.mjs +0 -7
package/package.json
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@catheadowl/dsh-eval",
|
|
3
3
|
"description": "Agent eval framework over dsh headless runs: case runner, session-trace assertions, and a scripted mock-LLM layer for plugin intent tests.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "src/index.mjs",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.mjs",
|
|
10
|
+
"./experimental": "./src/experimental.mjs",
|
|
11
|
+
"./bin/dsh-eval": "./bin/dsh-eval.mjs",
|
|
12
|
+
"./bin/dsh-review": "./bin/dsh-review.mjs",
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
8
15
|
"peerDependencies": {
|
|
9
16
|
"@deepseek-ai/dsh-llm": "*"
|
|
10
17
|
},
|
|
@@ -20,12 +27,15 @@
|
|
|
20
27
|
"src/",
|
|
21
28
|
"docs/",
|
|
22
29
|
"README.md",
|
|
30
|
+
"README.zh.md",
|
|
23
31
|
"LICENSE"
|
|
24
32
|
],
|
|
25
33
|
"scripts": {
|
|
26
34
|
"verify:publish": "node scripts/verify-publish-readiness.mjs",
|
|
27
35
|
"verify:face": "node scripts/verify-manifest-face.mjs",
|
|
28
|
-
"
|
|
29
|
-
"
|
|
36
|
+
"verify:experimental": "node scripts/verify-experimental-face.mjs",
|
|
37
|
+
"verify:readme-i18n": "node scripts/verify-readme-i18n.mjs",
|
|
38
|
+
"prepack": "node scripts/verify-manifest-face.mjs && node scripts/verify-publish-readiness.mjs && node scripts/verify-readme-i18n.mjs && node scripts/verify-experimental-face.mjs",
|
|
39
|
+
"test": "node --test --test-isolation=none tests/trace.test.mjs tests/assertions.test.mjs tests/overlay.test.mjs tests/staging.test.mjs tests/runner.test.mjs tests/cli.test.mjs tests/discovery.test.mjs tests/review.test.mjs tests/report.test.mjs tests/config.test.mjs tests/review-report.test.mjs && node scripts/verify-publish-readiness.mjs && node scripts/verify-manifest-face.mjs && node scripts/verify-readme-i18n.mjs && node scripts/verify-experimental-face.mjs"
|
|
30
40
|
}
|
|
31
41
|
}
|
|
@@ -1,192 +1,157 @@
|
|
|
1
|
-
/** dsh-headless execution adapter for model-independent review experiments. */
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from '
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
'tool-
|
|
30
|
-
'tool-
|
|
31
|
-
'tool-
|
|
32
|
-
'tool-
|
|
33
|
-
'tool-
|
|
34
|
-
'tool-
|
|
35
|
-
'tool-
|
|
36
|
-
'tool-
|
|
37
|
-
'tool-
|
|
38
|
-
'tool-
|
|
39
|
-
'tool-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
// disables every subagent tool row.
|
|
159
|
-
const trace = loadTraceDir(join(dshHome, 'sessions'))
|
|
160
|
-
if (trace) {
|
|
161
|
-
const validation = validateToolBoundary(trace, { allowedTools })
|
|
162
|
-
result.toolValidation = validation
|
|
163
|
-
if (!validation.ok) {
|
|
164
|
-
// Attach evidence for the caller to persist (the adapter's
|
|
165
|
-
// runDir is ephemeral — removed by the finally block). The
|
|
166
|
-
// bin writes this to `.runs/<id>/tool-boundary-evidence.json`.
|
|
167
|
-
result.toolBoundaryEvidence = renderToolBoundaryEvidence(validation, { runDir, profile })
|
|
168
|
-
const boundaryError = new Error(
|
|
169
|
-
`tool boundary violation: unexpected tools [${validation.unexpected.join(', ')}]`,
|
|
170
|
-
)
|
|
171
|
-
boundaryError.result = result
|
|
172
|
-
throw boundaryError
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return result
|
|
177
|
-
} finally {
|
|
178
|
-
if (process.env.DSH_REVIEW_KEEP_TMP !== '1') {
|
|
179
|
-
for (const junction of junctions) {
|
|
180
|
-
try { unlinkSync(junction) } catch { /* already absent */ }
|
|
181
|
-
}
|
|
182
|
-
rmSync(runDir, { recursive: true, force: true })
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/** Run a review experiment through fresh dsh headless processes. */
|
|
189
|
-
export async function runDshReviewExperiment(experiment, options) {
|
|
190
|
-
const executor = createDshHeadlessReviewExecutor(options)
|
|
191
|
-
return executeReviewExperiment(experiment, executor, { runs: options.runs })
|
|
192
|
-
}
|
|
1
|
+
/** dsh-headless execution adapter for model-independent review experiments. */
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdtempSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join, resolve } from 'node:path'
|
|
6
|
+
import { executeReviewExperiment } from '../../experiment/review.mjs'
|
|
7
|
+
import { CLI_RELATIVE_PATH } from '../../cli.mjs'
|
|
8
|
+
import { overlayDisableRows } from '../../overlay.mjs'
|
|
9
|
+
import {
|
|
10
|
+
resolveRealDshHome, stageSandboxHome, teardownSandbox, spawnHeadlessDsh,
|
|
11
|
+
} from '../../sandbox.mjs'
|
|
12
|
+
import { loadTraceDir } from '../../trace.mjs'
|
|
13
|
+
import { validateToolBoundary, renderToolBoundaryEvidence } from '../../tool-validation.mjs'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Model-facing tool rows every shipped dsh profile mounts from `dsh-base`.
|
|
17
|
+
* The overlay disables all host tool rows as a supplementary guard; the
|
|
18
|
+
* primary isolation comes from the sterile review profile (dsh-base +
|
|
19
|
+
* dsh-headless only, no out-of-tree plugins). Post-run tool boundary
|
|
20
|
+
* validation (see `validateToolBoundary`) detects any residual drift.
|
|
21
|
+
*/
|
|
22
|
+
const REVIEW_DISABLED_TOOL_ROWS = [
|
|
23
|
+
'tool-bash',
|
|
24
|
+
'tool-pwsh',
|
|
25
|
+
'tool-jobs',
|
|
26
|
+
'tool-fs',
|
|
27
|
+
'tool-fs-search',
|
|
28
|
+
'tool-skill',
|
|
29
|
+
'tool-todo',
|
|
30
|
+
'tool-goal',
|
|
31
|
+
'tool-ralph',
|
|
32
|
+
'tool-str-replace-editor',
|
|
33
|
+
'tool-web',
|
|
34
|
+
'tool-subagent-control',
|
|
35
|
+
'tool-subagent-list-agents',
|
|
36
|
+
'tool-subagent',
|
|
37
|
+
'tool-subagent-fork',
|
|
38
|
+
'tool-subagent-report',
|
|
39
|
+
'tool-workflow',
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
/** Serialize the tool-less overlay: disable every host model-facing tool row. */
|
|
43
|
+
function buildReviewOverlayYaml() {
|
|
44
|
+
return overlayDisableRows(REVIEW_DISABLED_TOOL_ROWS)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Resolve and validate the compiled dsh CLI entry point. */
|
|
48
|
+
export function resolveDshCli(dshRepoDir) {
|
|
49
|
+
const repoDir = resolve(dshRepoDir)
|
|
50
|
+
const cli = join(repoDir, ...CLI_RELATIVE_PATH.split(/[\\/]/))
|
|
51
|
+
if (!existsSync(cli)) {
|
|
52
|
+
throw new Error(`no compiled dsh CLI at '${cli}' (build deepseek-harness first)`)
|
|
53
|
+
}
|
|
54
|
+
return cli
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The CLI entry for an executor: explicit cliPath (C6 chain result) wins;
|
|
58
|
+
* otherwise fall back to the legacy repo form. Neither being set is a caller
|
|
59
|
+
* bug the CLI bins already catch — this guard serves direct API consumers. */
|
|
60
|
+
function executorCli(options) {
|
|
61
|
+
if (options.cliPath !== undefined) return resolve(options.cliPath)
|
|
62
|
+
if (options.dshRepoDir !== undefined) return resolveDshCli(options.dshRepoDir)
|
|
63
|
+
throw new Error('review adapter needs a CLI location: pass cliPath (C6 chain result) or dshRepoDir')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Create an executor compatible with executeReviewExperiment.
|
|
68
|
+
*
|
|
69
|
+
* The executor boots a sterile review profile (default: the host's
|
|
70
|
+
* `headless` template, bundles = dsh-base + dsh-headless, no out-of-tree
|
|
71
|
+
* plugins) in an isolated DSH_HOME (staging/teardown mechanics shared with
|
|
72
|
+
* the behavior runner via sandbox.mjs), disables every host tool row via
|
|
73
|
+
* overlay, and validates the tool boundary after the run. `options.profile`
|
|
74
|
+
* must name a profile whose installed plugin set is empty or review-safe.
|
|
75
|
+
*
|
|
76
|
+
* @param {object} options
|
|
77
|
+
* @param {string} [options.profile='headless'] - the sterile review profile.
|
|
78
|
+
* @param {Set<string>} [options.allowedTools] - tool names permitted in the reviewer's session (default: empty).
|
|
79
|
+
*/
|
|
80
|
+
export function createDshHeadlessReviewExecutor(options) {
|
|
81
|
+
const cli = executorCli(options)
|
|
82
|
+
const profile = options.profile ?? 'headless'
|
|
83
|
+
if (typeof profile !== 'string' || profile.length === 0) {
|
|
84
|
+
throw new TypeError('dsh review adapter requires a profile')
|
|
85
|
+
}
|
|
86
|
+
const timeoutMs = options.timeoutMs ?? 300_000
|
|
87
|
+
const allowedTools = options.allowedTools ?? new Set()
|
|
88
|
+
|
|
89
|
+
return async function executeWithDsh(task) {
|
|
90
|
+
// A fresh process alone is not enough: dsh also stores settings, titles,
|
|
91
|
+
// and sessions below DSH_HOME. Reuse the behavior harness's sandbox
|
|
92
|
+
// (sandbox.mjs) so every reviewer receives an isolated runtime state
|
|
93
|
+
// while retaining the selected profile's model config and plugin links.
|
|
94
|
+
const runDir = mkdtempSync(join(tmpdir(), 'dsh-review-'))
|
|
95
|
+
const dshHome = join(runDir, 'dsh-home')
|
|
96
|
+
const overlayPath = join(runDir, 'review-overlay.yml')
|
|
97
|
+
writeFileSync(overlayPath, buildReviewOverlayYaml(), 'utf8')
|
|
98
|
+
stageSandboxHome(options.dshHome ?? resolveRealDshHome(), dshHome, profile)
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const { stdout, stderr, exitCode, timedOut } = await spawnHeadlessDsh({
|
|
102
|
+
cli,
|
|
103
|
+
cliArgs: ['--profile', profile, '--patch', overlayPath, task],
|
|
104
|
+
cwd: options.cwd ?? runDir,
|
|
105
|
+
env: {
|
|
106
|
+
...process.env,
|
|
107
|
+
...options.env,
|
|
108
|
+
DSH_HOME: dshHome,
|
|
109
|
+
DSH_TELEMETRY_DISABLED: '1',
|
|
110
|
+
},
|
|
111
|
+
timeoutMs,
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const result = { stdout, stderr, exitCode, timedOut, profile, cli, runDir }
|
|
115
|
+
if (exitCode !== 0 || timedOut) {
|
|
116
|
+
const error = new Error(`dsh reviewer exited with code ${exitCode}${timedOut ? ' after timeout' : ''}`)
|
|
117
|
+
error.result = result
|
|
118
|
+
throw error
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Post-run tool boundary check: parse the session
|
|
122
|
+
// trace, verify no unexpected tools were mounted in the reviewer's
|
|
123
|
+
// session, fail the run on violation. An absent session log skips
|
|
124
|
+
// the check gracefully (accepted fail-open).
|
|
125
|
+
// Validation inspects the main session only (buildTrace selects
|
|
126
|
+
// non-subagent logs); plugin tools leaking in a subagent session
|
|
127
|
+
// would not be caught — irrelevant in review where the overlay
|
|
128
|
+
// disables every subagent tool row.
|
|
129
|
+
const trace = loadTraceDir(join(dshHome, 'sessions'))
|
|
130
|
+
if (trace) {
|
|
131
|
+
const validation = validateToolBoundary(trace, { allowedTools })
|
|
132
|
+
result.toolValidation = validation
|
|
133
|
+
if (!validation.ok) {
|
|
134
|
+
// Attach evidence for the caller to persist (the adapter's
|
|
135
|
+
// runDir is ephemeral — removed by the finally block). The
|
|
136
|
+
// bin writes this to `.runs/<id>/tool-boundary-evidence.json`.
|
|
137
|
+
result.toolBoundaryEvidence = renderToolBoundaryEvidence(validation, { runDir, profile })
|
|
138
|
+
const boundaryError = new Error(
|
|
139
|
+
`tool boundary violation: unexpected tools [${validation.unexpected.join(', ')}]`,
|
|
140
|
+
)
|
|
141
|
+
boundaryError.result = result
|
|
142
|
+
throw boundaryError
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return result
|
|
147
|
+
} finally {
|
|
148
|
+
teardownSandbox(runDir, { keep: process.env.DSH_REVIEW_KEEP_TMP === '1' })
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Run a review experiment through fresh dsh headless processes. */
|
|
154
|
+
export async function runDshReviewExperiment(experiment, options) {
|
|
155
|
+
const executor = createDshHeadlessReviewExecutor(options)
|
|
156
|
+
return executeReviewExperiment(experiment, executor, { runs: options.runs })
|
|
157
|
+
}
|
package/src/cli.mjs
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Host CLI resolution chain
|
|
2
|
+
* Host CLI resolution chain.
|
|
3
3
|
*
|
|
4
4
|
* Locating the compiled dsh CLI follows the same two-layer model as package
|
|
5
5
|
* imports: committed files carry no real host-checkout path — the machine's
|
|
6
|
-
* resolution layer (node_modules
|
|
7
|
-
* absorbs it. Precedence, first hit wins:
|
|
6
|
+
* resolution layer (node_modules) absorbs it. Precedence, first hit wins:
|
|
8
7
|
*
|
|
9
8
|
* 1. explicit `--repo <dir>` flag — the documented escape hatch;
|
|
10
9
|
* 2. resolution layer — `node_modules/@deepseek-ai/dsh/lib/bin.js`
|
|
11
10
|
* (the CLI package's own bin target) reachable upward from startDir;
|
|
12
11
|
* 3. config `repo` key (legacy) — kept working for existing checked-in
|
|
13
|
-
* `dsh-eval.config.mjs` files
|
|
12
|
+
* `dsh-eval.config.mjs` files.
|
|
14
13
|
*
|
|
15
14
|
* Every miss fails loud with a fingerprint and placeholder-only guidance —
|
|
16
15
|
* no machine-specific example paths, no silent fallback to guessing.
|
|
@@ -29,10 +28,11 @@ const PACKAGED_CLI_PATH = join('node_modules', '@deepseek-ai', 'dsh', 'lib', 'bi
|
|
|
29
28
|
export const NO_CLI_GUIDANCE = [
|
|
30
29
|
'no dsh CLI found. In order:',
|
|
31
30
|
" 1) pass --repo <host-checkout> explicitly;",
|
|
32
|
-
' 2) or
|
|
33
|
-
'
|
|
34
|
-
'
|
|
35
|
-
'
|
|
31
|
+
' 2) or provide the CLI through the resolution layer:',
|
|
32
|
+
' node_modules/@deepseek-ai/dsh/lib/bin.js — install the @deepseek-ai/dsh',
|
|
33
|
+
' package (or link a built deepseek-harness checkout into node_modules',
|
|
34
|
+
' and build it so lib/bin.js exists);',
|
|
35
|
+
' 3) or set repo in dsh-eval.config.mjs (legacy).',
|
|
36
36
|
].join('\n')
|
|
37
37
|
|
|
38
38
|
/** Validate a repo-style candidate: return the CLI path or undefined. */
|
package/src/config.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared `dsh-eval.config.mjs` discovery and loading
|
|
2
|
+
* Shared `dsh-eval.config.mjs` discovery and loading.
|
|
3
3
|
*
|
|
4
4
|
* Both CLIs (`dsh-eval`, `dsh-review`) repeat `--profile/--repo` wiring in
|
|
5
5
|
* every consumer's package scripts. A per-package config file removes that
|
package/src/discovery.mjs
CHANGED
|
@@ -39,6 +39,23 @@ export function discoverFiles(path, suffix, out = []) {
|
|
|
39
39
|
return out
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Validate a `disableRows` declaration (case-level or ad-hoc): an array of
|
|
44
|
+
* non-empty loader row id strings. An EMPTY array is legal and means
|
|
45
|
+
* "disable nothing, explicitly" — it overrides a `disableRows` default
|
|
46
|
+
* from `dsh-eval.config.mjs`. Throws with `label` context. Shared by
|
|
47
|
+
* `validateEvalCase` (load time) and `runEvalCase` (execution time) so both
|
|
48
|
+
* report the identical message.
|
|
49
|
+
* @param {unknown} disableRows - the value to validate.
|
|
50
|
+
* @param {string} label - error-message context (e.g. `case '<id>'`).
|
|
51
|
+
*/
|
|
52
|
+
export function validateDisableRows(disableRows, label) {
|
|
53
|
+
if (!Array.isArray(disableRows)
|
|
54
|
+
|| disableRows.some(row => typeof row !== 'string' || row === '')) {
|
|
55
|
+
throw new Error(`${label}: disableRows must be a string[] of loader row ids (empty = explicit none, overriding config; got '${JSON.stringify(disableRows)}')`)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
42
59
|
/**
|
|
43
60
|
* Validate one eval case's shape. Throws with a descriptive message on
|
|
44
61
|
* the first violation found. Checks:
|
|
@@ -51,6 +68,10 @@ export function discoverFiles(path, suffix, out = []) {
|
|
|
51
68
|
* "disable nothing, explicitly" — it overrides a `disableRows` default
|
|
52
69
|
* from `dsh-eval.config.mjs`, which is how gate-interaction cases opt
|
|
53
70
|
* back in inside a package that disables the gate row by default.
|
|
71
|
+
* - `rowConfig` (if present) maps loader row ids to config objects whose
|
|
72
|
+
* leaf values are scalars or arrays of scalars (see `validateRowConfig`).
|
|
73
|
+
* The overlay REPLACES the row's whole config — restate any keys the row
|
|
74
|
+
* needs, not just the ones being changed.
|
|
54
75
|
* - `expect` is an array; every element has `describe` (string) and `check` (function).
|
|
55
76
|
* - mock mode requires a `script` with `steps` array.
|
|
56
77
|
*
|
|
@@ -74,10 +95,10 @@ export function validateEvalCase(evalCase, file) {
|
|
|
74
95
|
throw new Error(`${file}: case '${evalCase.id}': the 'gates' field was removed — declare disableRows: ['gates'] instead`)
|
|
75
96
|
}
|
|
76
97
|
if (evalCase.disableRows !== undefined) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
98
|
+
validateDisableRows(evalCase.disableRows, `${file}: case '${evalCase.id}'`)
|
|
99
|
+
}
|
|
100
|
+
if (evalCase.rowConfig !== undefined) {
|
|
101
|
+
validateRowConfig(evalCase.rowConfig, `case '${evalCase.id}'`)
|
|
81
102
|
}
|
|
82
103
|
if (!Array.isArray(evalCase.expect)) {
|
|
83
104
|
throw new Error(`${file}: case '${evalCase.id}': expect must be a Matcher[]`)
|
|
@@ -95,6 +116,36 @@ export function validateEvalCase(evalCase, file) {
|
|
|
95
116
|
}
|
|
96
117
|
}
|
|
97
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Validate a `rowConfig` mapping (case-level or ad-hoc): keys are loader row
|
|
121
|
+
* ids, values are config objects whose leaf values must be scalars (string /
|
|
122
|
+
* number / boolean) or arrays of scalars. Nested objects are rejected — the
|
|
123
|
+
* overlay emitter only handles flat config keys. Throws with `label` context.
|
|
124
|
+
* @param {unknown} rowConfig - the value to validate.
|
|
125
|
+
* @param {string} label - error-message context (e.g. `case '<id>'`).
|
|
126
|
+
*/
|
|
127
|
+
export function validateRowConfig(rowConfig, label) {
|
|
128
|
+
if (rowConfig === null || typeof rowConfig !== 'object' || Array.isArray(rowConfig)) {
|
|
129
|
+
throw new Error(`${label}: rowConfig must be an object mapping row ids to config objects (got '${JSON.stringify(rowConfig)}')`)
|
|
130
|
+
}
|
|
131
|
+
for (const [rowId, config] of Object.entries(rowConfig)) {
|
|
132
|
+
if (rowId === '') throw new Error(`${label}: rowConfig row id must be a non-empty string`)
|
|
133
|
+
if (config === null || typeof config !== 'object' || Array.isArray(config)) {
|
|
134
|
+
throw new Error(`${label}: rowConfig['${rowId}'] must be a config object (got '${JSON.stringify(config)}')`)
|
|
135
|
+
}
|
|
136
|
+
for (const [key, value] of Object.entries(config)) {
|
|
137
|
+
if (key === '') throw new Error(`${label}: rowConfig['${rowId}'] has an empty config key`)
|
|
138
|
+
if (Array.isArray(value)) {
|
|
139
|
+
if (value.some(item => item === null || typeof item === 'object')) {
|
|
140
|
+
throw new Error(`${label}: rowConfig['${rowId}']['${key}'] must be an array of scalars`)
|
|
141
|
+
}
|
|
142
|
+
} else if (value === null || typeof value === 'object') {
|
|
143
|
+
throw new Error(`${label}: rowConfig['${rowId}']['${key}'] must be a scalar or scalar array (nested objects are not supported)`)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
98
149
|
/**
|
|
99
150
|
* Detect duplicate case ids across a flat case list. Throws on the first
|
|
100
151
|
* duplicate found, naming both source files.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escape-hatch entry: mechanism primitives for ad-hoc diagnostic scripts and
|
|
3
|
+
* self-built execution faces. Reachable as `<pkg>/experimental`.
|
|
4
|
+
*
|
|
5
|
+
* Contract: NO compatibility promise — symbols here may change or move in
|
|
6
|
+
* any minor release. The stable surface for eval/review case authors is the
|
|
7
|
+
* package root entry (src/index.mjs); import from here only when you are
|
|
8
|
+
* building your own runner/driver and accept the follow-up cost.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// --- host CLI resolution (modern three-segment chain) ---
|
|
12
|
+
export { resolveDshCliChain } from './cli.mjs'
|
|
13
|
+
|
|
14
|
+
// --- sandbox / overlay mechanism ---
|
|
15
|
+
export { stageProfileStore } from './sandbox.mjs'
|
|
16
|
+
export { buildOverlayYaml, overlayDisableRows } from './overlay.mjs'
|
|
17
|
+
|
|
18
|
+
// --- session-trace primitives ---
|
|
19
|
+
export { parseSessionLog, buildTrace, loadTraceDir } from './trace.mjs'
|
|
20
|
+
|
|
21
|
+
// --- review experiment execution layer ---
|
|
22
|
+
export {
|
|
23
|
+
executeReviewExperiment,
|
|
24
|
+
materializeReviewExperiment,
|
|
25
|
+
renderObservationSections,
|
|
26
|
+
OBSERVATIONS_PLACEHOLDER,
|
|
27
|
+
} from './experiment/review.mjs'
|
|
28
|
+
|
|
29
|
+
// --- dsh review executors ---
|
|
30
|
+
export {
|
|
31
|
+
createDshHeadlessReviewExecutor,
|
|
32
|
+
runDshReviewExperiment,
|
|
33
|
+
} from './adapters/dsh/review.mjs'
|
|
34
|
+
|
|
35
|
+
// --- tool-boundary validation ---
|
|
36
|
+
export {
|
|
37
|
+
validateToolBoundary,
|
|
38
|
+
renderToolBoundaryEvidence,
|
|
39
|
+
} from './tool-validation.mjs'
|