dsh-harbor-evolution 0.8.1 → 0.8.2
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 +5 -4
- package/index.js +3 -1
- package/lib/client.js +207 -4
- package/lib/historical-web.js +180 -0
- package/lib/service.js +9 -0
- package/lib/session-diagnostic.js +25 -7
- package/lib/web.js +9 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,8 +14,8 @@ npx --yes dsh-harbor-evolution@latest setup --project-root "$PWD"
|
|
|
14
14
|
|
|
15
15
|
The setup command installs both required runtimes:
|
|
16
16
|
|
|
17
|
-
- `harbor-dsh-evolution==0.8.
|
|
18
|
-
- `dsh-harbor-evolution@0.8.
|
|
17
|
+
- `harbor-dsh-evolution==0.8.2` in a managed Python environment.
|
|
18
|
+
- `dsh-harbor-evolution@0.8.2` in the selected DSH profile.
|
|
19
19
|
|
|
20
20
|
It then stores the absolute Harbor executable paths and a fallback `projectRoot` in the profile's `harbor-evolution` block and verifies the integration. Agent Tool calls always use the calling session's absolute working directory as their project root; the configured value remains the Web Workbench and non-Agent fallback. Existing unrelated profile entries are preserved, and rerunning setup updates the same block.
|
|
21
21
|
|
|
@@ -58,18 +58,19 @@ The Plugin registers:
|
|
|
58
58
|
In the `web` profile, the same package also registers:
|
|
59
59
|
|
|
60
60
|
- a localized nine-stage Workbench that directly exposes fixed experiment identities, Agent-visible Dataset queries/instructions, safe business-artifact previews, Ground Truth meta-evaluation, paginated per-Trial evidence and recommendations, Population validity/coverage, controlled optimization hypotheses, and Baseline/Gate deltas; raw JSON remains in the audit drawer;
|
|
61
|
+
- a first-class `Evaluate recent Sessions` action that previews up to ten safe Session records, shows the frozen Evaluator/Judge and diagnostic boundaries, requires explicit confirmation, runs in the background, and opens the completed Job;
|
|
61
62
|
- descriptor-authorized Evaluator/Rubric source editing for `script` and `llm-as-judge` implementations, with optimistic concurrency and mandatory new identities;
|
|
62
63
|
- a `harbor-dsh-evaluator/v1` interface shared by deterministic scripts and LLM-as-Judge implementations;
|
|
63
64
|
- compact result cards for all Harbor Tool calls;
|
|
64
65
|
- a `Harbor Evolution` Settings section that checks the configured project, Evaluation Stack, Jobs directory, and CLI paths, supports process-local `projectRoot` reload, and checks npm for a newer formal release without silently installing it.
|
|
65
66
|
|
|
66
|
-
The Web UI is
|
|
67
|
+
The Web UI is read-only except for two narrow, explicit workflows: descriptor-authorized Evaluator source updates and the confirmed Historical Session launcher. The launcher follows `Preview → confirm → background run → open Job`; its private selection token never enters browser state. Page refreshes never start Jobs, and Candidate evaluation, comparison, Gate, promotion, deployment, and publishing remain explicit Agent + Skill workflows.
|
|
67
68
|
|
|
68
69
|
A direct evaluation requires `candidatePath`, `datasetPath`, `stackPath`, and explicit `mode`; `promotion-eligible` additionally requires `policyPath`. Prefer the Skill because it will not run or compare Jobs until the material identities and evaluation contract are resolved.
|
|
69
70
|
|
|
70
71
|
## Historical Session cold start
|
|
71
72
|
|
|
72
|
-
When the user does not provide a Dataset, the
|
|
73
|
+
When the user does not provide a Dataset, the simplest entry is the `Evaluate recent Sessions` button in the Harbor tab. It previews up to ten recent completed business Sessions, shows only safe metadata plus the Evaluator, Judge, same-model coupling, estimated requests, expiry and local retention boundaries, and starts nothing until the user confirms. The Host keeps the short-lived selection token in memory; the browser receives only an opaque Preview id. The bundled Skill remains the conversational entry and uses the same Preview/Run services from the Agent's exact working directory.
|
|
73
74
|
|
|
74
75
|
After explicit confirmation, `harbor_session_diagnostic_run` receives only the `selectionToken` and an optional Job name. It revalidates the frozen Session and Feedback digests, materializes an immutable Historical Batch plus matching Dataset and Stack, and evaluates one Session Observation per Harbor Trial. The Job does not rerun a Candidate, cannot enter Promotion Gate, and records Evaluator Meta-Evaluation as `not-run` because evaluator reliability requires a separate independent Ground Truth workflow.
|
|
75
76
|
|
package/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { loadBundledSkill } from './lib/official-skill.js'
|
|
|
8
8
|
import { CandidateModelRuntime } from './lib/model-runtime.js'
|
|
9
9
|
import { RUNTIME_POLICY } from './lib/runtime-identity.js'
|
|
10
10
|
import { SessionDiagnosticService } from './lib/session-diagnostic.js'
|
|
11
|
+
import { HistoricalWebController } from './lib/historical-web.js'
|
|
11
12
|
import { EvolutionService } from './lib/service.js'
|
|
12
13
|
import { runHistoricalEvaluation } from './lib/evolution.js'
|
|
13
14
|
import { installDashboardWeb } from './lib/web.js'
|
|
@@ -95,13 +96,14 @@ export function apply(ctx, config) {
|
|
|
95
96
|
modelRuntime,
|
|
96
97
|
runHistoricalEvaluation,
|
|
97
98
|
})
|
|
99
|
+
const historicalWeb = new HistoricalWebController({ service, sessionDiagnostic })
|
|
98
100
|
const serviceForTool = exec => {
|
|
99
101
|
const projectRoot = synchronizeWorkbenchProjectRoot(service, exec)
|
|
100
102
|
return new EvolutionService({ ...resolved, projectRoot }, metadata, modelRuntime)
|
|
101
103
|
}
|
|
102
104
|
|
|
103
105
|
ctx.skills.register(loadBundledSkill())
|
|
104
|
-
installDashboardWeb(ctx, service)
|
|
106
|
+
installDashboardWeb(ctx, service, historicalWeb)
|
|
105
107
|
|
|
106
108
|
ctx.tools.register(jsonTool({
|
|
107
109
|
name: 'harbor_candidate_snapshot',
|
package/lib/client.js
CHANGED
|
@@ -63,7 +63,44 @@ var dictionaries = {
|
|
|
63
63
|
jobsHint: "\u70B9\u51FB Job \u540E\uFF0C\u6700\u591A\u518D\u70B9\u4E00\u6B21\u5373\u53EF\u8FDB\u5165\u5BF9\u5E94 Trial \u7684\u8BC1\u636E\u3002",
|
|
64
64
|
workspace: "\u5DE5\u4F5C\u7A7A\u95F4",
|
|
65
65
|
workspaceSelect: "\u9009\u62E9 Harbor \u5DE5\u4F5C\u7A7A\u95F4",
|
|
66
|
-
empty: "\u8FD8\u6CA1\u6709
|
|
66
|
+
empty: "\u8FD8\u6CA1\u6709 Harbor Job\u3002\u53EF\u4EE5\u5148\u8BC4\u6D4B\u8FD9\u4E2A\u5DE5\u4F5C\u7A7A\u95F4\u6700\u8FD1\u5B8C\u6210\u7684\u771F\u5B9E\u4F1A\u8BDD\u3002",
|
|
67
|
+
historicalLaunch: "\u8BC4\u6D4B\u6700\u8FD1\u4F1A\u8BDD",
|
|
68
|
+
historicalLaunchShort: "\u5F00\u59CB\u8BC4\u6D4B",
|
|
69
|
+
historicalLaunchHint: "\u6700\u591A 10 \u6761 \xB7 \u5148\u9884\u89C8\u518D\u8FD0\u884C",
|
|
70
|
+
historicalLaunchBody: "\u7528\u5F53\u524D DSH Agent \u5DF2\u5B8C\u6210\u7684\u771F\u5B9E\u4EFB\u52A1\u505A\u8BCA\u65AD\uFF0C\u4E0D\u91CD\u65B0\u8FD0\u884C Candidate\u3002",
|
|
71
|
+
historicalPreparing: "\u6B63\u5728\u67E5\u627E\u53EF\u8BC4\u6D4B\u4F1A\u8BDD\u2026",
|
|
72
|
+
historicalPreparingShort: "\u8BFB\u53D6\u4E2D\u2026",
|
|
73
|
+
historicalPreviewTitle: "\u786E\u8BA4\u5386\u53F2\u4F1A\u8BDD\u8BC4\u6D4B",
|
|
74
|
+
historicalPreviewHint: "\u8FD9\u91CC\u53EA\u5C55\u793A\u5B89\u5168\u5143\u6570\u636E\u3002\u786E\u8BA4\u524D\u4E0D\u4F1A\u5199\u5165 Batch\uFF0C\u4E5F\u4E0D\u4F1A\u542F\u52A8 Harbor Job\u3002",
|
|
75
|
+
historicalConfirm: "\u786E\u8BA4\u5E76\u5F00\u59CB\u8BC4\u6D4B",
|
|
76
|
+
historicalStarting: "\u6B63\u5728\u542F\u52A8\u2026",
|
|
77
|
+
historicalRunning: "\u5386\u53F2\u4F1A\u8BDD\u8BC4\u6D4B\u8FD0\u884C\u4E2D",
|
|
78
|
+
historicalRunningHint: "\u53EF\u4EE5\u5173\u95ED\u6B64\u7A97\u53E3\u7EE7\u7EED\u5DE5\u4F5C\u3002Harbor \u4F1A\u5728\u540E\u53F0\u8FD0\u884C\uFF0C\u5B8C\u6210\u540E\u81EA\u52A8\u6253\u5F00 Job\u3002",
|
|
79
|
+
historicalActive: "\u67E5\u770B\u8FD0\u884C\u72B6\u6001",
|
|
80
|
+
historicalActiveShort: "\u67E5\u770B\u72B6\u6001",
|
|
81
|
+
historicalCompleted: "\u8BC4\u6D4B\u5B8C\u6210\uFF0C\u6B63\u5728\u6253\u5F00 Job\u2026",
|
|
82
|
+
recentSessions: "\u672C\u6B21\u4F1A\u8BDD\u6837\u672C",
|
|
83
|
+
selectedSessions: "\u9009\u4E2D\u4F1A\u8BDD",
|
|
84
|
+
requestEstimate: "\u9884\u8BA1 Judge \u8BF7\u6C42",
|
|
85
|
+
tokenExpiry: "\u9884\u89C8\u6709\u6548\u671F",
|
|
86
|
+
generatorRole: "\u751F\u6210\u5668",
|
|
87
|
+
generatorRoleValue: "\u4EA7\u751F\u8FD9\u4E9B\u4F1A\u8BDD\u7684 DSH Agent",
|
|
88
|
+
evaluatorIdentity: "\u8BC4\u6D4B\u5668\u8EAB\u4EFD",
|
|
89
|
+
judgeIdentity: "Judge \u8EAB\u4EFD",
|
|
90
|
+
coupling: "\u6A21\u578B\u8026\u5408",
|
|
91
|
+
evidenceRetention: "\u8BC1\u636E\u4FDD\u7559",
|
|
92
|
+
historicalBoundaries: "\u672C\u6B21\u8FD0\u884C\u8FB9\u754C",
|
|
93
|
+
historicalBoundaryDetail: "\u4E0D\u8FD0\u884C Candidate \xB7 \u4E0D\u505A\u8BC4\u6D4B\u5668\u5143\u8BC4\u6D4B \xB7 \u4E0D\u8FDB\u5165 Gate / \u664B\u7EA7",
|
|
94
|
+
feedbackCounts: "\u53CD\u9988",
|
|
95
|
+
turnCounts: "\u8F6E\u6B21",
|
|
96
|
+
toolCounts: "\u5DE5\u5177\u8C03\u7528",
|
|
97
|
+
previewAgain: "\u91CD\u65B0\u9884\u89C8",
|
|
98
|
+
recent30Days: "\u4EC5\u770B\u6700\u8FD1 30 \u5929",
|
|
99
|
+
noEligibleHint: "\u5F53\u524D\u5DE5\u4F5C\u7A7A\u95F4\u6CA1\u6709\u7B26\u5408\u6761\u4EF6\u7684\u5DF2\u5B8C\u6210\u9876\u5C42\u4F1A\u8BDD\u3002\u5148\u5728\u8FD9\u4E2A\u76EE\u5F55\u5B8C\u6210\u4E00\u4E2A\u6709\u7528\u6237\u8F93\u5165\u548C Agent \u8F93\u51FA\u7684\u771F\u5B9E\u4EFB\u52A1\uFF0C\u6216\u6539\u7528\u663E\u5F0F Dataset\u3002",
|
|
100
|
+
narrowScanHint: "\u8FD9\u4E2A\u5DE5\u4F5C\u7A7A\u95F4\u7684\u4F1A\u8BDD\u592A\u591A\u3002\u53EF\u4EE5\u628A\u626B\u63CF\u8303\u56F4\u7F29\u5230\u6700\u8FD1 30 \u5929\u540E\u91CD\u8BD5\u3002",
|
|
101
|
+
changedSessionHint: "\u9884\u89C8\u540E\u4F1A\u8BDD\u3001\u53CD\u9988\u6216\u5DE5\u4F5C\u7A7A\u95F4\u53D1\u751F\u4E86\u53D8\u5316\u3002\u4E3A\u4E86\u907F\u514D\u8BC4\u9519\u8BC1\u636E\uFF0C\u8BF7\u91CD\u65B0\u9884\u89C8\u3002",
|
|
102
|
+
historicalGenericError: "\u6CA1\u6709\u542F\u52A8 Job\u3002\u8BF7\u68C0\u67E5\u63D0\u793A\u540E\u91CD\u65B0\u9884\u89C8\u3002",
|
|
103
|
+
cancel: "\u53D6\u6D88",
|
|
67
104
|
completed: "\u5DF2\u5B8C\u6210",
|
|
68
105
|
partial: "\u5B8C\u6210\u4F46\u6709\u5F02\u5E38",
|
|
69
106
|
failed: "\u8BFB\u53D6\u5931\u8D25",
|
|
@@ -300,7 +337,44 @@ var dictionaries = {
|
|
|
300
337
|
jobsHint: "Open a Job, then reach Trial evidence in at most one more interaction.",
|
|
301
338
|
workspace: "Workspace",
|
|
302
339
|
workspaceSelect: "Select Harbor workspace",
|
|
303
|
-
empty: "No
|
|
340
|
+
empty: "No Harbor Jobs yet. Start by evaluating recent completed Sessions in this workspace.",
|
|
341
|
+
historicalLaunch: "Evaluate recent Sessions",
|
|
342
|
+
historicalLaunchShort: "Start evaluation",
|
|
343
|
+
historicalLaunchHint: "Up to 10 \xB7 preview before running",
|
|
344
|
+
historicalLaunchBody: "Diagnose real tasks already completed by the current DSH Agent without rerunning a Candidate.",
|
|
345
|
+
historicalPreparing: "Finding eligible Sessions\u2026",
|
|
346
|
+
historicalPreparingShort: "Loading\u2026",
|
|
347
|
+
historicalPreviewTitle: "Confirm Historical Session evaluation",
|
|
348
|
+
historicalPreviewHint: "Only safe metadata is shown. No Batch is written and no Harbor Job starts until you confirm.",
|
|
349
|
+
historicalConfirm: "Confirm and start evaluation",
|
|
350
|
+
historicalStarting: "Starting\u2026",
|
|
351
|
+
historicalRunning: "Historical Session evaluation is running",
|
|
352
|
+
historicalRunningHint: "You can close this window and keep working. Harbor runs in the background and opens the Job when it completes.",
|
|
353
|
+
historicalActive: "View run status",
|
|
354
|
+
historicalActiveShort: "View status",
|
|
355
|
+
historicalCompleted: "Evaluation complete. Opening the Job\u2026",
|
|
356
|
+
recentSessions: "Session sample",
|
|
357
|
+
selectedSessions: "Selected Sessions",
|
|
358
|
+
requestEstimate: "Estimated Judge requests",
|
|
359
|
+
tokenExpiry: "Preview expires",
|
|
360
|
+
generatorRole: "Generator",
|
|
361
|
+
generatorRoleValue: "The DSH Agent that produced these Sessions",
|
|
362
|
+
evaluatorIdentity: "Evaluator identity",
|
|
363
|
+
judgeIdentity: "Judge identity",
|
|
364
|
+
coupling: "Model coupling",
|
|
365
|
+
evidenceRetention: "Evidence retention",
|
|
366
|
+
historicalBoundaries: "Run boundaries",
|
|
367
|
+
historicalBoundaryDetail: "No Candidate run \xB7 no Evaluator meta-evaluation \xB7 no Gate or promotion",
|
|
368
|
+
feedbackCounts: "Feedback",
|
|
369
|
+
turnCounts: "Turns",
|
|
370
|
+
toolCounts: "Tool calls",
|
|
371
|
+
previewAgain: "Preview again",
|
|
372
|
+
recent30Days: "Only last 30 days",
|
|
373
|
+
noEligibleHint: "No eligible completed top-level Sessions were found in this workspace. Complete a real task here with direct user input and Agent output, or use an explicit Dataset.",
|
|
374
|
+
narrowScanHint: "This workspace has too many Sessions to scan safely. Narrow the scan to the last 30 days and try again.",
|
|
375
|
+
changedSessionHint: "A Session, its feedback, or the workspace changed after Preview. Preview again so Harbor cannot evaluate stale evidence.",
|
|
376
|
+
historicalGenericError: "No Job was started. Review the message and preview again.",
|
|
377
|
+
cancel: "Cancel",
|
|
304
378
|
completed: "Completed",
|
|
305
379
|
partial: "Completed with errors",
|
|
306
380
|
failed: "Read failed",
|
|
@@ -539,8 +613,11 @@ var CSS = `
|
|
|
539
613
|
.hse-identity-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px}.hse-evidence-table{width:100%;border-collapse:collapse;font-size:10px}.hse-evidence-table th,.hse-evidence-table td{padding:9px;border-bottom:1px solid var(--dsw-alias-border-l1,#dce4f0);text-align:left;vertical-align:top}.hse-evidence-table th{color:var(--dsw-alias-label-secondary,#748096);font-weight:500}.hse-evidence-table code{overflow-wrap:anywhere}.hse-chip-list{display:flex;gap:6px;flex-wrap:wrap}.hse-chip-list span{padding:6px 8px;border-radius:999px;background:#2875ff12;font-size:9px}.hse-hypotheses{display:grid;gap:10px}.hse-hypothesis{padding:14px;border:1px solid #2875ff3d;border-radius:12px;background:linear-gradient(145deg,#2875ff0c,#44d9ff05)}.hse-hypothesis h4{margin:0 0 10px;font-size:13px}.hse-hypothesis dl{display:grid;grid-template-columns:150px minmax(0,1fr);gap:8px 12px;margin:0;font-size:10px}.hse-hypothesis dt{color:var(--dsw-alias-label-secondary,#748096)}.hse-hypothesis dd{margin:0;overflow-wrap:anywhere}.hse-gate-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px}.hse-decision{padding:8px 12px;border-radius:999px;color:#126d50;background:#23ba8318;font-weight:800}.hse-decision[data-pass=false]{color:#b52f45;background:#ee647818}
|
|
540
614
|
.hse-report-table button{border:0;color:var(--ocean-600);background:none;text-align:left;cursor:pointer;font:inherit}.hse-report-table tr[data-selected=true]{background:#2875ff10}.hse-report-score{font-size:15px;font-weight:800}.hse-report-score[data-valid=false]{color:var(--coral-500)}.hse-report-detail{margin-top:12px;border:1px solid #2875ff40;border-radius:13px;overflow:hidden}.hse-report-detail-head{display:flex;justify-content:space-between;gap:12px;padding:14px 16px;background:linear-gradient(145deg,#2875ff14,#44d9ff08)}.hse-report-detail-head h4{margin:0;font-size:14px}.hse-report-detail-head span,.hse-report-detail-head code{display:block;margin-top:4px;color:var(--dsw-alias-label-secondary,#748096);font-size:9px}.hse-report-detail-head b{font-size:25px}.hse-report-criteria{display:grid;gap:9px;padding:14px}.hse-report-criterion{padding:12px;border-radius:10px;background:var(--dsw-alias-bg-layer-1,#f3f7fb)}.hse-report-criterion header{display:flex;justify-content:space-between;gap:10px}.hse-report-criterion header b:last-child{font-size:17px}.hse-report-criterion dl{display:grid;grid-template-columns:92px minmax(0,1fr);gap:8px 10px;margin:10px 0 0;font-size:10px;line-height:1.55}.hse-report-criterion dt{color:var(--dsw-alias-label-secondary,#748096)}.hse-report-criterion dd{margin:0;overflow-wrap:anywhere}.hse-report-recommendation{color:var(--ocean-600)}
|
|
541
615
|
.hse-stage-nav{grid-template-columns:repeat(9,minmax(88px,1fr))}.hse-report-compare{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:12px;padding:14px;align-items:start}.hse-report-compare .hse-report-criteria{padding:0}.hse-meta-flow{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px}.hse-meta-flow div{position:relative;padding:13px;border-radius:10px;background:#2875ff0f;font-size:10px}.hse-meta-flow div:not(:last-child):after{content:'\u2192';position:absolute;right:-8px;top:50%;z-index:1;color:var(--ocean-600);font-weight:800}.hse-badcase{color:#b52f45;background:#ee647817!important}.hse-hook-state{margin-bottom:12px;padding:11px 13px;border-left:3px solid var(--ocean-600);border-radius:8px;background:#2875ff0d;font-size:10px}.hse-hook-state[data-executed=false]{border-color:var(--amber-500);background:#e4a23b12}
|
|
616
|
+
.hse-launch-card{display:flex;align-items:center;gap:13px;margin:0 0 18px;padding:15px 17px;border:1px solid #2875ff40;border-radius:16px;background:linear-gradient(135deg,#2875ff16,#44d9ff0b);box-shadow:0 10px 30px #0a4b8f0d}.hse-launch-mark{display:grid;place-items:center;flex:0 0 38px;height:38px;border-radius:12px;color:#fff;background:linear-gradient(145deg,var(--ocean-600),var(--ocean-300));box-shadow:0 8px 18px #2875ff35;font-size:18px}.hse-launch-copy{display:grid;gap:3px;min-width:0}.hse-launch-copy b{font-size:13px}.hse-launch-copy span{color:var(--dsw-alias-label-secondary,#68778d);font-size:10px;line-height:1.5}.hse-launch-copy small{color:var(--ocean-600);font-size:9px;font-weight:800}.hse-launch-button{margin-left:auto;padding:10px 15px;border:0;border-radius:10px;color:#fff;background:var(--whale-500);box-shadow:0 8px 20px #2875ff30;cursor:pointer;font:inherit;font-size:11px;font-weight:800;white-space:nowrap}.hse-launch-button:disabled{opacity:.55;cursor:wait}.hse-launch-overlay{position:fixed;inset:0;z-index:1200;display:grid;place-items:center;padding:18px;background:#03152fa3;backdrop-filter:blur(5px)}.hse-launch-dialog{display:flex;flex-direction:column;width:min(780px,calc(100vw - 32px));max-height:min(860px,calc(100vh - 36px));overflow:hidden;border:1px solid var(--dsw-alias-border-l1,#d7e2ef);border-radius:20px;color:var(--dsw-alias-label-primary,#1d2a3d);background:var(--dsw-alias-bg-layer-2,#fff);box-shadow:0 28px 90px #03152f6b}.hse-launch-head{display:flex;justify-content:space-between;gap:20px;padding:20px 22px 16px;border-bottom:1px solid var(--dsw-alias-border-l1,#e1e8f1)}.hse-launch-head span{color:var(--ocean-600);font-size:9px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.hse-launch-head h2{margin:5px 0 6px;font-size:20px}.hse-launch-head p{margin:0;color:var(--dsw-alias-label-secondary,#748096);font-size:10px;line-height:1.6}.hse-dialog-close{align-self:flex-start;width:30px;height:30px;border:1px solid var(--dsw-alias-border-l1,#d7e2ef);border-radius:9px;color:inherit;background:transparent;cursor:pointer;font-size:20px;line-height:1}.hse-launch-body{overflow:auto;padding:18px 22px}.hse-launch-summary{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;margin-bottom:13px}.hse-launch-summary div,.hse-launch-grid div{padding:11px;border-radius:10px;background:var(--dsw-alias-bg-layer-1,#f3f7fb)}.hse-launch-summary span,.hse-launch-grid span{display:block;color:var(--dsw-alias-label-secondary,#748096);font-size:9px}.hse-launch-summary b,.hse-launch-grid b{display:block;margin-top:4px;overflow-wrap:anywhere;font-size:11px}.hse-launch-section{margin-top:12px;padding:14px;border:1px solid var(--dsw-alias-border-l1,#d7e2ef);border-radius:13px}.hse-launch-section h3{margin:0 0 10px;font-size:12px}.hse-session-list{display:grid;gap:7px;max-height:300px;overflow:auto}.hse-session-list article{padding:10px 11px;border-radius:9px;background:var(--dsw-alias-bg-layer-1,#f3f7fb)}.hse-session-list article>div{display:flex;justify-content:space-between;gap:12px}.hse-session-list b{font-size:10px}.hse-session-list span,.hse-session-list p,.hse-session-list code{color:var(--dsw-alias-label-secondary,#748096);font-size:9px}.hse-session-list p{margin:6px 0 3px}.hse-session-list code{overflow-wrap:anywhere}.hse-launch-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.hse-boundary-note{margin:10px 0 0;padding:9px 11px;border-left:3px solid var(--amber-500);border-radius:7px;background:#e4a23b12;font-size:10px}.hse-run-state{display:grid;justify-items:center;gap:9px;padding:42px 18px;text-align:center}.hse-run-state b{font-size:16px}.hse-run-state span,.hse-run-state p{max-width:560px;margin:0;color:var(--dsw-alias-label-secondary,#748096);font-size:10px;line-height:1.6}.hse-launch-error{padding:15px;border-left:4px solid var(--coral-500);border-radius:10px;background:#ee647812}.hse-launch-error b{color:#bd3148;font-size:11px}.hse-launch-error p{margin:7px 0;font-size:11px;overflow-wrap:anywhere}.hse-launch-error span{color:var(--dsw-alias-label-secondary,#748096);font-size:10px;line-height:1.6}.hse-launch-actions{display:flex;justify-content:flex-end;gap:8px;padding:13px 22px;border-top:1px solid var(--dsw-alias-border-l1,#e1e8f1)}.hse-launch-actions button{padding:9px 13px;border:1px solid var(--dsw-alias-border-l1,#c8d6e7);border-radius:9px;color:inherit;background:transparent;cursor:pointer;font:inherit;font-size:10px}.hse-launch-actions .hse-confirm{border-color:var(--whale-500);color:#fff;background:var(--whale-500);font-weight:800}
|
|
617
|
+
.hse-launch-card{margin-top:14px}.hse-launch-button-short{display:none}
|
|
542
618
|
@keyframes hse-spin{to{transform:rotate(360deg)}}@keyframes hse-pulse{50%{opacity:.38}}@keyframes hse-ripple{0%{transform:scale(.75);opacity:.4}70%,100%{transform:scale(1.12);opacity:0}}
|
|
543
|
-
@media(max-width:900px){.hse-page{width:calc(100% - 20px)}.hse-meta-grid,.hse-kpis,.hse-identity-grid{grid-template-columns:repeat(2,1fr)}.hse-trial-layout,.hse-output-layout{grid-template-columns:1fr}.hse-trial-detail{position:static;max-height:none}.hse-components,.hse-governance-id{grid-template-columns:repeat(2,1fr)}.hse-drawer{width:100vw}.hse-workbench{padding:12px}.hse-stage-nav{top:62px}.hse-trial-tools{grid-template-columns:1fr 1fr}.hse-grid,.hse-checks,.hse-version-grid{grid-template-columns:1fr}.hse-hypothesis dl{grid-template-columns:1fr}}
|
|
619
|
+
@media(max-width:900px){.hse-page{width:calc(100% - 20px)}.hse-meta-grid,.hse-kpis,.hse-identity-grid{grid-template-columns:repeat(2,1fr)}.hse-trial-layout,.hse-output-layout{grid-template-columns:1fr}.hse-trial-detail{position:static;max-height:none}.hse-components,.hse-governance-id{grid-template-columns:repeat(2,1fr)}.hse-drawer{width:100vw}.hse-workbench{padding:12px}.hse-stage-nav{top:62px}.hse-trial-tools{grid-template-columns:1fr 1fr}.hse-grid,.hse-checks,.hse-version-grid,.hse-launch-summary,.hse-launch-grid{grid-template-columns:1fr}.hse-hypothesis dl{grid-template-columns:1fr}.hse-launch-card{align-items:flex-start;flex-wrap:wrap}.hse-launch-button{width:100%;margin-left:51px}.hse-launch-dialog{width:calc(100vw - 20px)}.hse-launch-head,.hse-launch-body,.hse-launch-actions{padding-left:15px;padding-right:15px}}
|
|
620
|
+
@media(max-width:520px){.hse-hero{min-height:auto;padding:22px}.hse-hero h1{font-size:30px}.hse-stats{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));width:100%}.hse-stat{min-width:0}.hse-launch-card{display:grid;grid-template-columns:38px minmax(0,1fr) 108px;align-items:center;flex-wrap:nowrap}.hse-launch-copy span{display:none}.hse-launch-button{width:100%;margin:0;padding:9px;white-space:normal}.hse-launch-button-full{display:none}.hse-launch-button-short{display:inline}}
|
|
544
621
|
@media(prefers-reduced-motion:reduce){.hse-spin,.hse-status:before,.hse-hero:after{animation:none}.hse-job{transition:none}.hse-job:hover{transform:none}}
|
|
545
622
|
@media(max-width:900px){.hse-report-compare,.hse-meta-flow{grid-template-columns:1fr}.hse-meta-flow div:after{display:none}}
|
|
546
623
|
`;
|
|
@@ -1026,6 +1103,128 @@ function Workbench({ job, workspace, jobs, close, t }) {
|
|
|
1026
1103
|
else content = stage === "integration" ? /* @__PURE__ */ import_react.default.createElement(ContractPanel, { artifacts, component, t }) : /* @__PURE__ */ import_react.default.createElement("section", { className: "hse-section" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "hse-components" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "hse-component" }, /* @__PURE__ */ import_react.default.createElement("span", null, stage, component?.reward_affecting ? " \xB7 reward-affecting" : ""), /* @__PURE__ */ import_react.default.createElement("b", null, component?.id ?? "\u2014", " \xB7 ", component?.version ?? "\u2014"), /* @__PURE__ */ import_react.default.createElement("code", null, short(component?.digest)))));
|
|
1027
1104
|
return /* @__PURE__ */ import_react.default.createElement("div", { className: "hse-overlay", role: "presentation", onMouseDown: (event) => event.target === event.currentTarget && close() }, /* @__PURE__ */ import_react.default.createElement("aside", { className: "hse-drawer", role: "dialog", "aria-modal": "true", "aria-label": job }, /* @__PURE__ */ import_react.default.createElement("header", { className: "hse-drawer-head" }, /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement("h2", null, job), /* @__PURE__ */ import_react.default.createElement("p", null, historical ? `${t("historicalTarget")} \xB7 ${target.source_kind ?? activeJob?.generationSource?.kind ?? "\u2014"} \xB7 ${target.record_count ?? activeJob?.nTrials ?? 0} ${t("generationRecords")}` : `${activeJob?.candidate?.candidate_id ?? "\u2014"} \xB7 ${activeJob?.candidate?.version ?? "\u2014"}`, " \xB7 ", activeJob?.mode ?? "\u2014", " \xB7 ", activeJob?.progress?.completed ?? 0, "/", activeJob?.progress?.total ?? 0)), /* @__PURE__ */ import_react.default.createElement("button", { type: "button", className: "hse-close", onClick: close }, t("close"))), /* @__PURE__ */ import_react.default.createElement("div", { className: "hse-workbench" }, /* @__PURE__ */ import_react.default.createElement("nav", { className: "hse-stage-nav", "aria-label": t("stageNav") }, STAGES.map((item) => /* @__PURE__ */ import_react.default.createElement("button", { type: "button", key: item, "data-active": stage === item, "aria-current": stage === item ? "step" : void 0, onClick: () => setStage(item) }, STAGES.indexOf(item) + 1, ". ", historical && item === "candidate" ? t("historicalTarget") : historical && item === "dataset" ? t("generationRecords") : t(item)))), state.status === "loading" ? /* @__PURE__ */ import_react.default.createElement("div", { className: "hse-empty" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "hse-spin" }), t("loading")) : state.status === "error" ? /* @__PURE__ */ import_react.default.createElement("div", { className: "hse-error" }, state.error, /* @__PURE__ */ import_react.default.createElement("br", null), /* @__PURE__ */ import_react.default.createElement("button", { className: "hse-button", onClick: () => void load() }, t("retry"))) : /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, !contextSupported ? /* @__PURE__ */ import_react.default.createElement("div", { className: "hse-capability" }, t("capabilityUnavailable")) : null, content, /* @__PURE__ */ import_react.default.createElement("details", { className: "hse-section hse-audit" }, /* @__PURE__ */ import_react.default.createElement("summary", null, t("audit"), " / ", t("artifacts")), /* @__PURE__ */ import_react.default.createElement("pre", null, pretty({ validation: detail.validation, registry: artifacts.registry, context: artifacts.context, doctor: artifacts.doctor })))))));
|
|
1028
1105
|
}
|
|
1106
|
+
function historicalError(value) {
|
|
1107
|
+
const message = value?.message ?? String(value ?? "");
|
|
1108
|
+
const code = value?.code ?? message.match(/\b([A-Z][A-Z0-9_]{3,})\b/)?.[1] ?? "HISTORICAL_JOB_FAILED";
|
|
1109
|
+
return { code, message: message.replace(new RegExp(`^${code}:\\s*`), "") };
|
|
1110
|
+
}
|
|
1111
|
+
function historicalErrorHint(code, t) {
|
|
1112
|
+
if (code === "NO_ELIGIBLE_SESSIONS") return t("noEligibleHint");
|
|
1113
|
+
if (code === "SESSION_SELECTION_TOO_EXPENSIVE") return t("narrowScanHint");
|
|
1114
|
+
if (/SESSION_(?:SAMPLE|FEEDBACK)_CHANGED|WORKSPACE_MISMATCH|TOKEN_(?:INVALID|EXPIRED)|PREVIEW_(?:INVALID|WORKSPACE_MISMATCH)/.test(code)) return t("changedSessionHint");
|
|
1115
|
+
return t("historicalGenericError");
|
|
1116
|
+
}
|
|
1117
|
+
function HistoricalLauncher({ snapshot, reload, onCompleted, t }) {
|
|
1118
|
+
const [state, setState] = (0, import_react.useState)({ status: "idle" });
|
|
1119
|
+
const [open, setOpen] = (0, import_react.useState)(false);
|
|
1120
|
+
const workspace = snapshot?.workspace?.id;
|
|
1121
|
+
const operationId = state.operation?.operationId;
|
|
1122
|
+
(0, import_react.useEffect)(() => {
|
|
1123
|
+
let alive = true;
|
|
1124
|
+
setState({ status: "idle" });
|
|
1125
|
+
setOpen(false);
|
|
1126
|
+
if (!workspace) return () => {
|
|
1127
|
+
alive = false;
|
|
1128
|
+
};
|
|
1129
|
+
void api("historical-operation", { workspace }).then((operation) => {
|
|
1130
|
+
if (alive && ["queued", "running"].includes(operation?.status)) {
|
|
1131
|
+
setState({ status: "running", operation });
|
|
1132
|
+
}
|
|
1133
|
+
}).catch(() => {
|
|
1134
|
+
});
|
|
1135
|
+
return () => {
|
|
1136
|
+
alive = false;
|
|
1137
|
+
};
|
|
1138
|
+
}, [workspace]);
|
|
1139
|
+
(0, import_react.useEffect)(() => {
|
|
1140
|
+
if (!workspace || !operationId || !["queued", "running"].includes(state.operation?.status)) return void 0;
|
|
1141
|
+
let alive = true;
|
|
1142
|
+
let timer;
|
|
1143
|
+
const poll = async () => {
|
|
1144
|
+
try {
|
|
1145
|
+
const operation = await api("historical-operation", { workspace, operationId });
|
|
1146
|
+
if (!alive) return;
|
|
1147
|
+
if (operation.status === "completed") {
|
|
1148
|
+
setState({ status: "completed", operation });
|
|
1149
|
+
setOpen(false);
|
|
1150
|
+
await reload(true);
|
|
1151
|
+
if (alive) onCompleted(operation);
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
if (operation.status === "failed") {
|
|
1155
|
+
setState({ status: "error", error: historicalError(operation.error), operation });
|
|
1156
|
+
setOpen(true);
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
setState({ status: "running", operation });
|
|
1160
|
+
} catch {
|
|
1161
|
+
}
|
|
1162
|
+
if (alive) timer = window.setTimeout(() => void poll(), 2e3);
|
|
1163
|
+
};
|
|
1164
|
+
timer = window.setTimeout(() => void poll(), 1e3);
|
|
1165
|
+
return () => {
|
|
1166
|
+
alive = false;
|
|
1167
|
+
window.clearTimeout(timer);
|
|
1168
|
+
};
|
|
1169
|
+
}, [workspace, operationId, state.operation?.status, reload, onCompleted]);
|
|
1170
|
+
(0, import_react.useEffect)(() => {
|
|
1171
|
+
if (!open) return void 0;
|
|
1172
|
+
const escape = (event) => {
|
|
1173
|
+
if (event.key !== "Escape") return;
|
|
1174
|
+
setOpen(false);
|
|
1175
|
+
if (!["running", "starting"].includes(state.status)) setState({ status: "idle" });
|
|
1176
|
+
};
|
|
1177
|
+
window.addEventListener("keydown", escape);
|
|
1178
|
+
return () => window.removeEventListener("keydown", escape);
|
|
1179
|
+
}, [open, state.status]);
|
|
1180
|
+
const preview = async (days) => {
|
|
1181
|
+
setOpen(true);
|
|
1182
|
+
setState({ status: "previewing" });
|
|
1183
|
+
try {
|
|
1184
|
+
const value = await mutate("historical-preview", {
|
|
1185
|
+
workspace,
|
|
1186
|
+
limit: 10,
|
|
1187
|
+
includeFeedback: true,
|
|
1188
|
+
...days ? { createdAfter: new Date(Date.now() - days * 864e5).toISOString() } : {}
|
|
1189
|
+
});
|
|
1190
|
+
setState({ status: "ready", preview: value });
|
|
1191
|
+
} catch (error) {
|
|
1192
|
+
setState({ status: "error", error: historicalError(error) });
|
|
1193
|
+
}
|
|
1194
|
+
};
|
|
1195
|
+
const confirm = async () => {
|
|
1196
|
+
if (!state.preview) return;
|
|
1197
|
+
setState((current) => ({ ...current, status: "starting" }));
|
|
1198
|
+
try {
|
|
1199
|
+
const operation = await mutate("historical-run", { workspace, previewId: state.preview.previewId });
|
|
1200
|
+
setState({ status: "running", operation });
|
|
1201
|
+
} catch (error) {
|
|
1202
|
+
const normalized = historicalError(error);
|
|
1203
|
+
if (normalized.code === "HISTORICAL_JOB_ALREADY_RUNNING") {
|
|
1204
|
+
try {
|
|
1205
|
+
const operation = await api("historical-operation", { workspace });
|
|
1206
|
+
if (["queued", "running"].includes(operation?.status)) {
|
|
1207
|
+
setState({ status: "running", operation });
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
} catch {
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
setState({ status: "error", error: normalized });
|
|
1214
|
+
}
|
|
1215
|
+
};
|
|
1216
|
+
const close = () => {
|
|
1217
|
+
setOpen(false);
|
|
1218
|
+
if (!["running", "starting"].includes(state.status)) setState({ status: "idle" });
|
|
1219
|
+
};
|
|
1220
|
+
const previewValue = state.preview;
|
|
1221
|
+
const evaluator = previewValue?.evaluation?.evaluator;
|
|
1222
|
+
const judge = previewValue?.evaluation?.judge;
|
|
1223
|
+
const active = ["running", "starting"].includes(state.status);
|
|
1224
|
+
const buttonLabel = active ? t("historicalActive") : state.status === "previewing" ? t("historicalPreparing") : t("historicalLaunch");
|
|
1225
|
+
const buttonShort = active ? t("historicalActiveShort") : state.status === "previewing" ? t("historicalPreparingShort") : t("historicalLaunchShort");
|
|
1226
|
+
return /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, /* @__PURE__ */ import_react.default.createElement("section", { className: "hse-launch-card", "aria-live": "polite" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "hse-launch-mark", "aria-hidden": "true" }, "\u2726"), /* @__PURE__ */ import_react.default.createElement("div", { className: "hse-launch-copy" }, /* @__PURE__ */ import_react.default.createElement("b", null, active ? t("historicalRunning") : t("historicalLaunch")), /* @__PURE__ */ import_react.default.createElement("span", null, active ? t("historicalRunningHint") : t("historicalLaunchBody")), /* @__PURE__ */ import_react.default.createElement("small", null, active ? `${state.operation?.selectedCount ?? "\u2014"} Trials` : t("historicalLaunchHint"))), /* @__PURE__ */ import_react.default.createElement("button", { type: "button", className: "hse-launch-button", disabled: !workspace || state.status === "previewing", onClick: () => active ? setOpen(true) : void preview() }, /* @__PURE__ */ import_react.default.createElement("span", { className: "hse-launch-button-full" }, buttonLabel), /* @__PURE__ */ import_react.default.createElement("span", { className: "hse-launch-button-short" }, buttonShort))), open ? /* @__PURE__ */ import_react.default.createElement("div", { className: "hse-launch-overlay", role: "presentation", onMouseDown: (event) => event.target === event.currentTarget && close() }, /* @__PURE__ */ import_react.default.createElement("section", { className: "hse-launch-dialog", role: "dialog", "aria-modal": "true", "aria-labelledby": "hse-historical-title" }, /* @__PURE__ */ import_react.default.createElement("header", { className: "hse-launch-head" }, /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement("span", null, t("historicalLaunchHint")), /* @__PURE__ */ import_react.default.createElement("h2", { id: "hse-historical-title" }, state.status === "running" ? t("historicalRunning") : t("historicalPreviewTitle")), /* @__PURE__ */ import_react.default.createElement("p", null, state.status === "running" ? t("historicalRunningHint") : t("historicalPreviewHint"))), /* @__PURE__ */ import_react.default.createElement("button", { type: "button", className: "hse-dialog-close", "aria-label": t("close"), onClick: close }, "\xD7")), /* @__PURE__ */ import_react.default.createElement("div", { className: "hse-launch-body" }, state.status === "previewing" ? /* @__PURE__ */ import_react.default.createElement("div", { className: "hse-empty" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "hse-spin" }), t("historicalPreparing")) : null, state.status === "starting" ? /* @__PURE__ */ import_react.default.createElement("div", { className: "hse-empty" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "hse-spin" }), t("historicalStarting")) : null, state.status === "running" ? /* @__PURE__ */ import_react.default.createElement("div", { className: "hse-run-state" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "hse-spin" }), /* @__PURE__ */ import_react.default.createElement("b", null, t("historicalRunning")), /* @__PURE__ */ import_react.default.createElement("span", null, state.operation?.selectedCount ?? "\u2014", " Trials \xB7 ", snapshot.workspace.label), /* @__PURE__ */ import_react.default.createElement("p", null, t("historicalRunningHint"))) : null, state.status === "completed" ? /* @__PURE__ */ import_react.default.createElement("div", { className: "hse-run-state" }, /* @__PURE__ */ import_react.default.createElement("b", null, "\u2713 ", t("historicalCompleted"))) : null, state.status === "error" ? /* @__PURE__ */ import_react.default.createElement("div", { className: "hse-launch-error" }, /* @__PURE__ */ import_react.default.createElement("b", null, state.error.code), /* @__PURE__ */ import_react.default.createElement("p", null, state.error.message), /* @__PURE__ */ import_react.default.createElement("span", null, historicalErrorHint(state.error.code, t))) : null, state.status === "ready" && previewValue ? /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, /* @__PURE__ */ import_react.default.createElement("div", { className: "hse-launch-summary" }, /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement("span", null, t("selectedSessions")), /* @__PURE__ */ import_react.default.createElement("b", null, previewValue.selected.length)), /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement("span", null, t("requestEstimate")), /* @__PURE__ */ import_react.default.createElement("b", null, previewValue.estimatedJudgeRequests)), /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement("span", null, t("tokenExpiry")), /* @__PURE__ */ import_react.default.createElement("b", null, new Date(previewValue.expiresAt).toLocaleTimeString())), /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement("span", null, t("workspace")), /* @__PURE__ */ import_react.default.createElement("b", null, snapshot.workspace.label))), /* @__PURE__ */ import_react.default.createElement("section", { className: "hse-launch-section" }, /* @__PURE__ */ import_react.default.createElement("h3", null, t("recentSessions")), /* @__PURE__ */ import_react.default.createElement("div", { className: "hse-session-list" }, previewValue.selected.map((session) => /* @__PURE__ */ import_react.default.createElement("article", { key: session.trialId }, /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement("b", null, session.title), /* @__PURE__ */ import_react.default.createElement("span", null, session.lastActivityAt ? new Date(session.lastActivityAt).toLocaleString() : "\u2014")), /* @__PURE__ */ import_react.default.createElement("p", null, t("turnCounts"), " ", session.turnCount ?? 0, " \xB7 ", t("toolCounts"), " ", session.toolCallCount ?? 0, " \xB7 ", t("feedbackCounts"), " +", session.feedback?.positive ?? 0, " / -", session.feedback?.negative ?? 0), /* @__PURE__ */ import_react.default.createElement("code", null, (session.modelRoutes ?? []).map((route) => `${route.provider}/${route.model}`).join(" \xB7 ") || session.agentPreset || "\u2014"))))), /* @__PURE__ */ import_react.default.createElement("section", { className: "hse-launch-section" }, /* @__PURE__ */ import_react.default.createElement("h3", null, t("historicalBoundaries")), /* @__PURE__ */ import_react.default.createElement("div", { className: "hse-launch-grid" }, /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement("span", null, t("generatorRole")), /* @__PURE__ */ import_react.default.createElement("b", null, t("generatorRoleValue"))), /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement("span", null, t("evaluatorIdentity")), /* @__PURE__ */ import_react.default.createElement("b", null, evaluator?.id ?? "\u2014", " \xB7 ", evaluator?.version ?? "\u2014")), /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement("span", null, t("judgeIdentity")), /* @__PURE__ */ import_react.default.createElement("b", null, judge?.provider ?? "\u2014", " / ", judge?.model ?? "\u2014")), /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement("span", null, t("coupling")), /* @__PURE__ */ import_react.default.createElement("b", null, previewValue.evaluation?.coupling ?? "\u2014")), /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement("span", null, t("evidenceRetention")), /* @__PURE__ */ import_react.default.createElement("b", null, previewValue.retention?.privateEvidence, " \xB7 ", previewValue.retention?.jobEvidence))), /* @__PURE__ */ import_react.default.createElement("p", { className: "hse-boundary-note" }, t("historicalBoundaryDetail")))) : null), /* @__PURE__ */ import_react.default.createElement("footer", { className: "hse-launch-actions" }, state.status === "ready" ? /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, /* @__PURE__ */ import_react.default.createElement("button", { type: "button", onClick: close }, t("cancel")), /* @__PURE__ */ import_react.default.createElement("button", { type: "button", className: "hse-confirm", onClick: () => void confirm() }, t("historicalConfirm"))) : null, state.status === "error" ? /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, /* @__PURE__ */ import_react.default.createElement("button", { type: "button", onClick: close }, t("close")), state.error.code === "SESSION_SELECTION_TOO_EXPENSIVE" ? /* @__PURE__ */ import_react.default.createElement("button", { type: "button", onClick: () => void preview(30) }, t("recent30Days")) : /* @__PURE__ */ import_react.default.createElement("button", { type: "button", onClick: () => void preview() }, t("previewAgain"))) : null, state.status === "running" ? /* @__PURE__ */ import_react.default.createElement("button", { type: "button", onClick: close }, t("close")) : null))) : null);
|
|
1227
|
+
}
|
|
1029
1228
|
function DashboardView({ t }) {
|
|
1030
1229
|
const [workspace, setWorkspace] = (0, import_react.useState)("");
|
|
1031
1230
|
const [offset, setOffset] = (0, import_react.useState)(0);
|
|
@@ -1043,7 +1242,11 @@ function DashboardView({ t }) {
|
|
|
1043
1242
|
setWorkspace(snapshot.workspace.id);
|
|
1044
1243
|
setSelected({ job, workspace: snapshot.workspace.id });
|
|
1045
1244
|
};
|
|
1046
|
-
|
|
1245
|
+
const completedHistorical = (0, import_react.useCallback)((operation) => {
|
|
1246
|
+
setWorkspace(operation.workspace);
|
|
1247
|
+
setSelected({ job: operation.jobName, workspace: operation.workspace });
|
|
1248
|
+
}, []);
|
|
1249
|
+
return /* @__PURE__ */ import_react.default.createElement("main", { className: "hse-root" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "hse-page" }, /* @__PURE__ */ import_react.default.createElement("section", { className: "hse-hero", style: { "--ocean-image": `url(${harbor_ocean_default})` } }, /* @__PURE__ */ import_react.default.createElement("button", { className: "hse-refresh", onClick: () => void state.load() }, t("refresh")), /* @__PURE__ */ import_react.default.createElement("div", { className: "hse-eyebrow" }, /* @__PURE__ */ import_react.default.createElement("span", { className: "hse-whale", "aria-hidden": "true" }, "\u{1F433}"), t("eyebrow")), /* @__PURE__ */ import_react.default.createElement("h1", null, t("heroTitle")), /* @__PURE__ */ import_react.default.createElement("p", null, t("heroBody")), /* @__PURE__ */ import_react.default.createElement("div", { className: "hse-stats" }, stats.map(([label, value]) => /* @__PURE__ */ import_react.default.createElement("div", { className: "hse-stat", key: label }, /* @__PURE__ */ import_react.default.createElement("span", null, label), /* @__PURE__ */ import_react.default.createElement("b", null, value))))), snapshot?.workspace ? /* @__PURE__ */ import_react.default.createElement(HistoricalLauncher, { snapshot, reload: state.load, onCompleted: completedHistorical, t }) : null, /* @__PURE__ */ import_react.default.createElement("div", { className: "hse-head" }, /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement("h2", null, t("jobs")), /* @__PURE__ */ import_react.default.createElement("p", null, t("jobsHint"))), snapshot?.workspaces?.length ? /* @__PURE__ */ import_react.default.createElement("select", { className: "hse-select", "aria-label": t("workspaceSelect"), value: snapshot.workspace?.id ?? "", onChange: switchWorkspace }, snapshot.workspaces.map((item) => /* @__PURE__ */ import_react.default.createElement("option", { value: item.id, key: item.id }, item.label, " \xB7 ", item.root))) : null), snapshot?.workspace ? /* @__PURE__ */ import_react.default.createElement("div", { className: "hse-hook-state" }, /* @__PURE__ */ import_react.default.createElement("b", null, t("workspace"), ": ", snapshot.workspace.label), /* @__PURE__ */ import_react.default.createElement("br", null), snapshot.config.projectRoot, " \xB7 ", snapshot.config.jobsDir) : null, state.status === "loading" ? /* @__PURE__ */ import_react.default.createElement("div", { className: "hse-empty" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "hse-spin" }), t("loading")) : state.status === "error" && !snapshot ? /* @__PURE__ */ import_react.default.createElement("div", { className: "hse-error" }, state.error, /* @__PURE__ */ import_react.default.createElement("br", null), /* @__PURE__ */ import_react.default.createElement("button", { className: "hse-button", onClick: () => void state.load() }, t("retry"))) : !snapshot?.jobs?.length ? /* @__PURE__ */ import_react.default.createElement("div", { className: "hse-empty" }, t("empty")) : /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, /* @__PURE__ */ import_react.default.createElement("div", { className: "hse-list" }, snapshot.jobs.map((job) => /* @__PURE__ */ import_react.default.createElement(JobCard, { job, t, open: openJob, key: job.name }))), /* @__PURE__ */ import_react.default.createElement("div", { className: "hse-pager" }, /* @__PURE__ */ import_react.default.createElement("span", null, pagination.total ? `${offset + 1}\u2013${Math.min(offset + (snapshot.jobs?.length ?? 0), pagination.total)} / ${pagination.total}` : "0 / 0"), /* @__PURE__ */ import_react.default.createElement("button", { disabled: !offset, onClick: () => setOffset(Math.max(0, offset - (pagination.limit ?? 20))) }, t("previous")), /* @__PURE__ */ import_react.default.createElement("button", { disabled: !pagination.hasMore, onClick: () => setOffset(offset + (pagination.limit ?? 20)) }, t("next"))))), selected ? /* @__PURE__ */ import_react.default.createElement(Workbench, { job: selected.job, workspace: selected.workspace, jobs: snapshot.jobs ?? [], close: () => setSelected(void 0), t }) : null);
|
|
1047
1250
|
}
|
|
1048
1251
|
function VersionPanel({ t }) {
|
|
1049
1252
|
const state = useVersionCheck();
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
const OPERATION_RETENTION_MS = 60 * 60 * 1000
|
|
4
|
+
|
|
5
|
+
function timestamp(now) {
|
|
6
|
+
const value = now()
|
|
7
|
+
return value instanceof Date ? value : new Date(value)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function publicError(error) {
|
|
11
|
+
const raw = error instanceof Error ? error.message : String(error)
|
|
12
|
+
const message = raw
|
|
13
|
+
.replace(/(?:\/[A-Za-z0-9._ -]+){2,}/g, '[local path]')
|
|
14
|
+
.replace(/[A-Za-z]:\\[^\s]+/g, '[local path]')
|
|
15
|
+
const match = message.match(/^([A-Z][A-Z0-9_]+):\s*(.*)$/s)
|
|
16
|
+
return {
|
|
17
|
+
code: match?.[1] ?? 'HISTORICAL_JOB_FAILED',
|
|
18
|
+
message: match?.[2] || message || 'Historical evaluation failed',
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function publicOperation(value) {
|
|
23
|
+
if (!value) return { status: 'idle' }
|
|
24
|
+
return {
|
|
25
|
+
schemaVersion: 1,
|
|
26
|
+
operationId: value.operationId,
|
|
27
|
+
workspace: value.workspace,
|
|
28
|
+
status: value.status,
|
|
29
|
+
selectedCount: value.selectedCount,
|
|
30
|
+
createdAt: value.createdAt,
|
|
31
|
+
...(value.startedAt ? { startedAt: value.startedAt } : {}),
|
|
32
|
+
...(value.finishedAt ? { finishedAt: value.finishedAt } : {}),
|
|
33
|
+
...(value.jobName ? { jobName: value.jobName } : {}),
|
|
34
|
+
...(value.error ? { error: value.error } : {}),
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Same-origin Web orchestration for the narrow Historical Session diagnostic path. */
|
|
39
|
+
export class HistoricalWebController {
|
|
40
|
+
constructor({
|
|
41
|
+
service,
|
|
42
|
+
sessionDiagnostic,
|
|
43
|
+
now = () => new Date(),
|
|
44
|
+
randomId = () => randomUUID(),
|
|
45
|
+
schedule = callback => queueMicrotask(callback),
|
|
46
|
+
operationRetentionMs = OPERATION_RETENTION_MS,
|
|
47
|
+
}) {
|
|
48
|
+
this.service = service
|
|
49
|
+
this.sessionDiagnostic = sessionDiagnostic
|
|
50
|
+
this.now = now
|
|
51
|
+
this.randomId = randomId
|
|
52
|
+
this.schedule = schedule
|
|
53
|
+
this.operationRetentionMs = operationRetentionMs
|
|
54
|
+
this.previews = new Map()
|
|
55
|
+
this.operations = new Map()
|
|
56
|
+
this.consumedPreviews = new Map()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
_cleanup() {
|
|
60
|
+
const now = timestamp(this.now).getTime()
|
|
61
|
+
for (const [previewId, preview] of this.previews) {
|
|
62
|
+
if (Date.parse(preview.expiresAt) <= now) this.previews.delete(previewId)
|
|
63
|
+
}
|
|
64
|
+
for (const [operationId, operation] of this.operations) {
|
|
65
|
+
if (operation.finishedAt && Date.parse(operation.finishedAt) + this.operationRetentionMs <= now) {
|
|
66
|
+
this.operations.delete(operationId)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
for (const [previewId, operationId] of this.consumedPreviews) {
|
|
70
|
+
if (!this.operations.has(operationId)) this.consumedPreviews.delete(previewId)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_activeOperation(workspace) {
|
|
75
|
+
return [...this.operations.values()]
|
|
76
|
+
.filter(item => item.workspace === workspace && ['queued', 'running'].includes(item.status))
|
|
77
|
+
.sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async preview(args = {}) {
|
|
81
|
+
this._cleanup()
|
|
82
|
+
const resolved = await this.service.historicalWorkspace({ workspace: args.workspace })
|
|
83
|
+
const previewId = this.randomId()
|
|
84
|
+
const identity = {
|
|
85
|
+
projectRoot: resolved.projectRoot,
|
|
86
|
+
ownerSessionId: `web-historical:${this.randomId()}`,
|
|
87
|
+
}
|
|
88
|
+
const preview = await this.sessionDiagnostic.previewWithIdentity({
|
|
89
|
+
limit: args.limit === undefined ? 10 : args.limit,
|
|
90
|
+
createdAfter: args.createdAfter,
|
|
91
|
+
includeFeedback: args.includeFeedback !== false,
|
|
92
|
+
}, identity, { config: resolved.config })
|
|
93
|
+
const { selectionToken, ...visible } = preview
|
|
94
|
+
this.previews.set(previewId, {
|
|
95
|
+
previewId,
|
|
96
|
+
workspace: resolved.workspace,
|
|
97
|
+
identity,
|
|
98
|
+
config: resolved.config,
|
|
99
|
+
selectionToken,
|
|
100
|
+
selectedCount: preview.selected.length,
|
|
101
|
+
expiresAt: preview.expiresAt,
|
|
102
|
+
})
|
|
103
|
+
return {
|
|
104
|
+
...visible,
|
|
105
|
+
schemaVersion: 1,
|
|
106
|
+
previewId,
|
|
107
|
+
workspace: resolved.workspace,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async run(args = {}) {
|
|
112
|
+
this._cleanup()
|
|
113
|
+
const previewId = String(args.previewId ?? '')
|
|
114
|
+
if (!previewId) throw new Error('HISTORICAL_PREVIEW_REQUIRED: preview the recent Sessions before confirming the Job')
|
|
115
|
+
const existingOperationId = this.consumedPreviews.get(previewId)
|
|
116
|
+
if (existingOperationId) {
|
|
117
|
+
const existing = this.operations.get(existingOperationId)
|
|
118
|
+
if (args.workspace && existing?.workspace !== args.workspace) {
|
|
119
|
+
throw new Error('HISTORICAL_PREVIEW_WORKSPACE_MISMATCH: the workspace changed; preview again')
|
|
120
|
+
}
|
|
121
|
+
return publicOperation(existing)
|
|
122
|
+
}
|
|
123
|
+
const preview = this.previews.get(previewId)
|
|
124
|
+
if (!preview) throw new Error('HISTORICAL_PREVIEW_INVALID: this preview expired or was already discarded; preview again')
|
|
125
|
+
if (args.workspace && preview.workspace !== args.workspace) {
|
|
126
|
+
throw new Error('HISTORICAL_PREVIEW_WORKSPACE_MISMATCH: the workspace changed; preview again')
|
|
127
|
+
}
|
|
128
|
+
const active = this._activeOperation(preview.workspace)
|
|
129
|
+
if (active) {
|
|
130
|
+
throw new Error('HISTORICAL_JOB_ALREADY_RUNNING: wait for the current Historical Session Job to finish')
|
|
131
|
+
}
|
|
132
|
+
const operationId = this.randomId()
|
|
133
|
+
const createdAt = timestamp(this.now).toISOString()
|
|
134
|
+
const operation = {
|
|
135
|
+
operationId,
|
|
136
|
+
workspace: preview.workspace,
|
|
137
|
+
status: 'queued',
|
|
138
|
+
selectedCount: preview.selectedCount,
|
|
139
|
+
createdAt,
|
|
140
|
+
}
|
|
141
|
+
this.previews.delete(previewId)
|
|
142
|
+
this.operations.set(operationId, operation)
|
|
143
|
+
this.consumedPreviews.set(previewId, operationId)
|
|
144
|
+
this.schedule(() => { void this._execute(operation, preview) })
|
|
145
|
+
return publicOperation(operation)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async _execute(operation, preview) {
|
|
149
|
+
operation.status = 'running'
|
|
150
|
+
operation.startedAt = timestamp(this.now).toISOString()
|
|
151
|
+
try {
|
|
152
|
+
const result = await this.sessionDiagnostic.runWithIdentity({
|
|
153
|
+
selectionToken: preview.selectionToken,
|
|
154
|
+
}, preview.identity, { config: preview.config })
|
|
155
|
+
operation.status = 'completed'
|
|
156
|
+
operation.jobName = String(result.job ?? '').split(/[\\/]/).filter(Boolean).at(-1)
|
|
157
|
+
if (!operation.jobName) throw new Error('HISTORICAL_JOB_INCOMPLETE: the completed run returned no Job identity')
|
|
158
|
+
} catch (error) {
|
|
159
|
+
operation.status = 'failed'
|
|
160
|
+
operation.error = publicError(error)
|
|
161
|
+
} finally {
|
|
162
|
+
operation.finishedAt = timestamp(this.now).toISOString()
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
operation(args = {}) {
|
|
167
|
+
this._cleanup()
|
|
168
|
+
const operationId = String(args.operationId ?? '')
|
|
169
|
+
if (operationId) {
|
|
170
|
+
const operation = this.operations.get(operationId)
|
|
171
|
+
if (!operation) return { status: 'idle' }
|
|
172
|
+
if (args.workspace && operation.workspace !== args.workspace) {
|
|
173
|
+
throw new Error('HISTORICAL_OPERATION_WORKSPACE_MISMATCH: the operation belongs to another workspace')
|
|
174
|
+
}
|
|
175
|
+
return publicOperation(operation)
|
|
176
|
+
}
|
|
177
|
+
if (!args.workspace) return { status: 'idle' }
|
|
178
|
+
return publicOperation(this._activeOperation(String(args.workspace)))
|
|
179
|
+
}
|
|
180
|
+
}
|
package/lib/service.js
CHANGED
|
@@ -181,6 +181,15 @@ export class EvolutionService {
|
|
|
181
181
|
}, args)
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
async historicalWorkspace(args = {}) {
|
|
185
|
+
const { config } = await this._webContext(args)
|
|
186
|
+
return {
|
|
187
|
+
workspace: config.workspaceId,
|
|
188
|
+
projectRoot: path.resolve(config.projectRoot),
|
|
189
|
+
config: { ...config },
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
184
193
|
async version(args = {}) {
|
|
185
194
|
let config = this.config
|
|
186
195
|
if (args.workspace) ({ config } = await this._webContext(args))
|
|
@@ -28,6 +28,16 @@ function executionIdentity(exec) {
|
|
|
28
28
|
return { projectRoot: path.resolve(header.cwd), ownerSessionId: header.id }
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
function normalizedIdentity(identity) {
|
|
32
|
+
if (typeof identity?.projectRoot !== 'string' || !path.isAbsolute(identity.projectRoot)) {
|
|
33
|
+
throw new Error('HISTORICAL_EXECUTION_IDENTITY_INVALID: projectRoot must be an absolute directory')
|
|
34
|
+
}
|
|
35
|
+
if (typeof identity.ownerSessionId !== 'string' || !identity.ownerSessionId) {
|
|
36
|
+
throw new Error('HISTORICAL_EXECUTION_IDENTITY_INVALID: ownerSessionId is required')
|
|
37
|
+
}
|
|
38
|
+
return { projectRoot: path.resolve(identity.projectRoot), ownerSessionId: identity.ownerSessionId }
|
|
39
|
+
}
|
|
40
|
+
|
|
31
41
|
function feedbackItems(result) {
|
|
32
42
|
return result?.ok === true && Array.isArray(result.value?.items) ? result.value.items : []
|
|
33
43
|
}
|
|
@@ -147,7 +157,11 @@ export class SessionDiagnosticService {
|
|
|
147
157
|
}
|
|
148
158
|
|
|
149
159
|
async preview(args = {}, exec) {
|
|
150
|
-
|
|
160
|
+
return this.previewWithIdentity(args, executionIdentity(exec), { signal: exec?.signal })
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async previewWithIdentity(args = {}, requestedIdentity, { signal, config = this.config } = {}) {
|
|
164
|
+
const identity = normalizedIdentity(requestedIdentity)
|
|
151
165
|
const sessionQuery = capability(this.ctx, 'sessionQuery')
|
|
152
166
|
const limit = args.limit ?? 10
|
|
153
167
|
const createdAfter = parseCreatedAfter(args.createdAfter)
|
|
@@ -156,10 +170,10 @@ export class SessionDiagnosticService {
|
|
|
156
170
|
projectRoot: identity.projectRoot,
|
|
157
171
|
currentSessionId: identity.ownerSessionId,
|
|
158
172
|
limit,
|
|
159
|
-
maxSessionReads:
|
|
160
|
-
concurrency:
|
|
173
|
+
maxSessionReads: config.sessionMaxReads ?? 100,
|
|
174
|
+
concurrency: config.sessionReadConcurrency ?? 4,
|
|
161
175
|
createdAfter,
|
|
162
|
-
signal
|
|
176
|
+
signal,
|
|
163
177
|
})
|
|
164
178
|
if (!result.selected.length) {
|
|
165
179
|
throw new Error('NO_ELIGIBLE_SESSIONS: no completed top-level DSH Sessions with direct human input and assistant output were found in this workspace')
|
|
@@ -218,7 +232,7 @@ export class SessionDiagnosticService {
|
|
|
218
232
|
evaluation,
|
|
219
233
|
retention: {
|
|
220
234
|
privateEvidence: '.harbor/private/session-batches',
|
|
221
|
-
jobEvidence:
|
|
235
|
+
jobEvidence: config.jobsDir ?? 'jobs',
|
|
222
236
|
vcsPolicy: 'an ignore-all file is created only when .harbor/private/.gitignore is absent; existing private rules and jobs retention/VCS policy remain project-owned',
|
|
223
237
|
},
|
|
224
238
|
confirmation: `Run 1 historical-generation-evaluation Job with ${selected.length} immutable Trial(s) using ${evaluation.evaluator.id}@${evaluation.evaluator.version} and Judge ${evaluation.judge.provider}/${evaluation.judge.model} (${evaluation.coupling}); no Candidate will be executed or promoted.`,
|
|
@@ -226,7 +240,11 @@ export class SessionDiagnosticService {
|
|
|
226
240
|
}
|
|
227
241
|
|
|
228
242
|
async run(args = {}, exec) {
|
|
229
|
-
|
|
243
|
+
return this.runWithIdentity(args, executionIdentity(exec))
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async runWithIdentity(args = {}, requestedIdentity, { config = this.config } = {}) {
|
|
247
|
+
const identity = normalizedIdentity(requestedIdentity)
|
|
230
248
|
if (args.stackPath !== undefined) {
|
|
231
249
|
throw new Error('HISTORICAL_CUSTOM_STACK_UNSUPPORTED: the first release binds the materialized Broker Evaluator and Stack as one immutable unit')
|
|
232
250
|
}
|
|
@@ -294,7 +312,7 @@ export class SessionDiagnosticService {
|
|
|
294
312
|
observations,
|
|
295
313
|
})
|
|
296
314
|
const result = await this.runHistoricalEvaluation(
|
|
297
|
-
{ ...
|
|
315
|
+
{ ...config, projectRoot: identity.projectRoot },
|
|
298
316
|
{
|
|
299
317
|
batchPath: written.batchPath,
|
|
300
318
|
batchDir: written.batchDir,
|
package/lib/web.js
CHANGED
|
@@ -10,6 +10,9 @@ export const EVALUATOR_ROUTE = '/_dsh/harbor-evolution/evaluator'
|
|
|
10
10
|
export const META_ROUTE = '/_dsh/harbor-evolution/meta'
|
|
11
11
|
export const PROJECT_ROOT_ROUTE = '/_dsh/harbor-evolution/project-root'
|
|
12
12
|
export const VERSION_ROUTE = '/_dsh/harbor-evolution/version'
|
|
13
|
+
export const HISTORICAL_PREVIEW_ROUTE = '/_dsh/harbor-evolution/historical-preview'
|
|
14
|
+
export const HISTORICAL_RUN_ROUTE = '/_dsh/harbor-evolution/historical-run'
|
|
15
|
+
export const HISTORICAL_OPERATION_ROUTE = '/_dsh/harbor-evolution/historical-operation'
|
|
13
16
|
const MAX_MUTATION_BYTES = 256 * 1024
|
|
14
17
|
|
|
15
18
|
function sendJson(response, status, body) {
|
|
@@ -104,7 +107,7 @@ export function createMutationHandler(update, code = 'update-failed') {
|
|
|
104
107
|
}
|
|
105
108
|
}
|
|
106
109
|
|
|
107
|
-
export function installDashboardWeb(ctx, service) {
|
|
110
|
+
export function installDashboardWeb(ctx, service, historicalController) {
|
|
108
111
|
if (typeof ctx.inject !== 'function') return
|
|
109
112
|
ctx.inject(['webServer'], (webCtx) => {
|
|
110
113
|
const routes = [
|
|
@@ -118,6 +121,11 @@ export function installDashboardWeb(ctx, service) {
|
|
|
118
121
|
[GOVERNANCE_ROUTE, createApiHandler(args => service.governance(args), 'governance-unavailable')],
|
|
119
122
|
[EVALUATOR_ROUTE, createMutationHandler(args => service.evaluator(args), 'evaluator-update-failed')],
|
|
120
123
|
[META_ROUTE, createApiHandler(args => service.meta(args), 'meta-evaluation-unavailable')],
|
|
124
|
+
...(historicalController ? [
|
|
125
|
+
[HISTORICAL_PREVIEW_ROUTE, createMutationHandler(args => historicalController.preview(args), 'historical-preview-failed')],
|
|
126
|
+
[HISTORICAL_RUN_ROUTE, createMutationHandler(args => historicalController.run(args), 'historical-run-failed')],
|
|
127
|
+
[HISTORICAL_OPERATION_ROUTE, createApiHandler(args => historicalController.operation(args), 'historical-operation-unavailable')],
|
|
128
|
+
] : []),
|
|
121
129
|
[VERSION_ROUTE, createApiHandler(args => service.version(args), 'version-check-unavailable')],
|
|
122
130
|
[PROJECT_ROOT_ROUTE, createMutationHandler(args => service.setProjectRoot(args), 'project-root-update-failed')],
|
|
123
131
|
]
|
package/package.json
CHANGED