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/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 available = 100 - suffix.length - 1
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 candidateDir = resolveWithin(config.projectRoot, args.candidatePath, 'candidatePath')
43
- const dataset = resolveWithin(config.projectRoot, args.datasetPath, 'datasetPath')
44
- const jobsDir = resolveWithin(config.projectRoot, config.jobsDir, 'jobsDir')
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=${candidateDir}`,
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', jobsDir,
190
+ '--jobs-dir', inputs.jobs,
56
191
  '--plugin', config.pluginImportPath,
57
- '--plugin-kwarg', `candidate_manifest=${path.join(candidateDir, MANIFEST_NAME)}`,
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
- const result = await runProcess(config.harborBin, harborArgs, {
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(jobsDir, jobName)
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 { manifest, jobDir, summary, process: { code: result.code } }
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
- const result = await runProcess(config.harborDshBin, [
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 { readDashboardSnapshot } from './dashboard.js'
2
- import { compareCandidates, readEvaluation, runEvaluation, snapshot } from './evolution.js'
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 (parsed.protocol === 'http:' || parsed.protocol === 'https:') && parsed.host === host
35
+ return ['http:', 'https:'].includes(parsed.protocol) && parsed.host === host
27
36
  } catch {
28
37
  return false
29
38
  }
30
39
  }
31
40
 
32
- export function createDashboardHandler(service) {
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
- Promise.resolve(service.dashboard()).then(
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
- /** Add the dashboard route only in profiles that provide the optional Web service. */
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
- webCtx.effect(() => webCtx.webServer.register({
58
- kind: 'exact',
59
- path: DASHBOARD_ROUTE,
60
- handler: createDashboardHandler(service),
61
- }), 'harbor-evolution: dashboard route')
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.4.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
+ }