dsh-harbor-evolution 0.4.0 → 0.6.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.md +15 -7
- package/index.js +109 -2
- package/lib/candidate.js +29 -2
- package/lib/client.js +767 -260
- package/lib/dashboard.js +644 -67
- package/lib/evolution.js +164 -26
- package/lib/process.js +2 -1
- package/lib/service.js +137 -2
- package/lib/web.js +76 -15
- package/package.json +7 -1
- package/schemas/evaluation-result.schema.json +27 -0
- package/schemas/evaluator-observations.schema.json +51 -0
- package/schemas/ground-truth.schema.json +61 -0
- package/schemas/meta-evaluation-report.schema.json +23 -0
- package/skills/evolve-agent-with-harbor/SKILL.md +149 -78
- package/skills/evolve-agent-with-harbor/references/evaluator-upgrade.md +61 -0
- package/skills/evolve-agent-with-harbor/references/initialization.md +100 -81
package/lib/evolution.js
CHANGED
|
@@ -23,47 +23,194 @@ export async function snapshot(config, args) {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
function slug(value) {
|
|
26
|
-
return String(value)
|
|
27
|
-
.toLowerCase()
|
|
28
|
-
.replace(/[^a-z0-9._-]+/g, '-')
|
|
29
|
-
.replace(/^[._-]+|[._-]+$/g, '') || 'candidate'
|
|
26
|
+
return String(value).toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[._-]+|[._-]+$/g, '') || 'candidate'
|
|
30
27
|
}
|
|
31
28
|
|
|
32
29
|
export function makeJobName(manifest, now = new Date()) {
|
|
33
30
|
const timestamp = now.toISOString().replace(/\.\d{3}Z$/, 'Z').replace(/[-:]/g, '')
|
|
34
31
|
const suffix = `${timestamp}-${manifest.digest.slice(7, 15)}`
|
|
35
|
-
const
|
|
36
|
-
const identity = `${slug(manifest.candidate_id)}-${slug(manifest.version)}`.slice(0, available)
|
|
32
|
+
const identity = `${slug(manifest.candidate_id)}-${slug(manifest.version)}`.slice(0, 99 - suffix.length)
|
|
37
33
|
return `${identity}-${suffix}`
|
|
38
34
|
}
|
|
39
35
|
|
|
36
|
+
async function cliJson(config, args, { allowedExitCodes = [0], input } = {}) {
|
|
37
|
+
let result
|
|
38
|
+
try {
|
|
39
|
+
result = await runProcess(config.harborDshBin, args, {
|
|
40
|
+
cwd: config.projectRoot,
|
|
41
|
+
timeoutMs: config.timeoutMs,
|
|
42
|
+
allowedExitCodes,
|
|
43
|
+
input,
|
|
44
|
+
env: { ...process.env, ...(config.pythonPath ? { PYTHONPATH: config.pythonPath } : {}) },
|
|
45
|
+
})
|
|
46
|
+
} catch (error) {
|
|
47
|
+
const detail = error?.result?.stderr?.trim().split('\n').at(-1)?.replace(/^[A-Za-z]+Error:\s*/, '')
|
|
48
|
+
throw new Error(detail || error.message)
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(result.stdout)
|
|
52
|
+
} catch {
|
|
53
|
+
throw new Error(`harbor-dsh returned invalid JSON for ${args.slice(0, 2).join(' ')}`)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function inspectEvaluator(config, args = {}) {
|
|
58
|
+
const stack = resolveWithin(config.projectRoot, args.stackPath ?? '.harbor/evaluation-stack.yml', 'stackPath')
|
|
59
|
+
return cliJson(config, [
|
|
60
|
+
'evaluator', 'inspect',
|
|
61
|
+
'--project-root', config.projectRoot,
|
|
62
|
+
'--stack', stack,
|
|
63
|
+
])
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function updateEvaluator(config, args) {
|
|
67
|
+
const stack = resolveWithin(config.projectRoot, args.stackPath ?? '.harbor/evaluation-stack.yml', 'stackPath')
|
|
68
|
+
if (typeof args.content !== 'string') throw new Error('content is required')
|
|
69
|
+
return cliJson(config, [
|
|
70
|
+
'evaluator', 'update',
|
|
71
|
+
'--project-root', config.projectRoot,
|
|
72
|
+
'--stack', stack,
|
|
73
|
+
'--file', String(args.filePath ?? ''),
|
|
74
|
+
'--expected-digest', String(args.expectedDigest ?? ''),
|
|
75
|
+
'--new-evaluator-version', String(args.newEvaluatorVersion ?? ''),
|
|
76
|
+
'--new-stack-version', String(args.newStackVersion ?? ''),
|
|
77
|
+
'--content-stdin',
|
|
78
|
+
], { input: args.content })
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function initializeGroundTruth(config, args) {
|
|
82
|
+
const output = resolveWithin(config.projectRoot, args.outputPath ?? '.harbor/ground-truth.json', 'outputPath')
|
|
83
|
+
return cliJson(config, [
|
|
84
|
+
'ground-truth', 'init',
|
|
85
|
+
'--project-root', config.projectRoot,
|
|
86
|
+
'--output', output,
|
|
87
|
+
'--id', String(args.groundTruthId ?? ''),
|
|
88
|
+
'--version', String(args.version ?? ''),
|
|
89
|
+
'--source-kind', String(args.sourceKind ?? ''),
|
|
90
|
+
'--source-description', String(args.sourceDescription ?? ''),
|
|
91
|
+
'--provenance', String(args.provenance ?? ''),
|
|
92
|
+
'--criteria', String(args.criteria ?? ''),
|
|
93
|
+
])
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function runMetaEvaluation(config, args) {
|
|
97
|
+
const groundTruth = resolveWithin(config.projectRoot, args.groundTruthPath ?? '.harbor/ground-truth.json', 'groundTruthPath')
|
|
98
|
+
const observations = resolveWithin(config.projectRoot, args.observationsPath, 'observationsPath')
|
|
99
|
+
const output = resolveWithin(config.projectRoot, args.outputPath ?? '.harbor/meta-evaluation-report.json', 'outputPath')
|
|
100
|
+
return cliJson(config, [
|
|
101
|
+
'meta-evaluate',
|
|
102
|
+
'--project-root', config.projectRoot,
|
|
103
|
+
'--ground-truth', groundTruth,
|
|
104
|
+
'--observations', observations,
|
|
105
|
+
'--output', output,
|
|
106
|
+
])
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function strictInputs(config, args) {
|
|
110
|
+
const projectRoot = path.resolve(config.projectRoot)
|
|
111
|
+
const candidate = resolveWithin(projectRoot, args.candidatePath, 'candidatePath')
|
|
112
|
+
const dataset = resolveWithin(projectRoot, args.datasetPath, 'datasetPath')
|
|
113
|
+
const stack = resolveWithin(projectRoot, args.stackPath, 'stackPath')
|
|
114
|
+
const jobs = resolveWithin(projectRoot, config.jobsDir, 'jobsDir')
|
|
115
|
+
const mode = args.mode
|
|
116
|
+
if (!['diagnostic', 'promotion-eligible'].includes(mode)) throw new Error('mode must be diagnostic or promotion-eligible')
|
|
117
|
+
const policy = args.policyPath ? resolveWithin(projectRoot, args.policyPath, 'policyPath') : undefined
|
|
118
|
+
if (mode === 'promotion-eligible' && !policy) throw new Error('promotion-eligible mode requires policyPath')
|
|
119
|
+
return { projectRoot, candidate, dataset, stack, jobs, mode, policy }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function validateDataset(config, args) {
|
|
123
|
+
const dataset = resolveWithin(config.projectRoot, args.datasetPath, 'datasetPath')
|
|
124
|
+
return cliJson(config, ['dataset', 'validate', dataset, '--project-root', config.projectRoot], { allowedExitCodes: [0, 2] })
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function initializeProject(config, args) {
|
|
128
|
+
const dataset = resolveWithin(config.projectRoot, args.datasetPath, 'datasetPath')
|
|
129
|
+
return cliJson(config, [
|
|
130
|
+
'init', '--project-root', config.projectRoot, '--dataset', dataset,
|
|
131
|
+
'--stack-id', args.stackId, '--stack-version', args.stackVersion,
|
|
132
|
+
'--dataset-id', args.datasetId, '--dataset-version', args.datasetVersion,
|
|
133
|
+
'--contract-id', args.contractId, '--contract-version', args.contractVersion,
|
|
134
|
+
'--primary-metric', args.primaryMetric, '--primary-direction', args.primaryDirection,
|
|
135
|
+
'--judge-provider', args.judgeProvider, '--judge-model', args.judgeModel, '--judge-version', args.judgeVersion,
|
|
136
|
+
'--policy-id', args.policyId, '--policy-version', args.policyVersion,
|
|
137
|
+
'--min-improvement', String(args.minImprovement),
|
|
138
|
+
])
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function runDoctor(config, args) {
|
|
142
|
+
const inputs = strictInputs(config, { ...args, mode: args.mode ?? 'diagnostic' })
|
|
143
|
+
const command = ['doctor', '--architecture', '--project-root', inputs.projectRoot, '--stack', inputs.stack, '--dataset', inputs.dataset]
|
|
144
|
+
if (args.candidatePath) command.push('--candidate', inputs.candidate)
|
|
145
|
+
if (inputs.policy) command.push('--policy', inputs.policy)
|
|
146
|
+
return cliJson(config, command, { allowedExitCodes: [0, 2] })
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function previewContext(config, args) {
|
|
150
|
+
const manifest = await snapshot(config, args)
|
|
151
|
+
const inputs = strictInputs(config, args)
|
|
152
|
+
const preview = await cliJson(config, [
|
|
153
|
+
'context', 'preview',
|
|
154
|
+
'--project-root', inputs.projectRoot,
|
|
155
|
+
'--candidate', inputs.candidate,
|
|
156
|
+
'--dataset', inputs.dataset,
|
|
157
|
+
'--stack', inputs.stack,
|
|
158
|
+
'--jobs-dir', inputs.jobs,
|
|
159
|
+
'--mode', inputs.mode,
|
|
160
|
+
])
|
|
161
|
+
return { manifest, ...preview }
|
|
162
|
+
}
|
|
163
|
+
|
|
40
164
|
export async function runEvaluation(config, args) {
|
|
41
165
|
const manifest = await snapshot(config, args)
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
|
|
166
|
+
const inputs = strictInputs(config, args)
|
|
167
|
+
const datasetValidation = await validateDataset(config, args)
|
|
168
|
+
if (!datasetValidation.valid) {
|
|
169
|
+
throw new Error(`Dataset validation failed: ${datasetValidation.findings.map(item => item.code).join(', ')}`)
|
|
170
|
+
}
|
|
171
|
+
const doctor = await runDoctor(config, args)
|
|
172
|
+
if (inputs.mode === 'promotion-eligible' && !doctor.promotion_ready) {
|
|
173
|
+
throw new Error(`Architecture Doctor blocked promotion-eligible Job: ${doctor.findings.filter(item => item.level === 'error').map(item => item.code).join(', ')}`)
|
|
174
|
+
}
|
|
175
|
+
const preview = await cliJson(config, [
|
|
176
|
+
'context', 'preview', '--project-root', inputs.projectRoot,
|
|
177
|
+
'--candidate', inputs.candidate, '--dataset', inputs.dataset,
|
|
178
|
+
'--stack', inputs.stack, '--jobs-dir', inputs.jobs, '--mode', inputs.mode,
|
|
179
|
+
])
|
|
45
180
|
const jobName = args.jobName ?? makeJobName(manifest)
|
|
46
181
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/.test(jobName)) throw new Error('jobName contains unsupported characters')
|
|
47
182
|
|
|
48
183
|
const harborArgs = [
|
|
49
|
-
'run', '-p', dataset,
|
|
184
|
+
'run', '-p', inputs.dataset,
|
|
50
185
|
'-a', config.agentImportPath,
|
|
51
|
-
'--ak', `candidate_path=${
|
|
186
|
+
'--ak', `candidate_path=${inputs.candidate}`,
|
|
52
187
|
'--ak', `candidate_version=${manifest.version}`,
|
|
53
188
|
'--ak', `candidate_digest=${manifest.digest}`,
|
|
54
189
|
'--job-name', jobName,
|
|
55
|
-
'--jobs-dir',
|
|
190
|
+
'--jobs-dir', inputs.jobs,
|
|
56
191
|
'--plugin', config.pluginImportPath,
|
|
57
|
-
'--plugin-kwarg', `candidate_manifest=${path.join(
|
|
192
|
+
'--plugin-kwarg', `candidate_manifest=${path.join(inputs.candidate, MANIFEST_NAME)}`,
|
|
193
|
+
'--plugin-kwarg', `dataset_path=${inputs.dataset}`,
|
|
194
|
+
'--plugin-kwarg', `stack_path=${inputs.stack}`,
|
|
195
|
+
'--plugin-kwarg', `project_root=${inputs.projectRoot}`,
|
|
196
|
+
'--plugin-kwarg', `mode=${inputs.mode}`,
|
|
58
197
|
]
|
|
59
|
-
|
|
198
|
+
if (inputs.policy) harborArgs.push('--plugin-kwarg', `policy_path=${inputs.policy}`)
|
|
199
|
+
const processResult = await runProcess(config.harborBin, harborArgs, {
|
|
60
200
|
cwd: config.projectRoot,
|
|
61
201
|
timeoutMs: config.timeoutMs,
|
|
62
202
|
env: { ...process.env, ...(config.pythonPath ? { PYTHONPATH: config.pythonPath } : {}) },
|
|
63
203
|
})
|
|
64
|
-
const jobDir = path.join(
|
|
204
|
+
const jobDir = path.join(inputs.jobs, jobName)
|
|
65
205
|
const summary = JSON.parse(await readFile(path.join(jobDir, 'evaluation-summary.json'), 'utf8'))
|
|
66
|
-
return {
|
|
206
|
+
return {
|
|
207
|
+
manifest,
|
|
208
|
+
job: path.relative(inputs.projectRoot, jobDir),
|
|
209
|
+
summary,
|
|
210
|
+
doctor,
|
|
211
|
+
contextPreview: preview,
|
|
212
|
+
process: { code: processResult.code },
|
|
213
|
+
}
|
|
67
214
|
}
|
|
68
215
|
|
|
69
216
|
export async function readEvaluation(config, args) {
|
|
@@ -75,14 +222,5 @@ export async function compareCandidates(config, args) {
|
|
|
75
222
|
const baseline = resolveWithin(config.projectRoot, args.baselineJob, 'baselineJob')
|
|
76
223
|
const candidate = resolveWithin(config.projectRoot, args.candidateJob, 'candidateJob')
|
|
77
224
|
const policy = resolveWithin(config.projectRoot, args.policyPath, 'policyPath')
|
|
78
|
-
|
|
79
|
-
'promote', baseline, candidate, '--policy', policy,
|
|
80
|
-
], {
|
|
81
|
-
cwd: config.projectRoot,
|
|
82
|
-
timeoutMs: config.timeoutMs,
|
|
83
|
-
// harbor-dsh uses exit 1 for a valid REJECT decision. The report remains
|
|
84
|
-
// model-readable; infrastructure failures still reject the subprocess.
|
|
85
|
-
allowedExitCodes: [0, 1],
|
|
86
|
-
})
|
|
87
|
-
return JSON.parse(result.stdout)
|
|
225
|
+
return cliJson(config, ['promote', baseline, candidate, '--policy', policy], { allowedExitCodes: [0, 1] })
|
|
88
226
|
}
|
package/lib/process.js
CHANGED
|
@@ -6,7 +6,7 @@ export function runProcess(command, args, options = {}) {
|
|
|
6
6
|
cwd: options.cwd,
|
|
7
7
|
env: options.env ?? process.env,
|
|
8
8
|
shell: false,
|
|
9
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
9
|
+
stdio: [options.input === undefined ? 'ignore' : 'pipe', 'pipe', 'pipe'],
|
|
10
10
|
})
|
|
11
11
|
let stdout = ''
|
|
12
12
|
let stderr = ''
|
|
@@ -16,6 +16,7 @@ export function runProcess(command, args, options = {}) {
|
|
|
16
16
|
}, options.timeoutMs ?? 1_800_000)
|
|
17
17
|
child.stdout.on('data', chunk => { stdout += chunk })
|
|
18
18
|
child.stderr.on('data', chunk => { stderr += chunk })
|
|
19
|
+
if (options.input !== undefined) child.stdin.end(options.input)
|
|
19
20
|
child.on('error', error => {
|
|
20
21
|
clearTimeout(timeout)
|
|
21
22
|
reject(error)
|
package/lib/service.js
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { access } from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
readComparison,
|
|
6
|
+
readDashboardSnapshot,
|
|
7
|
+
readDatasetPreview,
|
|
8
|
+
readEvaluatorGovernance,
|
|
9
|
+
readJobDetail,
|
|
10
|
+
readJobProgress,
|
|
11
|
+
readMetaEvaluation,
|
|
12
|
+
readTrialDetail,
|
|
13
|
+
readTrialsPage,
|
|
14
|
+
} from './dashboard.js'
|
|
15
|
+
import {
|
|
16
|
+
compareCandidates,
|
|
17
|
+
initializeGroundTruth,
|
|
18
|
+
initializeProject,
|
|
19
|
+
inspectEvaluator,
|
|
20
|
+
previewContext,
|
|
21
|
+
readEvaluation,
|
|
22
|
+
runDoctor,
|
|
23
|
+
runEvaluation,
|
|
24
|
+
runMetaEvaluation,
|
|
25
|
+
snapshot,
|
|
26
|
+
updateEvaluator,
|
|
27
|
+
validateDataset,
|
|
28
|
+
} from './evolution.js'
|
|
29
|
+
|
|
30
|
+
export async function resolveEvaluatorStackPath(config, governance, explicitPath) {
|
|
31
|
+
if (explicitPath) return explicitPath
|
|
32
|
+
const root = path.resolve(config.projectRoot)
|
|
33
|
+
const entry = governance.components?.evaluator?.entry
|
|
34
|
+
if (typeof entry !== 'string' || !entry) return undefined
|
|
35
|
+
let directory = path.dirname(path.resolve(root, entry))
|
|
36
|
+
if (directory !== root && !directory.startsWith(`${root}${path.sep}`)) return undefined
|
|
37
|
+
while (directory === root || directory.startsWith(`${root}${path.sep}`)) {
|
|
38
|
+
const candidate = path.join(directory, '.harbor', 'evaluation-stack.yml')
|
|
39
|
+
try {
|
|
40
|
+
await access(candidate)
|
|
41
|
+
return path.relative(root, candidate)
|
|
42
|
+
} catch {}
|
|
43
|
+
if (directory === root) break
|
|
44
|
+
directory = path.dirname(directory)
|
|
45
|
+
}
|
|
46
|
+
return undefined
|
|
47
|
+
}
|
|
3
48
|
|
|
4
49
|
/** One Host-side boundary shared by Agent tools and the Web dashboard. */
|
|
5
50
|
export class EvolutionService {
|
|
@@ -12,11 +57,24 @@ export class EvolutionService {
|
|
|
12
57
|
return snapshot(this.config, args)
|
|
13
58
|
}
|
|
14
59
|
|
|
60
|
+
initialize(args) {
|
|
61
|
+
return initializeProject(this.config, args)
|
|
62
|
+
}
|
|
63
|
+
|
|
15
64
|
run(args) {
|
|
16
65
|
return runEvaluation(this.config, args)
|
|
17
66
|
}
|
|
18
67
|
|
|
19
68
|
result(args) {
|
|
69
|
+
const job = String(args.jobPath ?? '').split(/[\\/]/).filter(Boolean).at(-1)
|
|
70
|
+
if (args.view === 'job') return readJobDetail(this.config, { job })
|
|
71
|
+
if (args.view === 'progress') return readJobProgress(this.config, { job, since: args.since })
|
|
72
|
+
if (args.view === 'trial') {
|
|
73
|
+
if (!args.trialId) throw new Error('trialId is required when view=trial')
|
|
74
|
+
return readTrialDetail(this.config, { job, trial: args.trialId })
|
|
75
|
+
}
|
|
76
|
+
if (args.view === 'dataset') return readDatasetPreview(this.config, { job })
|
|
77
|
+
if (args.view === 'governance') return readEvaluatorGovernance(this.config, { job, compareJob: args.compareJob })
|
|
20
78
|
return readEvaluation(this.config, args)
|
|
21
79
|
}
|
|
22
80
|
|
|
@@ -24,7 +82,84 @@ export class EvolutionService {
|
|
|
24
82
|
return compareCandidates(this.config, args)
|
|
25
83
|
}
|
|
26
84
|
|
|
85
|
+
doctor(args) {
|
|
86
|
+
return runDoctor(this.config, args)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
validateDataset(args) {
|
|
90
|
+
return validateDataset(this.config, args)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
previewContext(args) {
|
|
94
|
+
return previewContext(this.config, args)
|
|
95
|
+
}
|
|
96
|
+
|
|
27
97
|
dashboard() {
|
|
28
98
|
return readDashboardSnapshot(this.config, this.metadata)
|
|
29
99
|
}
|
|
100
|
+
|
|
101
|
+
job(args) {
|
|
102
|
+
return readJobDetail(this.config, args)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
trials(args) {
|
|
106
|
+
return readTrialsPage(this.config, args)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
trial(args) {
|
|
110
|
+
return readTrialDetail(this.config, args)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
dataset(args) {
|
|
114
|
+
return readDatasetPreview(this.config, args)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
progress(args) {
|
|
118
|
+
return readJobProgress(this.config, args)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
comparison(args) {
|
|
122
|
+
return readComparison(this.config, args)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async governance(args) {
|
|
126
|
+
const governance = await readEvaluatorGovernance(this.config, args)
|
|
127
|
+
try {
|
|
128
|
+
const stackPath = await resolveEvaluatorStackPath(this.config, governance, args.stackPath)
|
|
129
|
+
governance.evaluatorInterface = await inspectEvaluator(this.config, { ...args, stackPath })
|
|
130
|
+
governance.editingPolicy.browserWriteEnabled = true
|
|
131
|
+
governance.editingPolicy.stackPath = governance.evaluatorInterface.stack?.path
|
|
132
|
+
governance.editingPolicy.saveBehavior = 'Update one descriptor-authorized file with optimistic concurrency and create new Evaluator and Stack identities.'
|
|
133
|
+
} catch (error) {
|
|
134
|
+
governance.evaluatorInterface = { error: error instanceof Error ? error.message : String(error) }
|
|
135
|
+
}
|
|
136
|
+
return governance
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
evaluator(args) {
|
|
140
|
+
return updateEvaluator(this.config, args)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
evaluatorInspect(args) {
|
|
144
|
+
return inspectEvaluator(this.config, args)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
groundTruthInitialize(args) {
|
|
148
|
+
return initializeGroundTruth(this.config, args)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
evaluatorMetaEvaluate(args) {
|
|
152
|
+
return runMetaEvaluation(this.config, args)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async meta(args) {
|
|
156
|
+
const governance = await readEvaluatorGovernance(this.config, args)
|
|
157
|
+
const stackPath = await resolveEvaluatorStackPath(this.config, governance, args.stackPath)
|
|
158
|
+
if (!stackPath) return readMetaEvaluation(this.config)
|
|
159
|
+
const stackDirectory = path.dirname(path.resolve(this.config.projectRoot, stackPath))
|
|
160
|
+
const evaluationRoot = path.dirname(stackDirectory)
|
|
161
|
+
return readMetaEvaluation(this.config, {
|
|
162
|
+
evaluationRoot: path.relative(this.config.projectRoot, evaluationRoot),
|
|
163
|
+
})
|
|
164
|
+
}
|
|
30
165
|
}
|
package/lib/web.js
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
export const DASHBOARD_ROUTE = '/_dsh/harbor-evolution/dashboard'
|
|
2
|
+
export const JOB_ROUTE = '/_dsh/harbor-evolution/job'
|
|
3
|
+
export const TRIALS_ROUTE = '/_dsh/harbor-evolution/trials'
|
|
4
|
+
export const TRIAL_ROUTE = '/_dsh/harbor-evolution/trial'
|
|
5
|
+
export const DATASET_ROUTE = '/_dsh/harbor-evolution/dataset'
|
|
6
|
+
export const PROGRESS_ROUTE = '/_dsh/harbor-evolution/progress'
|
|
7
|
+
export const COMPARE_ROUTE = '/_dsh/harbor-evolution/compare'
|
|
8
|
+
export const GOVERNANCE_ROUTE = '/_dsh/harbor-evolution/governance'
|
|
9
|
+
export const EVALUATOR_ROUTE = '/_dsh/harbor-evolution/evaluator'
|
|
10
|
+
export const META_ROUTE = '/_dsh/harbor-evolution/meta'
|
|
11
|
+
const MAX_MUTATION_BYTES = 256 * 1024
|
|
2
12
|
|
|
3
13
|
function sendJson(response, status, body) {
|
|
4
14
|
response.writeHead(status, {
|
|
@@ -16,20 +26,24 @@ export function isSameOriginRequest(request) {
|
|
|
16
26
|
if (!origin) {
|
|
17
27
|
if (fetchSite === 'same-origin' || fetchSite === 'none') return true
|
|
18
28
|
const address = request.socket?.remoteAddress ?? ''
|
|
19
|
-
return address === '::1' || address === '127.0.0.1' || address.startsWith('127.')
|
|
20
|
-
|| address.startsWith('::ffff:127.')
|
|
29
|
+
return address === '::1' || address === '127.0.0.1' || address.startsWith('127.') || address.startsWith('::ffff:127.')
|
|
21
30
|
}
|
|
22
31
|
const host = request.headers.host
|
|
23
32
|
if (!host) return false
|
|
24
33
|
try {
|
|
25
34
|
const parsed = new URL(origin)
|
|
26
|
-
return
|
|
35
|
+
return ['http:', 'https:'].includes(parsed.protocol) && parsed.host === host
|
|
27
36
|
} catch {
|
|
28
37
|
return false
|
|
29
38
|
}
|
|
30
39
|
}
|
|
31
40
|
|
|
32
|
-
|
|
41
|
+
function safeError(error) {
|
|
42
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
43
|
+
return message.replace(/(?:\/[A-Za-z0-9._ -]+){2,}/g, '[local path]').replace(/[A-Za-z]:\\[^\s]+/g, '[local path]')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createApiHandler(load, code = 'request-failed') {
|
|
33
47
|
return (request, response) => {
|
|
34
48
|
if (request.method !== 'GET') {
|
|
35
49
|
response.writeHead(405, { allow: 'GET' })
|
|
@@ -40,24 +54,71 @@ export function createDashboardHandler(service) {
|
|
|
40
54
|
sendJson(response, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin request required' } })
|
|
41
55
|
return
|
|
42
56
|
}
|
|
43
|
-
|
|
57
|
+
const url = new URL(request.url ?? '/', 'http://localhost')
|
|
58
|
+
const args = Object.fromEntries(url.searchParams)
|
|
59
|
+
Promise.resolve(load(args)).then(
|
|
44
60
|
value => sendJson(response, 200, { ok: true, value }),
|
|
45
|
-
error => sendJson(response, 500, {
|
|
46
|
-
ok: false,
|
|
47
|
-
error: { code: 'dashboard-unavailable', message: error instanceof Error ? error.message : String(error) },
|
|
48
|
-
}),
|
|
61
|
+
error => sendJson(response, 500, { ok: false, error: { code, message: safeError(error) } }),
|
|
49
62
|
)
|
|
50
63
|
}
|
|
51
64
|
}
|
|
52
65
|
|
|
53
|
-
|
|
66
|
+
export function createDashboardHandler(service) {
|
|
67
|
+
return createApiHandler(() => service.dashboard(), 'dashboard-unavailable')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function createMutationHandler(update, code = 'update-failed') {
|
|
71
|
+
return async (request, response) => {
|
|
72
|
+
if (request.method !== 'POST') {
|
|
73
|
+
response.writeHead(405, { allow: 'POST' })
|
|
74
|
+
response.end()
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
if (!isSameOriginRequest(request)) {
|
|
78
|
+
sendJson(response, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin request required' } })
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
if (!String(request.headers['content-type'] ?? '').toLowerCase().startsWith('application/json')) {
|
|
82
|
+
sendJson(response, 415, { ok: false, error: { code: 'unsupported-media-type', message: 'application/json required' } })
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const chunks = []
|
|
87
|
+
let size = 0
|
|
88
|
+
for await (const chunk of request) {
|
|
89
|
+
size += chunk.length
|
|
90
|
+
if (size > MAX_MUTATION_BYTES) {
|
|
91
|
+
sendJson(response, 413, { ok: false, error: { code: 'payload-too-large', message: 'request body is too large' } })
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
|
|
95
|
+
}
|
|
96
|
+
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
|
97
|
+
const value = await update(body)
|
|
98
|
+
sendJson(response, 200, { ok: true, value })
|
|
99
|
+
} catch (error) {
|
|
100
|
+
sendJson(response, 400, { ok: false, error: { code, message: safeError(error) } })
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
54
105
|
export function installDashboardWeb(ctx, service) {
|
|
55
106
|
if (typeof ctx.inject !== 'function') return
|
|
56
107
|
ctx.inject(['webServer'], (webCtx) => {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
108
|
+
const routes = [
|
|
109
|
+
[DASHBOARD_ROUTE, createDashboardHandler(service)],
|
|
110
|
+
[JOB_ROUTE, createApiHandler(args => service.job(args), 'job-unavailable')],
|
|
111
|
+
[TRIALS_ROUTE, createApiHandler(args => service.trials(args), 'trials-unavailable')],
|
|
112
|
+
[TRIAL_ROUTE, createApiHandler(args => service.trial(args), 'trial-unavailable')],
|
|
113
|
+
[DATASET_ROUTE, createApiHandler(args => service.dataset(args), 'dataset-unavailable')],
|
|
114
|
+
[PROGRESS_ROUTE, createApiHandler(args => service.progress(args), 'progress-unavailable')],
|
|
115
|
+
[COMPARE_ROUTE, createApiHandler(args => service.comparison(args), 'comparison-unavailable')],
|
|
116
|
+
[GOVERNANCE_ROUTE, createApiHandler(args => service.governance(args), 'governance-unavailable')],
|
|
117
|
+
[EVALUATOR_ROUTE, createMutationHandler(args => service.evaluator(args), 'evaluator-update-failed')],
|
|
118
|
+
[META_ROUTE, createApiHandler(args => service.meta(args), 'meta-evaluation-unavailable')],
|
|
119
|
+
]
|
|
120
|
+
for (const [route, handler] of routes) {
|
|
121
|
+
webCtx.effect(() => webCtx.webServer.register({ kind: 'exact', path: route, handler }), `harbor-evolution: ${route}`)
|
|
122
|
+
}
|
|
62
123
|
})
|
|
63
124
|
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-harbor-evolution",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "DeepSeek Harness plugin and bundled Skill for safely evolving Cordis Candidates with Harbor.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./index.js",
|
|
9
9
|
"./client": "./lib/client.js",
|
|
10
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
11
|
+
"./schemas/evaluation-result.schema.json": "./schemas/evaluation-result.schema.json",
|
|
12
|
+
"./schemas/ground-truth.schema.json": "./schemas/ground-truth.schema.json",
|
|
13
|
+
"./schemas/evaluator-observations.schema.json": "./schemas/evaluator-observations.schema.json",
|
|
14
|
+
"./schemas/meta-evaluation-report.schema.json": "./schemas/meta-evaluation-report.schema.json",
|
|
10
15
|
"./package.json": "./package.json"
|
|
11
16
|
},
|
|
12
17
|
"bin": {
|
|
@@ -17,6 +22,7 @@
|
|
|
17
22
|
"lib/",
|
|
18
23
|
"bin/",
|
|
19
24
|
"skills/",
|
|
25
|
+
"schemas/",
|
|
20
26
|
"cordis.patch.yml",
|
|
21
27
|
"README.md",
|
|
22
28
|
"LICENSE"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "Harbor DSH Evaluator Result v1",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"additionalProperties": false,
|
|
6
|
+
"required": ["schema_version", "protocol", "criteria"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"schema_version": { "const": 1 },
|
|
9
|
+
"protocol": { "const": "evaluation-result/v1" },
|
|
10
|
+
"criteria": {
|
|
11
|
+
"type": "array",
|
|
12
|
+
"minItems": 1,
|
|
13
|
+
"items": {
|
|
14
|
+
"type": "object",
|
|
15
|
+
"additionalProperties": false,
|
|
16
|
+
"required": ["id", "score", "reason", "recommendation"],
|
|
17
|
+
"properties": {
|
|
18
|
+
"id": { "type": "string", "minLength": 1 },
|
|
19
|
+
"score": { "enum": [0, 0.5, 1] },
|
|
20
|
+
"reason": { "type": "string", "minLength": 1 },
|
|
21
|
+
"recommendation": { "type": "string", "minLength": 1 },
|
|
22
|
+
"evidence_refs": { "type": "array", "items": { "type": "string" } }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "Evaluator Observations v1",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": ["schema_version", "protocol", "evaluator", "repeat_policy", "observations"],
|
|
6
|
+
"properties": {
|
|
7
|
+
"schema_version": { "const": 1 },
|
|
8
|
+
"protocol": { "const": "evaluator-observations/v1" },
|
|
9
|
+
"evaluator": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"required": ["id", "version"],
|
|
12
|
+
"properties": {
|
|
13
|
+
"id": { "type": "string", "minLength": 1 },
|
|
14
|
+
"version": { "type": "string", "minLength": 1 },
|
|
15
|
+
"digest": { "type": "string" }
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"repeat_policy": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"required": ["repeats"],
|
|
21
|
+
"properties": {
|
|
22
|
+
"repeats": { "type": "integer", "minimum": 1 },
|
|
23
|
+
"seed_policy": { "type": "string" }
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"observations": {
|
|
27
|
+
"type": "array",
|
|
28
|
+
"minItems": 1,
|
|
29
|
+
"items": {
|
|
30
|
+
"type": "object",
|
|
31
|
+
"required": ["case_id", "repeat", "criteria"],
|
|
32
|
+
"properties": {
|
|
33
|
+
"case_id": { "type": "string", "minLength": 1 },
|
|
34
|
+
"repeat": { "type": "integer", "minimum": 1 },
|
|
35
|
+
"criteria": {
|
|
36
|
+
"type": "array",
|
|
37
|
+
"minItems": 1,
|
|
38
|
+
"items": {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"required": ["id", "score"],
|
|
41
|
+
"properties": {
|
|
42
|
+
"id": { "type": "string", "minLength": 1 },
|
|
43
|
+
"score": { "enum": [0, 0.5, 1] }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|