dsh-harbor-evolution 0.7.2 → 0.7.3

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/service.js CHANGED
@@ -1,7 +1,10 @@
1
- import { access } from 'node:fs/promises'
1
+ import { access, stat } from 'node:fs/promises'
2
2
  import path from 'node:path'
3
3
 
4
+ import { loadModelBinding } from './candidate.js'
5
+
4
6
  import {
7
+ discoverWorkspaceConfigs,
5
8
  readComparison,
6
9
  readDashboardSnapshot,
7
10
  readDatasetPreview,
@@ -16,6 +19,7 @@ import {
16
19
  compareCandidates,
17
20
  initializeGroundTruth,
18
21
  initializeProject,
22
+ initializeQuickDiagnostic,
19
23
  inspectEvaluator,
20
24
  previewContext,
21
25
  readEvaluation,
@@ -25,7 +29,9 @@ import {
25
29
  snapshot,
26
30
  updateEvaluator,
27
31
  validateDataset,
32
+ resolveWithin,
28
33
  } from './evolution.js'
34
+ import { createVersionChecker } from './version.js'
29
35
 
30
36
  export async function resolveEvaluatorStackPath(config, governance, explicitPath) {
31
37
  if (explicitPath) return explicitPath
@@ -49,9 +55,54 @@ export async function resolveEvaluatorStackPath(config, governance, explicitPath
49
55
  /** One Host-side boundary shared by Agent tools and the Web dashboard. */
50
56
  export class EvolutionService {
51
57
  constructor(config, metadata = {}, modelRuntime) {
52
- this.config = config
58
+ this.config = { jobsDir: 'jobs', ...config }
53
59
  this.metadata = metadata
54
60
  this.modelRuntime = modelRuntime
61
+ this.versionChecker = metadata.versionChecker ?? createVersionChecker()
62
+ this.projectRoots = new Map()
63
+ this.workspaceConfigs = new Map()
64
+ this.activeProjectRoot = path.resolve(this.config.projectRoot)
65
+ this._registerProjectRoot(this.activeProjectRoot, metadata.projectRootSource ?? 'configured')
66
+ }
67
+
68
+ _registerProjectRoot(projectRoot, source) {
69
+ const resolved = path.resolve(projectRoot)
70
+ this.projectRoots.set(resolved, { projectRoot: resolved, source, activatedAt: new Date().toISOString() })
71
+ this.activeProjectRoot = resolved
72
+ return this.projectRoots.get(resolved)
73
+ }
74
+
75
+ async _refreshWorkspaces() {
76
+ const discovered = []
77
+ this.workspaceConfigs.clear()
78
+ for (const [identity, root] of this.projectRoots.entries()) {
79
+ try {
80
+ const details = await stat(root.projectRoot)
81
+ if (!details.isDirectory()) throw new Error('not a directory')
82
+ } catch {
83
+ this.projectRoots.delete(identity)
84
+ continue
85
+ }
86
+ const configs = await discoverWorkspaceConfigs({ ...this.config, projectRoot: root.projectRoot })
87
+ for (const config of configs) {
88
+ const value = { ...config, projectRootSource: root.source }
89
+ this.workspaceConfigs.set(config.workspaceId, value)
90
+ discovered.push(value)
91
+ }
92
+ }
93
+ return discovered
94
+ }
95
+
96
+ async _webContext(args = {}) {
97
+ const workspaces = await this._refreshWorkspaces()
98
+ const requested = String(args.workspace ?? '').trim()
99
+ let config = requested ? this.workspaceConfigs.get(requested) : undefined
100
+ if (requested && !config) throw new Error('Workspace is unavailable; reload Harbor and select an active workspace')
101
+ config ??= workspaces.find(item => item.projectRoot === this.activeProjectRoot && item.workspaceRoot === '.')
102
+ ?? workspaces.find(item => item.projectRoot === this.activeProjectRoot)
103
+ ?? workspaces[0]
104
+ if (!config) throw new Error('No Harbor workspace is available')
105
+ return { config, workspaces }
55
106
  }
56
107
 
57
108
  snapshot(args) {
@@ -62,8 +113,22 @@ export class EvolutionService {
62
113
  return initializeProject(this.config, args)
63
114
  }
64
115
 
116
+ quickDiagnostic(args) {
117
+ return initializeQuickDiagnostic(this.config, args)
118
+ }
119
+
120
+ async _resolveCandidateModel(args) {
121
+ const candidatePath = resolveWithin(
122
+ this.config.projectRoot,
123
+ args.candidatePath,
124
+ 'candidatePath',
125
+ )
126
+ const pinnedBinding = await loadModelBinding(candidatePath)
127
+ return this.modelRuntime.resolve(args, pinnedBinding)
128
+ }
129
+
65
130
  async run(args) {
66
- const candidateModelBinding = await this.modelRuntime.resolve(args)
131
+ const candidateModelBinding = await this._resolveCandidateModel(args)
67
132
  return runEvaluation(this.config, { ...args, candidateModelBinding }, this.modelRuntime)
68
133
  }
69
134
 
@@ -85,7 +150,7 @@ export class EvolutionService {
85
150
  }
86
151
 
87
152
  async doctor(args) {
88
- const candidateModelBinding = await this.modelRuntime.resolve(args)
153
+ const candidateModelBinding = await this._resolveCandidateModel(args)
89
154
  const result = await runDoctor(this.config, args)
90
155
  return { ...result, candidate_model_binding: candidateModelBinding }
91
156
  }
@@ -95,54 +160,141 @@ export class EvolutionService {
95
160
  }
96
161
 
97
162
  async previewContext(args) {
98
- const candidateModelBinding = await this.modelRuntime.resolve(args)
163
+ const candidateModelBinding = await this._resolveCandidateModel(args)
99
164
  return previewContext(this.config, { ...args, candidateModelBinding })
100
165
  }
101
166
 
102
- dashboard() {
103
- return readDashboardSnapshot(this.config, this.metadata)
167
+ async dashboard(args = {}) {
168
+ const { config, workspaces } = await this._webContext(args)
169
+ return readDashboardSnapshot(config, {
170
+ ...this.metadata,
171
+ projectRootSource: config.projectRootSource,
172
+ workspaces: workspaces.map(item => ({
173
+ id: item.workspaceId,
174
+ label: item.workspaceLabel,
175
+ root: item.workspaceRoot,
176
+ projectRoot: item.projectRoot,
177
+ jobsDir: item.jobsDir,
178
+ stackPath: item.stackPath,
179
+ source: item.projectRootSource,
180
+ })),
181
+ }, args)
182
+ }
183
+
184
+ async version(args = {}) {
185
+ let config = this.config
186
+ if (args.workspace) ({ config } = await this._webContext(args))
187
+ else {
188
+ try { ({ config } = await this._webContext(args)) } catch {}
189
+ }
190
+ return this.versionChecker({
191
+ currentVersion: this.metadata.pluginVersion ?? 'development',
192
+ projectRoot: config.projectRoot,
193
+ refresh: args.refresh === true || args.refresh === 'true',
194
+ })
195
+ }
196
+
197
+ async modelBinding() {
198
+ const binding = await this.modelRuntime.currentBinding()
199
+ return {
200
+ schema_version: 1,
201
+ scope: 'new-candidate',
202
+ candidate_model_binding: binding,
203
+ transport: 'dsh-host-broker',
204
+ protocol: 'dsh-host-model-gateway/v1',
205
+ credentials: {
206
+ mode: 'host-broker-only',
207
+ note: 'The Candidate receives only a short-lived Job capability. Host OAuth and API credentials never enter the Candidate or Harbor artifacts.',
208
+ },
209
+ note: 'Write candidate_model_binding to model-binding.json before snapshotting. Later chat-model changes do not rewrite this Candidate.',
210
+ }
211
+ }
212
+
213
+ activateProjectRoot(requested, source = 'agent-session') {
214
+ if (!path.isAbsolute(requested)) throw new Error('projectRoot must be an absolute directory path')
215
+ const resolved = path.resolve(requested)
216
+ this._registerProjectRoot(resolved, source)
217
+ return {
218
+ projectRoot: resolved,
219
+ reloaded: true,
220
+ source,
221
+ scope: 'Web Workbench only; Agent tools remain isolated to each calling session working directory.',
222
+ }
223
+ }
224
+
225
+ async setProjectRoot(args) {
226
+ const requested = String(args?.projectRoot ?? '').trim()
227
+ if (!path.isAbsolute(requested)) throw new Error('projectRoot must be an absolute directory path')
228
+ const resolved = path.resolve(requested)
229
+ const details = await stat(resolved)
230
+ if (!details.isDirectory()) throw new Error('projectRoot must point to an existing directory')
231
+ return this.activateProjectRoot(resolved, 'manual')
104
232
  }
105
233
 
106
- job(args) {
107
- return readJobDetail(this.config, args)
234
+ async job(args) {
235
+ const { config } = await this._webContext(args)
236
+ return readJobDetail(config, args)
108
237
  }
109
238
 
110
- trials(args) {
111
- return readTrialsPage(this.config, args)
239
+ async trials(args) {
240
+ const { config } = await this._webContext(args)
241
+ return readTrialsPage(config, args)
112
242
  }
113
243
 
114
- trial(args) {
115
- return readTrialDetail(this.config, args)
244
+ async trial(args) {
245
+ const { config } = await this._webContext(args)
246
+ return readTrialDetail(config, args)
116
247
  }
117
248
 
118
- dataset(args) {
119
- return readDatasetPreview(this.config, args)
249
+ async dataset(args) {
250
+ const { config } = await this._webContext(args)
251
+ return readDatasetPreview(config, args)
120
252
  }
121
253
 
122
- progress(args) {
123
- return readJobProgress(this.config, args)
254
+ async progress(args) {
255
+ const { config } = await this._webContext(args)
256
+ return readJobProgress(config, args)
124
257
  }
125
258
 
126
- comparison(args) {
127
- return readComparison(this.config, args)
259
+ async comparison(args) {
260
+ const { config } = await this._webContext(args)
261
+ return readComparison(config, args)
128
262
  }
129
263
 
130
264
  async governance(args) {
131
- const governance = await readEvaluatorGovernance(this.config, args)
265
+ const { config } = await this._webContext(args)
266
+ const governance = await readEvaluatorGovernance(config, args)
132
267
  try {
133
- const stackPath = await resolveEvaluatorStackPath(this.config, governance, args.stackPath)
134
- governance.evaluatorInterface = await inspectEvaluator(this.config, { ...args, stackPath })
135
- governance.editingPolicy.browserWriteEnabled = true
136
- governance.editingPolicy.stackPath = governance.evaluatorInterface.stack?.path
137
- governance.editingPolicy.saveBehavior = 'Update one descriptor-authorized file with optimistic concurrency and create new Evaluator and Stack identities.'
268
+ const stackPath = await resolveEvaluatorStackPath(config, governance, args.stackPath)
269
+ const current = await inspectEvaluator(config, { ...args, stackPath })
270
+ const historicalEvaluator = governance.components?.evaluator
271
+ const identityMatches = current.stack?.id === governance.stackIdentity.id
272
+ && current.stack?.version === governance.stackIdentity.version
273
+ && current.evaluator?.evaluator_id === historicalEvaluator?.id
274
+ && current.evaluator?.version === historicalEvaluator?.version
275
+ && current.evaluator?.digest === historicalEvaluator?.digest
276
+ if (!identityMatches) {
277
+ governance.evaluatorInterface = {
278
+ error: 'The live Evaluator no longer matches this historical Job. Historical sources remain readable, but editing is disabled until you open a Job with the current Stack identity.',
279
+ }
280
+ governance.editingPolicy.identityMatch = false
281
+ } else {
282
+ governance.evaluatorInterface = current
283
+ governance.editingPolicy.browserWriteEnabled = true
284
+ governance.editingPolicy.identityMatch = true
285
+ governance.editingPolicy.stackPath = current.stack?.path
286
+ governance.editingPolicy.saveBehavior = 'Update one descriptor-authorized file with optimistic concurrency and create new Evaluator and Stack identities.'
287
+ }
138
288
  } catch (error) {
139
289
  governance.evaluatorInterface = { error: error instanceof Error ? error.message : String(error) }
290
+ governance.editingPolicy.identityMatch = false
140
291
  }
141
292
  return governance
142
293
  }
143
294
 
144
- evaluator(args) {
145
- return updateEvaluator(this.config, args)
295
+ async evaluator(args) {
296
+ const config = args.workspace ? (await this._webContext(args)).config : this.config
297
+ return updateEvaluator(config, args)
146
298
  }
147
299
 
148
300
  evaluatorInspect(args) {
@@ -158,13 +310,15 @@ export class EvolutionService {
158
310
  }
159
311
 
160
312
  async meta(args) {
161
- const governance = await readEvaluatorGovernance(this.config, args)
162
- const stackPath = await resolveEvaluatorStackPath(this.config, governance, args.stackPath)
163
- if (!stackPath) return readMetaEvaluation(this.config)
164
- const stackDirectory = path.dirname(path.resolve(this.config.projectRoot, stackPath))
313
+ const { config } = await this._webContext(args)
314
+ const governance = await readEvaluatorGovernance(config, args)
315
+ const stackPath = await resolveEvaluatorStackPath(config, governance, args.stackPath)
316
+ if (!stackPath) return readMetaEvaluation(config, args)
317
+ const stackDirectory = path.dirname(path.resolve(config.projectRoot, stackPath))
165
318
  const evaluationRoot = path.dirname(stackDirectory)
166
- return readMetaEvaluation(this.config, {
167
- evaluationRoot: path.relative(this.config.projectRoot, evaluationRoot),
319
+ return readMetaEvaluation(config, {
320
+ ...args,
321
+ evaluationRoot: path.relative(config.projectRoot, evaluationRoot),
168
322
  })
169
323
  }
170
324
  }
package/lib/setup.js CHANGED
@@ -5,14 +5,14 @@ import process from 'node:process'
5
5
  import { fileURLToPath } from 'node:url'
6
6
 
7
7
  import { runProcess } from './process.js'
8
-
9
- export const DSH_VERSION = '0.1.0-rc.6'
8
+ import { DSH_RUNTIME_VERSION } from './runtime-identity.js'
10
9
 
11
10
  const packageJson = JSON.parse(
12
11
  await readFile(new URL('../package.json', import.meta.url), 'utf8'),
13
12
  )
14
13
 
15
14
  export const INTEGRATION_VERSION = packageJson.version
15
+ export const DSH_VERSION = DSH_RUNTIME_VERSION
16
16
 
17
17
  function requireValue(args, index, flag) {
18
18
  const value = args[index + 1]
package/lib/version.js ADDED
@@ -0,0 +1,128 @@
1
+ export const NPM_PACKAGE_NAME = 'dsh-harbor-evolution'
2
+ export const NPM_LATEST_URL = `https://registry.npmjs.org/${NPM_PACKAGE_NAME}/latest`
3
+ export const RELEASES_URL = 'https://github.com/istarwyh/harbor-self-evolving/releases'
4
+
5
+ const DEFAULT_CACHE_TTL_MS = 6 * 60 * 60 * 1_000
6
+ const DEFAULT_FAILURE_TTL_MS = 5 * 60 * 1_000
7
+ const DEFAULT_TIMEOUT_MS = 2_500
8
+
9
+ export function parseSemver(value) {
10
+ const match = String(value ?? '').match(/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/)
11
+ if (!match) return undefined
12
+ return {
13
+ major: Number(match[1]),
14
+ minor: Number(match[2]),
15
+ patch: Number(match[3]),
16
+ prerelease: match[4] ? match[4].split('.') : [],
17
+ }
18
+ }
19
+
20
+ function compareIdentifier(left, right) {
21
+ const leftNumber = /^\d+$/.test(left) ? Number(left) : undefined
22
+ const rightNumber = /^\d+$/.test(right) ? Number(right) : undefined
23
+ if (leftNumber !== undefined && rightNumber !== undefined) return Math.sign(leftNumber - rightNumber)
24
+ if (leftNumber !== undefined) return -1
25
+ if (rightNumber !== undefined) return 1
26
+ return left.localeCompare(right)
27
+ }
28
+
29
+ export function compareSemver(leftValue, rightValue) {
30
+ const left = parseSemver(leftValue)
31
+ const right = parseSemver(rightValue)
32
+ if (!left || !right) return undefined
33
+ for (const key of ['major', 'minor', 'patch']) {
34
+ if (left[key] !== right[key]) return Math.sign(left[key] - right[key])
35
+ }
36
+ if (!left.prerelease.length && !right.prerelease.length) return 0
37
+ if (!left.prerelease.length) return 1
38
+ if (!right.prerelease.length) return -1
39
+ const length = Math.max(left.prerelease.length, right.prerelease.length)
40
+ for (let index = 0; index < length; index += 1) {
41
+ if (left.prerelease[index] === undefined) return -1
42
+ if (right.prerelease[index] === undefined) return 1
43
+ const comparison = compareIdentifier(left.prerelease[index], right.prerelease[index])
44
+ if (comparison) return Math.sign(comparison)
45
+ }
46
+ return 0
47
+ }
48
+
49
+ function shellQuote(value) {
50
+ return `'${String(value).replaceAll("'", `'"'"'`)}'`
51
+ }
52
+
53
+ export function renderUpdateCommand(latestVersion, projectRoot) {
54
+ if (!parseSemver(latestVersion)) throw new Error('latestVersion must be a valid semantic version')
55
+ return `npx --yes ${NPM_PACKAGE_NAME}@${latestVersion} setup --project-root ${shellQuote(projectRoot)}`
56
+ }
57
+
58
+ function buildResult(currentVersion, latestVersion, projectRoot, checkedAt, options = {}) {
59
+ const comparison = compareSemver(currentVersion, latestVersion)
60
+ if (comparison === undefined) {
61
+ return { status: 'unavailable', currentVersion, checkedAt, source: options.source, stale: options.stale }
62
+ }
63
+ const updateAvailable = comparison < 0
64
+ return {
65
+ status: updateAvailable ? 'update-available' : 'up-to-date',
66
+ currentVersion,
67
+ latestVersion,
68
+ checkedAt,
69
+ source: options.source,
70
+ stale: options.stale,
71
+ releaseUrl: updateAvailable ? `${RELEASES_URL}/tag/v${latestVersion}` : RELEASES_URL,
72
+ command: updateAvailable ? renderUpdateCommand(latestVersion, projectRoot) : undefined,
73
+ }
74
+ }
75
+
76
+ export function createVersionChecker(options = {}) {
77
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch
78
+ const now = options.now ?? Date.now
79
+ const cacheTtlMs = options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS
80
+ const failureTtlMs = options.failureTtlMs ?? DEFAULT_FAILURE_TTL_MS
81
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS
82
+ let successful
83
+ let failedAt = 0
84
+
85
+ return async function checkVersion({ currentVersion, projectRoot, refresh = false }) {
86
+ const checkedAt = new Date(now()).toISOString()
87
+ if (!refresh && successful && now() < successful.expiresAt) {
88
+ return buildResult(currentVersion, successful.latestVersion, projectRoot, successful.checkedAt, { source: 'cache' })
89
+ }
90
+ if (!refresh && !successful && failedAt && now() - failedAt < failureTtlMs) {
91
+ return { status: 'unavailable', currentVersion, checkedAt, source: 'cache' }
92
+ }
93
+ if (typeof fetchImpl !== 'function') {
94
+ failedAt = now()
95
+ return { status: 'unavailable', currentVersion, checkedAt, source: 'host' }
96
+ }
97
+
98
+ const controller = new AbortController()
99
+ const timeout = setTimeout(() => controller.abort(), timeoutMs)
100
+ try {
101
+ const response = await fetchImpl(NPM_LATEST_URL, {
102
+ headers: { accept: 'application/json' },
103
+ redirect: 'error',
104
+ signal: controller.signal,
105
+ })
106
+ if (!response.ok) throw new Error('registry request failed')
107
+ const manifest = await response.json()
108
+ if (manifest?.name !== NPM_PACKAGE_NAME || !parseSemver(manifest.version)) {
109
+ throw new Error('registry response is invalid')
110
+ }
111
+ successful = {
112
+ latestVersion: manifest.version,
113
+ checkedAt,
114
+ expiresAt: now() + cacheTtlMs,
115
+ }
116
+ failedAt = 0
117
+ return buildResult(currentVersion, successful.latestVersion, projectRoot, checkedAt, { source: 'registry' })
118
+ } catch {
119
+ failedAt = now()
120
+ if (successful) {
121
+ return buildResult(currentVersion, successful.latestVersion, projectRoot, successful.checkedAt, { source: 'cache', stale: true })
122
+ }
123
+ return { status: 'unavailable', currentVersion, checkedAt, source: 'registry' }
124
+ } finally {
125
+ clearTimeout(timeout)
126
+ }
127
+ }
128
+ }
package/lib/web.js CHANGED
@@ -8,6 +8,8 @@ export const COMPARE_ROUTE = '/_dsh/harbor-evolution/compare'
8
8
  export const GOVERNANCE_ROUTE = '/_dsh/harbor-evolution/governance'
9
9
  export const EVALUATOR_ROUTE = '/_dsh/harbor-evolution/evaluator'
10
10
  export const META_ROUTE = '/_dsh/harbor-evolution/meta'
11
+ export const PROJECT_ROOT_ROUTE = '/_dsh/harbor-evolution/project-root'
12
+ export const VERSION_ROUTE = '/_dsh/harbor-evolution/version'
11
13
  const MAX_MUTATION_BYTES = 256 * 1024
12
14
 
13
15
  function sendJson(response, status, body) {
@@ -64,7 +66,7 @@ export function createApiHandler(load, code = 'request-failed') {
64
66
  }
65
67
 
66
68
  export function createDashboardHandler(service) {
67
- return createApiHandler(() => service.dashboard(), 'dashboard-unavailable')
69
+ return createApiHandler(args => service.dashboard(args), 'dashboard-unavailable')
68
70
  }
69
71
 
70
72
  export function createMutationHandler(update, code = 'update-failed') {
@@ -116,6 +118,8 @@ export function installDashboardWeb(ctx, service) {
116
118
  [GOVERNANCE_ROUTE, createApiHandler(args => service.governance(args), 'governance-unavailable')],
117
119
  [EVALUATOR_ROUTE, createMutationHandler(args => service.evaluator(args), 'evaluator-update-failed')],
118
120
  [META_ROUTE, createApiHandler(args => service.meta(args), 'meta-evaluation-unavailable')],
121
+ [VERSION_ROUTE, createApiHandler(args => service.version(args), 'version-check-unavailable')],
122
+ [PROJECT_ROOT_ROUTE, createMutationHandler(args => service.setProjectRoot(args), 'project-root-update-failed')],
119
123
  ]
120
124
  for (const [route, handler] of routes) {
121
125
  webCtx.effect(() => webCtx.webServer.register({ kind: 'exact', path: route, handler }), `harbor-evolution: ${route}`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-harbor-evolution",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
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",
@@ -48,9 +48,14 @@
48
48
  "platform": "web"
49
49
  }
50
50
  },
51
+ "harborEvolution": {
52
+ "runtimePolicy": "follow-latest",
53
+ "dshRuntimeVersion": "latest",
54
+ "candidateAcpPackage": "@deepseek-ai/dsh-acp-demo@latest"
55
+ },
51
56
  "peerDependencies": {
52
- "@deepseek-ai/dsh-skill": "^0.1.0-rc.6",
53
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.6"
57
+ "@deepseek-ai/dsh-skill": ">=0.1.0-rc.6",
58
+ "@deepseek-ai/dsh-tools": ">=0.1.0-rc.6"
54
59
  },
55
60
  "dependencies": {
56
61
  "@deepseek-ai/schemastery": "3.18.1"