dsh-harbor-evolution 0.7.2 → 0.8.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 +16 -4
- package/index.js +72 -6
- package/lib/candidate.js +58 -5
- package/lib/client.js +283 -63
- package/lib/dashboard.js +307 -34
- package/lib/evolution.js +328 -20
- package/lib/model-runtime.js +53 -8
- package/lib/runtime-identity.js +7 -0
- package/lib/service.js +187 -33
- package/lib/session-diagnostic.js +320 -0
- package/lib/session-materializer.js +194 -0
- package/lib/session-projection.js +161 -0
- package/lib/session-redaction.js +311 -0
- package/lib/session-selection.js +294 -0
- package/lib/setup.js +11 -5
- package/lib/version.js +128 -0
- package/lib/web.js +5 -1
- package/package.json +13 -3
- package/schemas/dsh-session-observation.schema.json +69 -0
- package/schemas/evaluation-result-v2.schema.json +45 -0
- package/schemas/historical-evaluation-context.schema.json +66 -0
- package/schemas/historical-evaluation-summary.schema.json +49 -0
- package/schemas/historical-generation-batch.schema.json +76 -0
- package/skills/evolve-agent-with-harbor/SKILL.md +127 -13
- package/skills/evolve-agent-with-harbor/evals/evals.json +57 -9
- package/skills/evolve-agent-with-harbor/references/evaluator-upgrade.md +34 -1
- package/skills/evolve-agent-with-harbor/references/initialization.md +9 -2
package/lib/dashboard.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { access, constants, lstat, readdir, readFile, stat } from 'node:fs/promises'
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
2
3
|
import path from 'node:path'
|
|
3
4
|
|
|
4
5
|
import { resolveWithin } from './evolution.js'
|
|
5
6
|
|
|
6
7
|
const SUMMARY_NAME = 'evaluation-summary.json'
|
|
7
|
-
const
|
|
8
|
+
const HISTORICAL_COMPLETION_NAME = 'historical-evaluation-complete.json'
|
|
9
|
+
const DEFAULT_JOB_PAGE_SIZE = 20
|
|
10
|
+
const MAX_JOB_PAGE_SIZE = 100
|
|
8
11
|
const MAX_JSON_BYTES = 2 * 1024 * 1024
|
|
9
12
|
const MAX_SOURCE_BYTES = 128 * 1024
|
|
10
13
|
const MAX_PREVIEW_BYTES = 512 * 1024
|
|
@@ -12,6 +15,11 @@ const MAX_TRIAL_LIMIT = 100
|
|
|
12
15
|
const jsonCache = new Map()
|
|
13
16
|
const SENSITIVE_KEY = /authorization|cookie|token|api[_-]?key|secret|password|request[_-]?headers/i
|
|
14
17
|
const SENSITIVE_SOURCE_VALUE = /(authorization|cookie|token|api[_-]?key|secret|password)\s*[:=]\s*([^\s,;]+)/gi
|
|
18
|
+
const WORKSPACE_SKIP_DIRECTORIES = new Set([
|
|
19
|
+
'.cache', '.git', '.harbor', '.next', '.venv', '__pycache__',
|
|
20
|
+
'build', 'candidates', 'coverage', 'datasets', 'dist', 'jobs', 'node_modules', 'public', 'vendor', 'venv',
|
|
21
|
+
])
|
|
22
|
+
const MAX_WORKSPACE_DEPTH = 5
|
|
15
23
|
|
|
16
24
|
function safeSegment(value, label) {
|
|
17
25
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(String(value ?? ''))) throw new Error(`${label} is invalid`)
|
|
@@ -100,19 +108,65 @@ function isObject(value) {
|
|
|
100
108
|
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
101
109
|
}
|
|
102
110
|
|
|
111
|
+
const CANDIDATE_JOB_KIND = 'candidate-evaluation'
|
|
112
|
+
const HISTORICAL_JOB_KIND = 'historical-generation-evaluation'
|
|
113
|
+
|
|
114
|
+
function normalizedJobKind(summary, context) {
|
|
115
|
+
const declared = summary?.job_kind ?? context?.job_kind
|
|
116
|
+
if (typeof declared === 'string' && declared) return declared
|
|
117
|
+
if (context?.protocol === 'historical-generation-evaluation-context/v1') return HISTORICAL_JOB_KIND
|
|
118
|
+
return CANDIDATE_JOB_KIND
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function coverageView(summary) {
|
|
122
|
+
if (isObject(summary?.coverage)) return summary.coverage
|
|
123
|
+
const total = Number(summary?.n_trials ?? 0)
|
|
124
|
+
const scored = Number(summary?.scored_trial_count ?? summary?.n_valid_scores ?? 0)
|
|
125
|
+
const unscored = Number(
|
|
126
|
+
summary?.unscored_trial_count
|
|
127
|
+
?? summary?.status_counts?.['completed-unscored']
|
|
128
|
+
?? 0,
|
|
129
|
+
)
|
|
130
|
+
return {
|
|
131
|
+
scored_trials: scored,
|
|
132
|
+
unscored_trials: unscored,
|
|
133
|
+
total_trials: total,
|
|
134
|
+
trial_rate: total ? scored / total : undefined,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function evaluatorMetaEvaluation(summary, context) {
|
|
139
|
+
return summary?.evaluator_meta_evaluation
|
|
140
|
+
?? context?.downstream_analysis?.evaluator_meta_evaluation
|
|
141
|
+
?? (normalizedJobKind(summary, context) === HISTORICAL_JOB_KIND
|
|
142
|
+
? { status: 'not-run', validation_report_ref: null }
|
|
143
|
+
: undefined)
|
|
144
|
+
}
|
|
145
|
+
|
|
103
146
|
function capabilityMap(summary, context, lifecycle, registry, stack) {
|
|
147
|
+
const jobKind = normalizedJobKind(summary, context)
|
|
148
|
+
const historicalGeneration = jobKind === HISTORICAL_JOB_KIND
|
|
104
149
|
const contextV2 = context?.schema_version === 2
|
|
150
|
+
const historicalContext = context?.schema_version === 1
|
|
151
|
+
&& context?.protocol === 'historical-generation-evaluation-context/v1'
|
|
105
152
|
const scoreValidity = summary?.schema_version === 3
|
|
153
|
+
|| summary?.schema_version === 4
|
|
106
154
|
return {
|
|
155
|
+
jobKind,
|
|
107
156
|
contextV2,
|
|
157
|
+
contextSupported: contextV2 || historicalContext,
|
|
158
|
+
historicalGeneration,
|
|
159
|
+
candidateEvaluation: !historicalGeneration,
|
|
108
160
|
trialLifecycle: lifecycle?.schema_version === 1,
|
|
109
161
|
scoreValidity,
|
|
110
162
|
evidenceProvenance: scoreValidity,
|
|
111
|
-
artifactRegistry: registry?.schema_version
|
|
112
|
-
|
|
163
|
+
artifactRegistry: [1, 2].includes(registry?.schema_version),
|
|
164
|
+
source: historicalGeneration,
|
|
165
|
+
compare: contextV2 && !historicalGeneration,
|
|
113
166
|
evaluatorGovernance: stack?.schema_version === 1,
|
|
114
|
-
|
|
115
|
-
|
|
167
|
+
evaluatorMetaEvaluation: evaluatorMetaEvaluation(summary, context),
|
|
168
|
+
gate: contextV2 && !historicalGeneration && summary?.mode === 'promotion-eligible',
|
|
169
|
+
readOnlyLegacy: !contextV2 && !historicalContext,
|
|
116
170
|
}
|
|
117
171
|
}
|
|
118
172
|
|
|
@@ -124,7 +178,7 @@ function primaryMetric(summary, contract) {
|
|
|
124
178
|
}
|
|
125
179
|
|
|
126
180
|
function progressView(summary, lifecycle, updatedAt) {
|
|
127
|
-
const total = Number(lifecycle?.dataset_total ?? summary?.n_trials ?? 0)
|
|
181
|
+
const total = Number(lifecycle?.dataset_total ?? summary?.n_trials ?? summary?.coverage?.total_trials ?? 0)
|
|
128
182
|
const lifecycleTrials = selectedLifecycleTrials(lifecycle)
|
|
129
183
|
const completed = lifecycle
|
|
130
184
|
? lifecycleTrials.filter(item => item.terminal).length
|
|
@@ -144,19 +198,53 @@ function progressView(summary, lifecycle, updatedAt) {
|
|
|
144
198
|
}
|
|
145
199
|
}
|
|
146
200
|
|
|
147
|
-
|
|
201
|
+
const HISTORICAL_COVERAGE_KEYS = [
|
|
202
|
+
'scored_trials', 'unscored_trials', 'total_trials', 'trial_rate',
|
|
203
|
+
'criterion_scored', 'criterion_total', 'criterion_rate',
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
function historicalCompletionValid(summary, completion, jobName) {
|
|
207
|
+
return (
|
|
208
|
+
summary?.schema_version === 4
|
|
209
|
+
&& summary?.job === jobName
|
|
210
|
+
&& summary?.job_kind === HISTORICAL_JOB_KIND
|
|
211
|
+
&& summary?.mode === 'diagnostic'
|
|
212
|
+
&& summary?.execution_mode === 'observe-existing'
|
|
213
|
+
&& summary?.artifact_validation?.valid === true
|
|
214
|
+
&& summary?.candidate === undefined
|
|
215
|
+
&& completion?.schema_version === 1
|
|
216
|
+
&& completion?.job_kind === HISTORICAL_JOB_KIND
|
|
217
|
+
&& completion?.status === 'completed'
|
|
218
|
+
&& completion?.valid === true
|
|
219
|
+
&& completion?.job === jobName
|
|
220
|
+
&& completion?.summary_path === SUMMARY_NAME
|
|
221
|
+
&& completion?.artifact_registry_path === 'artifact-registry.json'
|
|
222
|
+
&& HISTORICAL_COVERAGE_KEYS.every(key => (
|
|
223
|
+
typeof summary?.coverage?.[key] === 'number'
|
|
224
|
+
&& summary.coverage[key] === completion?.coverage?.[key]
|
|
225
|
+
))
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function jobStatus(summary, lifecycle, progress, jobKind, completion, jobName) {
|
|
148
230
|
if (summary?.__readError) return 'failed'
|
|
149
231
|
if (!summary && !lifecycle) return 'pending'
|
|
150
232
|
if (progress.active) return 'running'
|
|
151
233
|
if (!summary && lifecycle) return 'running'
|
|
234
|
+
if (summary?.artifact_validation?.valid === false) return 'failed'
|
|
235
|
+
if (
|
|
236
|
+
jobKind === HISTORICAL_JOB_KIND
|
|
237
|
+
&& !historicalCompletionValid(summary, completion, jobName)
|
|
238
|
+
) return 'failed'
|
|
152
239
|
if (Number(summary.n_infrastructure_exceptions ?? summary.n_exceptions ?? 0) > 0 || Number(summary.n_evaluation_exceptions ?? 0) > 0) return 'partial'
|
|
153
|
-
|
|
240
|
+
const invalidScores = Number(summary.n_invalid_scores ?? 0)
|
|
241
|
+
if (invalidScores > 0) return 'attention'
|
|
154
242
|
return 'completed'
|
|
155
243
|
}
|
|
156
244
|
|
|
157
245
|
async function readJob(jobsDir, entry, details) {
|
|
158
246
|
const directory = path.join(jobsDir, entry.name)
|
|
159
|
-
const [summary, contextFile, promotion, contract, lifecycle, registry, stack] = await Promise.all([
|
|
247
|
+
const [summary, contextFile, promotion, contract, lifecycle, registry, stack, completion] = await Promise.all([
|
|
160
248
|
readJson(path.join(directory, SUMMARY_NAME)),
|
|
161
249
|
readJson(path.join(directory, 'evaluation-context.json')),
|
|
162
250
|
readJson(path.join(directory, 'promotion-report.json')),
|
|
@@ -164,25 +252,38 @@ async function readJob(jobsDir, entry, details) {
|
|
|
164
252
|
readJson(path.join(directory, 'trial-lifecycle.json')),
|
|
165
253
|
readJson(path.join(directory, 'artifact-registry.json')),
|
|
166
254
|
readJson(path.join(directory, 'evaluation-stack-manifest.json')),
|
|
255
|
+
readJson(path.join(directory, HISTORICAL_COMPLETION_NAME)),
|
|
167
256
|
])
|
|
168
257
|
const evaluationContext = summary?.evaluation_context ?? contextFile
|
|
169
258
|
if (!evaluationContext && !summary && !lifecycle) return undefined
|
|
170
259
|
const updatedAt = details.mtime.toISOString()
|
|
171
260
|
const progress = progressView(summary, lifecycle, updatedAt)
|
|
261
|
+
const jobKind = normalizedJobKind(summary, evaluationContext)
|
|
172
262
|
const capabilities = capabilityMap(summary, evaluationContext, lifecycle, registry, stack)
|
|
263
|
+
const evaluationTarget = summary?.evaluation_target ?? evaluationContext?.evaluation_target
|
|
264
|
+
const generationSource = summary?.generation_source ?? evaluationContext?.generation_source
|
|
265
|
+
const coverage = coverageView(summary)
|
|
173
266
|
return {
|
|
174
267
|
name: entry.name,
|
|
175
268
|
updatedAt,
|
|
176
|
-
status: jobStatus(summary, lifecycle, progress),
|
|
269
|
+
status: jobStatus(summary, lifecycle, progress, jobKind, completion, entry.name),
|
|
270
|
+
jobKind,
|
|
177
271
|
mode: summary?.mode ?? evaluationContext?.mode,
|
|
272
|
+
executionMode: summary?.execution_mode ?? evaluationContext?.execution_mode,
|
|
178
273
|
nTrials: progress.total,
|
|
179
274
|
nDiscoveredTrials: Number(summary?.n_discovered_trials ?? lifecycle?.attempt_count ?? 0),
|
|
180
275
|
nValidScores: summary?.n_valid_scores,
|
|
181
276
|
nInvalidScores: summary?.n_invalid_scores,
|
|
277
|
+
nUnscoredTrials: Number(coverage.unscored_trials ?? 0),
|
|
182
278
|
nExceptions: Number(summary?.n_exceptions ?? 0),
|
|
183
279
|
primaryMetric: primaryMetric(summary, contract),
|
|
184
280
|
metrics: summary?.metrics ?? {},
|
|
185
281
|
candidate: summary?.candidate ?? evaluationContext?.candidate,
|
|
282
|
+
evaluationTarget,
|
|
283
|
+
generationSource,
|
|
284
|
+
generatorPopulation: evaluationTarget?.generator_population,
|
|
285
|
+
coverage,
|
|
286
|
+
evaluatorMetaEvaluation: evaluatorMetaEvaluation(summary, evaluationContext),
|
|
186
287
|
dataset: evaluationContext?.dataset,
|
|
187
288
|
evaluationContext,
|
|
188
289
|
progress,
|
|
@@ -193,19 +294,96 @@ async function readJob(jobsDir, entry, details) {
|
|
|
193
294
|
}
|
|
194
295
|
}
|
|
195
296
|
|
|
196
|
-
async function listJobs(jobsDir) {
|
|
297
|
+
async function listJobs(jobsDir, { offset = 0, limit = DEFAULT_JOB_PAGE_SIZE } = {}) {
|
|
197
298
|
let entries
|
|
198
299
|
try {
|
|
199
300
|
entries = await readdir(jobsDir, { withFileTypes: true })
|
|
200
301
|
} catch (error) {
|
|
201
|
-
if (error.code === 'ENOENT') return []
|
|
302
|
+
if (error.code === 'ENOENT') return { items: [], total: 0, offset, limit, hasMore: false }
|
|
202
303
|
throw error
|
|
203
304
|
}
|
|
204
305
|
const directories = entries.filter(entry => entry.isDirectory() && !entry.isSymbolicLink())
|
|
205
306
|
const recent = await Promise.all(directories.map(async entry => ({ entry, details: await stat(path.join(jobsDir, entry.name)) })))
|
|
206
307
|
recent.sort((left, right) => right.details.mtimeMs - left.details.mtimeMs)
|
|
207
|
-
const
|
|
208
|
-
|
|
308
|
+
const page = recent.slice(offset, offset + limit)
|
|
309
|
+
const jobs = await Promise.all(page.map(({ entry, details }) => readJob(jobsDir, entry, details)))
|
|
310
|
+
return { items: jobs.filter(Boolean), total: recent.length, offset, limit, hasMore: offset + limit < recent.length }
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function relativePath(root, value) {
|
|
314
|
+
return path.relative(root, value).split(path.sep).join('/') || '.'
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function workspaceIdentity(projectRoot, workspaceRoot, jobsDir, preferred) {
|
|
318
|
+
const digest = createHash('sha256').update(`${projectRoot}\0${workspaceRoot}\0${jobsDir}`).digest('hex').slice(0, 12)
|
|
319
|
+
const label = String(preferred || path.basename(workspaceRoot) || 'root').replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'root'
|
|
320
|
+
return `${label}-${digest}`
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function regularFile(pathname) {
|
|
324
|
+
try {
|
|
325
|
+
const details = await lstat(pathname)
|
|
326
|
+
return details.isFile() && !details.isSymbolicLink()
|
|
327
|
+
} catch (error) {
|
|
328
|
+
if (error.code === 'ENOENT') return false
|
|
329
|
+
throw error
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Discover root and namespaced Harbor workspaces without interpreting YAML. */
|
|
334
|
+
export async function discoverWorkspaceConfigs(config) {
|
|
335
|
+
const projectRoot = path.resolve(config.projectRoot)
|
|
336
|
+
const found = []
|
|
337
|
+
async function visit(directory, depth) {
|
|
338
|
+
const harborDirectory = path.join(directory, '.harbor')
|
|
339
|
+
const descriptorPath = path.join(harborDirectory, 'workspace.json')
|
|
340
|
+
const stackPath = path.join(harborDirectory, 'evaluation-stack.yml')
|
|
341
|
+
const descriptor = await readJson(descriptorPath)
|
|
342
|
+
if (descriptor?.schema_version === 1 && descriptor.jobs && descriptor.stack) {
|
|
343
|
+
const jobs = relativePath(projectRoot, resolveWithin(projectRoot, path.resolve(directory, descriptor.jobs), 'workspace.jobs'))
|
|
344
|
+
const stack = relativePath(projectRoot, resolveWithin(projectRoot, path.resolve(directory, descriptor.stack), 'workspace.stack'))
|
|
345
|
+
const workspaceRoot = relativePath(projectRoot, directory)
|
|
346
|
+
found.push({
|
|
347
|
+
...config,
|
|
348
|
+
jobsDir: jobs,
|
|
349
|
+
stackPath: stack,
|
|
350
|
+
workspaceRoot,
|
|
351
|
+
workspaceLabel: descriptor.workspace_id ?? workspaceRoot,
|
|
352
|
+
workspaceId: workspaceIdentity(projectRoot, directory, jobs, descriptor.workspace_id),
|
|
353
|
+
})
|
|
354
|
+
} else if (await regularFile(stackPath)) {
|
|
355
|
+
const workspaceRoot = relativePath(projectRoot, directory)
|
|
356
|
+
const jobs = relativePath(projectRoot, path.join(directory, 'jobs'))
|
|
357
|
+
found.push({
|
|
358
|
+
...config,
|
|
359
|
+
jobsDir: jobs,
|
|
360
|
+
stackPath: relativePath(projectRoot, stackPath),
|
|
361
|
+
workspaceRoot,
|
|
362
|
+
workspaceLabel: workspaceRoot,
|
|
363
|
+
workspaceId: workspaceIdentity(projectRoot, directory, jobs, workspaceRoot),
|
|
364
|
+
})
|
|
365
|
+
}
|
|
366
|
+
if (depth >= MAX_WORKSPACE_DEPTH) return
|
|
367
|
+
let entries
|
|
368
|
+
try { entries = await readdir(directory, { withFileTypes: true }) } catch (error) {
|
|
369
|
+
if (error.code === 'ENOENT' || error.code === 'EACCES') return
|
|
370
|
+
throw error
|
|
371
|
+
}
|
|
372
|
+
await Promise.all(entries
|
|
373
|
+
.filter(entry => entry.isDirectory() && !entry.isSymbolicLink() && !WORKSPACE_SKIP_DIRECTORIES.has(entry.name))
|
|
374
|
+
.map(entry => visit(path.join(directory, entry.name), depth + 1)))
|
|
375
|
+
}
|
|
376
|
+
await visit(projectRoot, 0)
|
|
377
|
+
if (!found.some(item => item.workspaceRoot === '.')) {
|
|
378
|
+
found.unshift({
|
|
379
|
+
...config,
|
|
380
|
+
stackPath: '.harbor/evaluation-stack.yml',
|
|
381
|
+
workspaceRoot: '.',
|
|
382
|
+
workspaceLabel: path.basename(projectRoot) || 'root',
|
|
383
|
+
workspaceId: workspaceIdentity(projectRoot, projectRoot, config.jobsDir, path.basename(projectRoot)),
|
|
384
|
+
})
|
|
385
|
+
}
|
|
386
|
+
return found.sort((left, right) => left.workspaceRoot.localeCompare(right.workspaceRoot))
|
|
209
387
|
}
|
|
210
388
|
|
|
211
389
|
function jobsDirectory(config) {
|
|
@@ -216,32 +394,39 @@ function jobDirectory(config, job) {
|
|
|
216
394
|
return path.join(jobsDirectory(config), safeSegment(job, 'job'))
|
|
217
395
|
}
|
|
218
396
|
|
|
219
|
-
export async function readDashboardSnapshot(config, metadata = {}) {
|
|
397
|
+
export async function readDashboardSnapshot(config, metadata = {}, args = {}) {
|
|
220
398
|
const projectRoot = path.resolve(config.projectRoot)
|
|
221
399
|
const jobsDir = jobsDirectory(config)
|
|
222
|
-
const
|
|
223
|
-
|
|
400
|
+
const offset = Math.max(0, Number.parseInt(args.offset ?? 0, 10) || 0)
|
|
401
|
+
const limit = Math.min(MAX_JOB_PAGE_SIZE, Math.max(1, Number.parseInt(args.limit ?? DEFAULT_JOB_PAGE_SIZE, 10) || DEFAULT_JOB_PAGE_SIZE))
|
|
402
|
+
const [jobPage, projectRootCheck, jobsDirCheck, harborCheck, harborDshCheck, stackCheck] = await Promise.all([
|
|
403
|
+
listJobs(jobsDir, { offset, limit }),
|
|
224
404
|
directoryCheck(projectRoot),
|
|
225
405
|
directoryCheck(jobsDir, { optional: true }),
|
|
226
406
|
executableCheck(config.harborBin),
|
|
227
407
|
executableCheck(config.harborDshBin),
|
|
228
|
-
fileCheck(
|
|
408
|
+
fileCheck(resolveWithin(projectRoot, config.stackPath ?? '.harbor/evaluation-stack.yml', 'stackPath')),
|
|
229
409
|
])
|
|
410
|
+
const jobs = jobPage.items
|
|
230
411
|
const counts = jobs.reduce((result, job) => ({ ...result, [job.status]: (result[job.status] ?? 0) + 1 }), {})
|
|
231
412
|
const latestMetric = jobs.find(job => job.primaryMetric)?.primaryMetric
|
|
232
413
|
return {
|
|
233
414
|
schemaVersion: 3,
|
|
234
415
|
generatedAt: new Date().toISOString(),
|
|
235
416
|
pluginVersion: metadata.pluginVersion ?? 'development',
|
|
236
|
-
|
|
417
|
+
workspace: { id: config.workspaceId, label: config.workspaceLabel, root: config.workspaceRoot ?? '.', stackPath: config.stackPath },
|
|
418
|
+
workspaces: metadata.workspaces ?? [],
|
|
419
|
+
config: { projectRoot, projectRootSource: metadata.projectRootSource ?? 'configured', jobsDir: config.jobsDir, runtimePolicy: config.runtimePolicy ?? 'follow-latest', agentImportPath: config.agentImportPath, pluginImportPath: config.pluginImportPath },
|
|
237
420
|
checks: { projectRoot: projectRootCheck, jobsDir: jobsDirCheck, harbor: harborCheck, harborDsh: harborDshCheck, evaluationStack: stackCheck },
|
|
238
421
|
overview: {
|
|
239
|
-
totalJobs:
|
|
422
|
+
totalJobs: jobPage.total,
|
|
423
|
+
visibleJobs: jobs.length,
|
|
240
424
|
completedJobs: (counts.completed ?? 0) + (counts.partial ?? 0) + (counts.attention ?? 0),
|
|
241
425
|
activeJobs: (counts.pending ?? 0) + (counts.running ?? 0),
|
|
242
426
|
failedJobs: counts.failed ?? 0,
|
|
243
427
|
latestMetric,
|
|
244
428
|
},
|
|
429
|
+
jobPagination: { offset: jobPage.offset, limit: jobPage.limit, total: jobPage.total, hasMore: jobPage.hasMore },
|
|
245
430
|
jobs,
|
|
246
431
|
}
|
|
247
432
|
}
|
|
@@ -252,6 +437,7 @@ const DETAIL_ARTIFACTS = {
|
|
|
252
437
|
dataset: 'dataset-manifest.json',
|
|
253
438
|
datasetPreview: 'dataset-preview.json',
|
|
254
439
|
stack: 'evaluation-stack-manifest.json',
|
|
440
|
+
stackSources: 'evaluation-stack-sources.json',
|
|
255
441
|
context: 'evaluation-context.json',
|
|
256
442
|
contract: 'evaluation-contract.json',
|
|
257
443
|
doctor: 'architecture-doctor.json',
|
|
@@ -261,6 +447,7 @@ const DETAIL_ARTIFACTS = {
|
|
|
261
447
|
diagnosis: 'diagnosis-report.json',
|
|
262
448
|
optimization: 'optimization-report.json',
|
|
263
449
|
promotion: 'promotion-report.json',
|
|
450
|
+
completion: HISTORICAL_COMPLETION_NAME,
|
|
264
451
|
}
|
|
265
452
|
|
|
266
453
|
function schemaIssue(key, value) {
|
|
@@ -268,17 +455,21 @@ function schemaIssue(key, value) {
|
|
|
268
455
|
if (value?.__readError) return value.__readError
|
|
269
456
|
if (!isObject(value)) return 'artifact must be an object'
|
|
270
457
|
const versions = {
|
|
271
|
-
summary: [2, 3], candidate: [1], dataset: [1], datasetPreview: [1], stack: [1], context: [1, 2], contract: [1],
|
|
272
|
-
doctor: [1], population: [1, 2], lifecycle: [1], registry: [1], diagnosis: [1], optimization: [1, 2], promotion: [2],
|
|
458
|
+
summary: [2, 3, 4], candidate: [1], dataset: [1], datasetPreview: [1], stack: [1], stackSources: [1], context: [1, 2], contract: [1],
|
|
459
|
+
doctor: [1], population: [1, 2, 3], lifecycle: [1], registry: [1, 2], diagnosis: [1, 2], optimization: [1, 2, 3], promotion: [2], completion: [1],
|
|
273
460
|
}[key]
|
|
274
461
|
if (versions && !versions.includes(value.schema_version)) return `schema_version must be one of ${versions.join(', ')}`
|
|
275
462
|
const required = {
|
|
276
463
|
summary: ['job', 'metrics'], candidate: ['candidate_id', 'version', 'digest'], dataset: ['dataset_id', 'version', 'source_digest', 'tasks'], datasetPreview: ['dataset_id', 'version', 'source_digest', 'tasks'],
|
|
277
|
-
stack: ['stack_id', 'version', 'digest', 'components', 'judge'], context: ['digest'], contract: ['contract_id', 'version', 'primary_metric', 'metrics'],
|
|
464
|
+
stack: ['stack_id', 'version', 'digest', 'components', 'judge'], stackSources: ['stack_digest', 'components'], context: ['digest'], contract: ['contract_id', 'version', 'primary_metric', 'metrics'],
|
|
278
465
|
doctor: ['promotion_ready', 'findings'], population: ['population_size', 'groups', 'metrics'], lifecycle: ['dataset_total', 'trials'],
|
|
279
466
|
registry: ['artifacts'], diagnosis: ['diagnoses'], optimization: ['hypotheses'], promotion: ['decision', 'reasons', 'policy_digest'],
|
|
467
|
+
completion: ['job_kind', 'status', 'valid', 'job', 'summary_path', 'artifact_registry_path', 'coverage'],
|
|
280
468
|
}[key] ?? []
|
|
281
|
-
const
|
|
469
|
+
const requiredFields = key === 'population' && value.schema_version === 3
|
|
470
|
+
? ['population_size', 'coverage', 'metrics']
|
|
471
|
+
: required
|
|
472
|
+
const missing = requiredFields.filter(field => value[field] === undefined)
|
|
282
473
|
return missing.length ? `missing fields: ${missing.join(', ')}` : undefined
|
|
283
474
|
}
|
|
284
475
|
|
|
@@ -298,10 +489,35 @@ export async function readJobDetail(config, args) {
|
|
|
298
489
|
return [key, value === undefined ? { status: 'unavailable', reason: 'capability-not-produced' } : issue ? { status: 'invalid', error: issue } : { status: 'valid' }]
|
|
299
490
|
}))
|
|
300
491
|
const context = artifacts.context ?? values[Object.keys(DETAIL_ARTIFACTS).indexOf('context')]
|
|
492
|
+
const summary = values[Object.keys(DETAIL_ARTIFACTS).indexOf('summary')]
|
|
493
|
+
const jobKind = normalizedJobKind(summary, context)
|
|
494
|
+
if (
|
|
495
|
+
jobKind === HISTORICAL_JOB_KIND
|
|
496
|
+
&& !historicalCompletionValid(summary, artifacts.completion, job)
|
|
497
|
+
) {
|
|
498
|
+
validation.completion = {
|
|
499
|
+
status: 'invalid',
|
|
500
|
+
error: 'Historical completion sentinel is missing, stale, or inconsistent with the Summary',
|
|
501
|
+
}
|
|
502
|
+
}
|
|
301
503
|
const capabilities = capabilityMap(
|
|
302
|
-
|
|
504
|
+
summary, context, artifacts.lifecycle, artifacts.registry, artifacts.stack,
|
|
303
505
|
)
|
|
304
|
-
|
|
506
|
+
const evaluationTarget = summary?.evaluation_target ?? context?.evaluation_target
|
|
507
|
+
return {
|
|
508
|
+
schemaVersion: 3,
|
|
509
|
+
job,
|
|
510
|
+
jobKind,
|
|
511
|
+
evaluationTarget,
|
|
512
|
+
generationSource: summary?.generation_source ?? context?.generation_source,
|
|
513
|
+
generatorPopulation: evaluationTarget?.generator_population,
|
|
514
|
+
executionMode: summary?.execution_mode ?? context?.execution_mode,
|
|
515
|
+
coverage: coverageView(summary),
|
|
516
|
+
evaluatorMetaEvaluation: evaluatorMetaEvaluation(summary, context),
|
|
517
|
+
capabilities,
|
|
518
|
+
artifacts,
|
|
519
|
+
validation,
|
|
520
|
+
}
|
|
305
521
|
}
|
|
306
522
|
|
|
307
523
|
function selectedLifecycleTrials(lifecycle) {
|
|
@@ -316,13 +532,15 @@ function selectedLifecycleTrials(lifecycle) {
|
|
|
316
532
|
function normalizeTrial(trial, order) {
|
|
317
533
|
const score = trial.score ?? { value: undefined, valid: trial.exception ? false : true, invalid_reasons: trial.exception ? ['infrastructure-error'] : [] }
|
|
318
534
|
const datasetOrder = Number(trial.datasetOrder ?? trial.dataset_order ?? order)
|
|
535
|
+
const status = trial.status ?? trial.phase ?? (trial.exception ? 'infrastructure-error' : 'completed')
|
|
319
536
|
return {
|
|
320
537
|
id: trial.id ?? trial.execution_id ?? `dataset-${datasetOrder}`,
|
|
321
538
|
name: trial.name ?? trial.trial_name ?? trial.dataset_trial ?? trial.trial,
|
|
322
539
|
datasetTrial: trial.datasetTrial ?? trial.dataset_trial ?? trial.trial,
|
|
323
540
|
datasetOrder,
|
|
324
541
|
attempt: Number(trial.attempt ?? 1),
|
|
325
|
-
status
|
|
542
|
+
status,
|
|
543
|
+
scoringStatus: status === 'completed-unscored' ? 'unscored' : score.valid ? 'scored' : 'invalid',
|
|
326
544
|
terminal: trial.terminal ?? true,
|
|
327
545
|
updatedAt: trial.updatedAt ?? trial.updated_at,
|
|
328
546
|
score,
|
|
@@ -574,11 +792,22 @@ export async function readJobProgress(config, args) {
|
|
|
574
792
|
|
|
575
793
|
export async function readMetaEvaluation(config, args = {}) {
|
|
576
794
|
const evaluationRoot = resolveWithin(config.projectRoot, args.evaluationRoot ?? '.', 'evaluationRoot')
|
|
577
|
-
const
|
|
578
|
-
const
|
|
795
|
+
const index = await readJson(path.join(evaluationRoot, '.harbor', 'meta-artifacts.json'))
|
|
796
|
+
const registered = index?.schema_version === 1 ? index.artifacts ?? {} : {}
|
|
797
|
+
const groundTruthPath = resolveWithin(evaluationRoot, registered.ground_truth ?? '.harbor/ground-truth.json', 'groundTruthPath')
|
|
798
|
+
const reportPath = resolveWithin(evaluationRoot, registered.meta_evaluation_report ?? '.harbor/meta-evaluation-report.json', 'metaEvaluationReportPath')
|
|
799
|
+
const groundTruth = await readJson(groundTruthPath, { maxText: 64_000 })
|
|
800
|
+
const report = await readJson(reportPath, { maxText: 64_000 })
|
|
579
801
|
const availableGroundTruth = groundTruth && !groundTruth.__readError
|
|
580
802
|
const availableReport = report && !report.__readError
|
|
581
803
|
const cases = availableGroundTruth && Array.isArray(groundTruth.cases) ? groundTruth.cases : []
|
|
804
|
+
const disagreementOffset = Math.max(0, Number.parseInt(args.offset ?? 0, 10) || 0)
|
|
805
|
+
const disagreementLimit = Math.min(100, Math.max(1, Number.parseInt(args.limit ?? 20, 10) || 20))
|
|
806
|
+
const disagreements = availableReport && Array.isArray(report.disagreements) ? report.disagreements : []
|
|
807
|
+
const pagedReport = availableReport ? {
|
|
808
|
+
...report,
|
|
809
|
+
disagreements: disagreements.slice(disagreementOffset, disagreementOffset + disagreementLimit),
|
|
810
|
+
} : undefined
|
|
582
811
|
return {
|
|
583
812
|
schemaVersion: 1,
|
|
584
813
|
evaluationRoot: path.relative(config.projectRoot, evaluationRoot) || '.',
|
|
@@ -590,9 +819,16 @@ export async function readMetaEvaluation(config, args = {}) {
|
|
|
590
819
|
criteria: groundTruth.criteria ?? [],
|
|
591
820
|
caseCount: cases.length,
|
|
592
821
|
badcaseCount: cases.filter(item => item?.badcase).length,
|
|
593
|
-
path: path.relative(config.projectRoot,
|
|
822
|
+
path: path.relative(config.projectRoot, groundTruthPath),
|
|
594
823
|
} : undefined,
|
|
595
|
-
report:
|
|
824
|
+
report: pagedReport,
|
|
825
|
+
artifactIndex: index?.schema_version === 1 ? path.relative(config.projectRoot, path.join(evaluationRoot, '.harbor', 'meta-artifacts.json')) : undefined,
|
|
826
|
+
disagreementPagination: {
|
|
827
|
+
offset: disagreementOffset,
|
|
828
|
+
limit: disagreementLimit,
|
|
829
|
+
total: disagreements.length,
|
|
830
|
+
hasMore: disagreementOffset + disagreementLimit < disagreements.length,
|
|
831
|
+
},
|
|
596
832
|
workflow: {
|
|
597
833
|
candidate: 'Evaluator / Rubric / Judge identity',
|
|
598
834
|
dataset: 'Fixed artifacts plus independent Ground Truth',
|
|
@@ -627,6 +863,32 @@ export async function readComparison(config, args) {
|
|
|
627
863
|
if (!baseline || baseline.__readError || !candidate || candidate.__readError) throw new Error('Both Job summaries are required')
|
|
628
864
|
const baselineContext = baseline.evaluation_context ?? await readJson(path.join(jobDirectory(config, baselineJob), 'evaluation-context.json'))
|
|
629
865
|
const candidateContext = candidate.evaluation_context ?? await readJson(path.join(jobDirectory(config, candidateJob), 'evaluation-context.json'))
|
|
866
|
+
const baselineKind = normalizedJobKind(baseline, baselineContext)
|
|
867
|
+
const candidateKind = normalizedJobKind(candidate, candidateContext)
|
|
868
|
+
if (baselineKind !== CANDIDATE_JOB_KIND || candidateKind !== CANDIDATE_JOB_KIND) {
|
|
869
|
+
const error = {
|
|
870
|
+
code: 'UNSUPPORTED_JOB_KIND_FOR_PROMOTION',
|
|
871
|
+
message: 'Historical Generation Evaluation Jobs are diagnostic evidence and cannot be used as a Candidate baseline, comparison, or Promotion Gate input.',
|
|
872
|
+
}
|
|
873
|
+
return {
|
|
874
|
+
schemaVersion: 1,
|
|
875
|
+
baselineJob,
|
|
876
|
+
candidateJob,
|
|
877
|
+
baselineJobKind: baselineKind,
|
|
878
|
+
candidateJobKind: candidateKind,
|
|
879
|
+
comparable: false,
|
|
880
|
+
comparabilityReasons: [error],
|
|
881
|
+
metrics: {},
|
|
882
|
+
population: {},
|
|
883
|
+
improvedTrials: [],
|
|
884
|
+
regressedTrials: [],
|
|
885
|
+
newExceptions: [],
|
|
886
|
+
artifactRegressions: [],
|
|
887
|
+
gateEligibility: 'not-applicable',
|
|
888
|
+
error,
|
|
889
|
+
note: 'Convert reviewed badcases into a fixed regression Dataset before running Candidate comparison or Gate.',
|
|
890
|
+
}
|
|
891
|
+
}
|
|
630
892
|
const reasons = []
|
|
631
893
|
if (baselineContext?.schema_version !== 2 || candidateContext?.schema_version !== 2) reasons.push('Context v2 is required')
|
|
632
894
|
if (!baselineContext?.digest || baselineContext.digest !== candidateContext?.digest) reasons.push('Evaluation Context differs; establish a fresh baseline')
|
|
@@ -668,16 +930,27 @@ export async function readComparison(config, args) {
|
|
|
668
930
|
export async function readEvaluatorGovernance(config, args) {
|
|
669
931
|
const job = safeSegment(args.job, 'job')
|
|
670
932
|
const directory = jobDirectory(config, job)
|
|
671
|
-
const [stack, contract, context] = await Promise.all([
|
|
933
|
+
const [stack, sources, contract, context] = await Promise.all([
|
|
672
934
|
readJson(path.join(directory, 'evaluation-stack-manifest.json')),
|
|
935
|
+
readJson(path.join(directory, 'evaluation-stack-sources.json'), { maxText: MAX_SOURCE_BYTES }),
|
|
673
936
|
readJson(path.join(directory, 'evaluation-contract.json')),
|
|
674
937
|
readJson(path.join(directory, 'evaluation-context.json')),
|
|
675
938
|
])
|
|
676
939
|
if (!stack || stack.__readError) throw new Error('Evaluation Stack is unavailable')
|
|
940
|
+
const historicalSources = sources?.schema_version === 1 && sources.stack_digest === stack.digest
|
|
941
|
+
? sources
|
|
942
|
+
: undefined
|
|
677
943
|
const components = {}
|
|
678
944
|
for (const [role, component] of Object.entries(stack.components ?? {})) {
|
|
679
945
|
const entry = component?.entry
|
|
680
|
-
const
|
|
946
|
+
const snapshot = historicalSources?.components?.[role]
|
|
947
|
+
const snapshotFile = snapshot?.files?.find(item => item.path === entry && item.text)
|
|
948
|
+
?? snapshot?.files?.find(item => item.text)
|
|
949
|
+
const source = snapshotFile
|
|
950
|
+
? { ...snapshotFile, source: 'job-snapshot', readOnly: true }
|
|
951
|
+
: entry
|
|
952
|
+
? { ...(await readSafeText(resolveWithin(config.projectRoot, entry, `${role}.entry`), config.projectRoot)), source: 'historical-live-fallback', readOnly: true }
|
|
953
|
+
: { error: 'entry unavailable', source: 'unavailable', readOnly: true }
|
|
681
954
|
components[role] = { ...component, source }
|
|
682
955
|
}
|
|
683
956
|
let comparison
|
|
@@ -697,7 +970,7 @@ export async function readEvaluatorGovernance(config, args) {
|
|
|
697
970
|
}
|
|
698
971
|
}
|
|
699
972
|
return {
|
|
700
|
-
schemaVersion: 1, job, stackIdentity: { id: stack.stack_id, version: stack.version, digest: stack.digest },
|
|
973
|
+
schemaVersion: 1, job, stackIdentity: { id: stack.stack_id, version: stack.version, digest: stack.digest, comparisonDigest: stack.comparison_digest },
|
|
701
974
|
judge: stack.judge, contract, contextDigest: context?.digest, components, comparison,
|
|
702
975
|
editingPolicy: {
|
|
703
976
|
browserWriteEnabled: false,
|