@linxin666/dsh-client-ui-task-board 0.4.5 → 0.4.6
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.i18n.yaml +2 -2
- package/README.md +20 -12
- package/README.zh.md +20 -12
- package/lib/client.js +1327 -229
- package/lib/client.js.map +1 -1
- package/lib/index.js +1909 -348
- package/lib/types/client/TaskBoardSettingsCard.d.ts +12 -0
- package/lib/types/client/index.d.ts +4 -5
- package/lib/types/client/locales.d.ts +65 -0
- package/lib/types/client/plugin-card-seat.d.ts +5 -0
- package/lib/types/client/schedule-zone.d.ts +50 -0
- package/lib/types/core/controller.d.ts +10 -11
- package/lib/types/core/poll-cadence.d.ts +31 -0
- package/lib/types/core/tasks.d.ts +144 -14
- package/lib/types/core/use-cases/task-schedule.d.ts +68 -10
- package/lib/types/core/verification-budget.d.ts +52 -0
- package/lib/types/core/verification.d.ts +376 -9
- package/lib/types/host/verification-gate.d.ts +24 -0
- package/lib/types/host/verification-runner.d.ts +38 -7
- package/lib/types/host-ledger.d.ts +82 -7
- package/lib/types/host-runner.d.ts +32 -9
- package/lib/types/host-service.d.ts +117 -9
- package/lib/types/index.d.ts +54 -0
- package/lib/types/protocol.d.ts +50 -12
- package/package.json +2 -6
- package/src/client/TaskBoardSettingsCard.tsx +74 -1
- package/src/client/board/NewTaskModal.tsx +151 -20
- package/src/client/board/TaskCard.tsx +3 -3
- package/src/client/board/TaskDetail.tsx +219 -18
- package/src/client/board/VerificationReport.tsx +132 -13
- package/src/client/board/status-key.ts +3 -0
- package/src/client/index.ts +43 -50
- package/src/client/locales.ts +137 -4
- package/src/client/plugin-card-seat.ts +7 -0
- package/src/client/schedule-zone.ts +100 -2
- package/src/core/controller.ts +12 -8
- package/src/core/poll-cadence.ts +41 -0
- package/src/core/store.ts +64 -15
- package/src/core/tasks.ts +193 -39
- package/src/core/use-cases/task-create.ts +30 -9
- package/src/core/use-cases/task-schedule.ts +190 -25
- package/src/core/use-cases/task-update.ts +3 -3
- package/src/core/verification-budget.ts +115 -0
- package/src/core/verification.ts +785 -17
- package/src/host/agent-tools.ts +106 -21
- package/src/host/verification-gate.ts +172 -16
- package/src/host/verification-runner.ts +207 -40
- package/src/host-ledger.ts +328 -42
- package/src/host-runner.ts +52 -19
- package/src/host-service.ts +299 -36
- package/src/index.ts +101 -5
- package/src/protocol.ts +132 -28
- package/lib/invariant.js +0 -6
- package/lib/types/client/board-card-seat.d.ts +0 -34
- package/lib/types/invariant.d.ts +0 -3
- package/src/client/board-card-seat.ts +0 -102
- package/src/invariant.ts +0 -4
|
@@ -1,33 +1,54 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Schedule use
|
|
3
|
-
* Pure ledger transitions (no persistence or notify
|
|
4
|
-
* orchestrates those). Validation and next-run computation
|
|
5
|
-
* the core cron parser (schedule.ts) and the withSchedule
|
|
2
|
+
* Schedule use cases: arm/disarm a task's scheduling rule, and record what a
|
|
3
|
+
* due occurrence did to it. Pure ledger transitions (no persistence or notify
|
|
4
|
+
* — the controller orchestrates those). Validation and next-run computation
|
|
5
|
+
* live here, sharing the core cron parser (schedule.ts) and the withSchedule
|
|
6
|
+
* transition.
|
|
7
|
+
*
|
|
8
|
+
* A rule is either a recurring cron rule or a single planned instant, and
|
|
9
|
+
* either kind may be capped by a run budget. The counter (`runCount`) moves
|
|
10
|
+
* only when the Host actually opened an execution; a skipped occurrence
|
|
11
|
+
* (busy, unconfirmed permission, missed instant, unreachable plan) moves the
|
|
12
|
+
* rule without consuming its budget.
|
|
6
13
|
*/
|
|
7
14
|
import { isValidCron, isValidTimeZone, nextRunAtMs } from '../schedule.ts'
|
|
8
|
-
import { withSchedule, type TaskRecord } from '../tasks.ts'
|
|
15
|
+
import { withSchedule, type ScheduleMode, type ScheduleRule, type ScheduleStopReason, type TaskRecord } from '../tasks.ts'
|
|
9
16
|
|
|
10
17
|
/** Fields the schedule use case may change on a rule. */
|
|
11
18
|
export interface SetSchedulePatch {
|
|
12
19
|
enabled?: boolean
|
|
20
|
+
/** Which kind of plan to write; an absent key keeps the current mode. */
|
|
21
|
+
mode?: ScheduleMode
|
|
22
|
+
/** 5-field cron expression of a recurring rule. */
|
|
13
23
|
cron?: string
|
|
14
|
-
/**
|
|
24
|
+
/** Planned instant of a one-shot rule (ms epoch). */
|
|
25
|
+
at?: number
|
|
26
|
+
/** IANA zone for the wall clock; `null` clears it back to the Host zone. */
|
|
15
27
|
timeZone?: string | null
|
|
28
|
+
/** Run cap of a recurring rule; `null` clears it back to unlimited. */
|
|
29
|
+
maxRuns?: number | null
|
|
16
30
|
}
|
|
17
31
|
|
|
18
32
|
/** Result of arming/disarming a rule. */
|
|
19
33
|
export interface SetScheduleResult {
|
|
20
34
|
/** The next ledger; unchanged reference when rejected. */
|
|
21
35
|
tasks: readonly TaskRecord[]
|
|
22
|
-
/** Whether the rule was applied (false on unknown task / invalid
|
|
36
|
+
/** Whether the rule was applied (false on unknown task / invalid plan). */
|
|
23
37
|
applied: boolean
|
|
24
38
|
}
|
|
25
39
|
|
|
26
40
|
/**
|
|
27
|
-
* Set an on-board task's
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
41
|
+
* Set an on-board task's scheduling rule. An unknown or archived task, an
|
|
42
|
+
* unusable zone, a blank or invalid cron, a one-shot with no usable instant,
|
|
43
|
+
* a one-shot armed in the past, and a run cap that is not a positive integer
|
|
44
|
+
* are all rejected (state untouched). An enabled rule computes its next run
|
|
45
|
+
* instant immediately — a one-shot's next instant IS its planned instant — and
|
|
46
|
+
* a disabled rule carries none.
|
|
47
|
+
*
|
|
48
|
+
* Arming a rule that was not enabled, or switching its kind, starts a fresh
|
|
49
|
+
* budget: the run counter and any previous stop/skip record are cleared. Edits
|
|
50
|
+
* to a live rule (expression, zone, cap) keep the consumed count, so an
|
|
51
|
+
* already-spent budget can never silently reset and overrun.
|
|
31
52
|
* @param tasks - current ledger.
|
|
32
53
|
* @param id - the task to schedule.
|
|
33
54
|
* @param patch - rule fields to change (absent fields keep their current value).
|
|
@@ -44,28 +65,72 @@ export function applySetSchedule(
|
|
|
44
65
|
const task = tasks.find(candidate => candidate.id === id)
|
|
45
66
|
if (task === undefined || task.archivedAt !== undefined) return { tasks, applied: false }
|
|
46
67
|
const current = task.schedule
|
|
47
|
-
const
|
|
48
|
-
if (cron === '' || !isValidCron(cron)) return { tasks, applied: false }
|
|
68
|
+
const mode: ScheduleMode = patch.mode ?? current?.mode ?? 'cron'
|
|
49
69
|
// An explicit null clears the stored zone; an absent key keeps it.
|
|
50
|
-
const
|
|
51
|
-
if (
|
|
70
|
+
const timeZone = patch.timeZone === undefined ? current?.timeZone : patch.timeZone ?? undefined
|
|
71
|
+
if (timeZone !== undefined && !isValidTimeZone(timeZone)) return { tasks, applied: false }
|
|
52
72
|
const enabled = patch.enabled ?? current?.enabled ?? false
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
|
|
73
|
+
const restart = (enabled && current?.enabled !== true) || (current?.mode ?? 'cron') !== mode
|
|
74
|
+
const runCount = restart ? 0 : current?.runCount ?? 0
|
|
75
|
+
const effectiveZone = timeZone ?? hostTimeZone
|
|
76
|
+
const cleared = restart
|
|
77
|
+
? { endedAt: undefined, endedReason: undefined, skippedAt: undefined, skippedReason: undefined }
|
|
78
|
+
: { endedAt: current?.endedAt, endedReason: current?.endedReason, skippedAt: current?.skippedAt, skippedReason: current?.skippedReason }
|
|
79
|
+
|
|
80
|
+
if (mode === 'once') {
|
|
81
|
+
const at = patch.at ?? current?.at
|
|
82
|
+
if (at === undefined || !Number.isInteger(at)) return { tasks, applied: false }
|
|
83
|
+
// A one-shot is armed for a FUTURE instant: re-arming one at or before
|
|
84
|
+
// "now" would fire on the next tick, which is not what the user chose. A
|
|
85
|
+
// disabled rule may keep a stored past instant as the record of its plan.
|
|
86
|
+
if (enabled && at <= now) return { tasks, applied: false }
|
|
87
|
+
return {
|
|
88
|
+
tasks: tasks.map(candidate => candidate.id !== id ? candidate : withSchedule(candidate, {
|
|
89
|
+
enabled,
|
|
90
|
+
mode: 'once',
|
|
91
|
+
at,
|
|
92
|
+
cron: undefined,
|
|
93
|
+
maxRuns: undefined,
|
|
94
|
+
timeZone: timeZone ?? undefined,
|
|
95
|
+
nextRunAt: enabled ? at : undefined,
|
|
96
|
+
runCount,
|
|
97
|
+
...cleared,
|
|
98
|
+
}, now)),
|
|
99
|
+
applied: true,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const cron = (patch.cron ?? current?.cron ?? '').trim()
|
|
104
|
+
if (cron === '' || !isValidCron(cron)) return { tasks, applied: false }
|
|
105
|
+
// An absent key keeps the stored cap; an explicit null clears it (unlimited).
|
|
106
|
+
const maxRuns = ('maxRuns' in patch ? patch.maxRuns : current?.maxRuns) ?? undefined
|
|
107
|
+
if (maxRuns !== undefined && (!Number.isInteger(maxRuns) || maxRuns < 1)) return { tasks, applied: false }
|
|
108
|
+
const spent = maxRuns !== undefined && runCount >= maxRuns
|
|
109
|
+
const armed = enabled && !spent
|
|
110
|
+
const nextRunAt = armed ? nextRunAtMs(cron, now, effectiveZone) : undefined
|
|
111
|
+
if (armed && nextRunAt === undefined) return { tasks, applied: false }
|
|
112
|
+
// Lowering the cap below what the rule already ran ends it immediately
|
|
113
|
+
// instead of leaving an exhausted rule that would wait for a fire to die.
|
|
114
|
+
const exhausted = enabled && spent
|
|
56
115
|
return {
|
|
57
|
-
tasks: tasks.map(candidate =>
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
116
|
+
tasks: tasks.map(candidate => candidate.id !== id ? candidate : withSchedule(candidate, {
|
|
117
|
+
enabled: armed,
|
|
118
|
+
mode: 'cron',
|
|
119
|
+
cron,
|
|
120
|
+
at: undefined,
|
|
121
|
+
maxRuns,
|
|
122
|
+
timeZone: timeZone ?? undefined,
|
|
123
|
+
nextRunAt,
|
|
124
|
+
runCount,
|
|
125
|
+
...(exhausted ? { endedAt: now, endedReason: 'limit' as const } : cleared),
|
|
126
|
+
}, now)),
|
|
61
127
|
applied: true,
|
|
62
128
|
}
|
|
63
129
|
}
|
|
64
130
|
|
|
65
131
|
/**
|
|
66
|
-
* Roll a task's schedule rule forward (
|
|
67
|
-
* due instant and the trigger instant. No-op for tasks without a rule
|
|
68
|
-
* mid-tick, for example).
|
|
132
|
+
* Roll a task's schedule rule forward (legacy pure-controller seam): persist
|
|
133
|
+
* the next due instant and the trigger instant. No-op for tasks without a rule.
|
|
69
134
|
* @param tasks - current ledger.
|
|
70
135
|
* @param id - the task to roll forward.
|
|
71
136
|
* @param nextRunAt - next due instant (may be undefined to clear).
|
|
@@ -84,3 +149,103 @@ export function applyScheduleNextRun(
|
|
|
84
149
|
? withSchedule(task, { nextRunAt, lastTriggeredAt }, now)
|
|
85
150
|
: task)
|
|
86
151
|
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The effect one due occurrence has on its rule, applied by the Host ledger.
|
|
155
|
+
*/
|
|
156
|
+
export interface ScheduleProgress {
|
|
157
|
+
/** Executions this occurrence actually opened (0 or 1), added to the counter. */
|
|
158
|
+
consumed: number
|
|
159
|
+
/** Next due instant; absent stops the rule. */
|
|
160
|
+
nextRunAt?: number
|
|
161
|
+
/** Trigger instant recorded as `lastTriggeredAt` (a consumed run only). */
|
|
162
|
+
lastTriggeredAt?: number
|
|
163
|
+
/** Terminal reason when the rule stops itself (disarms it). */
|
|
164
|
+
endedReason?: ScheduleStopReason
|
|
165
|
+
/** Why this occurrence produced no run, when it did not. */
|
|
166
|
+
skippedReason?: ScheduleStopReason
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Refund the one scheduled run an occurrence consumed before its launch failed
|
|
171
|
+
* without ever creating a session. Nothing ran, so the run budget is restored
|
|
172
|
+
* and the rule is left able to serve its next occurrence — a launch failure
|
|
173
|
+
* must never silently spend the user's limited budget.
|
|
174
|
+
*
|
|
175
|
+
* A recurring rule keeps its already-rolled-forward target; a fire that ended
|
|
176
|
+
* the rule at its cap (no target left) is restored and re-armed at the next
|
|
177
|
+
* occurrence, so a failed launch consumes nothing. A one-shot has no later
|
|
178
|
+
* occurrence, so it is left stopped with `launch-failed` and its run returned.
|
|
179
|
+
* A rule the user disabled in the meantime is not resurrected.
|
|
180
|
+
* @param tasks - current ledger.
|
|
181
|
+
* @param id - the task whose occurrence is being refunded.
|
|
182
|
+
* @param now - clock instant (ms epoch).
|
|
183
|
+
* @param hostTimeZone - zone a rule with no stored zone follows.
|
|
184
|
+
* @returns the next ledger.
|
|
185
|
+
*/
|
|
186
|
+
export function applyScheduleRefund(
|
|
187
|
+
tasks: readonly TaskRecord[],
|
|
188
|
+
id: string,
|
|
189
|
+
now: number,
|
|
190
|
+
hostTimeZone: string,
|
|
191
|
+
): readonly TaskRecord[] {
|
|
192
|
+
return tasks.map(task => {
|
|
193
|
+
if (task.id !== id || task.archivedAt !== undefined || task.schedule === undefined) return task
|
|
194
|
+
const rule = task.schedule
|
|
195
|
+
const runCount = Math.max(rule.runCount - 1, 0)
|
|
196
|
+
if (rule.mode === 'once') {
|
|
197
|
+
return withSchedule(task, {
|
|
198
|
+
enabled: false,
|
|
199
|
+
runCount,
|
|
200
|
+
nextRunAt: undefined,
|
|
201
|
+
endedAt: now,
|
|
202
|
+
endedReason: 'launch-failed',
|
|
203
|
+
skippedAt: undefined,
|
|
204
|
+
skippedReason: undefined,
|
|
205
|
+
}, now)
|
|
206
|
+
}
|
|
207
|
+
const refunded = withSchedule(task, {
|
|
208
|
+
runCount,
|
|
209
|
+
skippedAt: now,
|
|
210
|
+
skippedReason: 'launch-failed',
|
|
211
|
+
}, now)
|
|
212
|
+
// The fire already rolled the target forward; only a fire that spent its
|
|
213
|
+
// last run (target cleared, ended at the cap) needs to be restored.
|
|
214
|
+
if (rule.nextRunAt !== undefined || rule.endedReason !== 'limit') return refunded
|
|
215
|
+
const cron = rule.cron ?? ''
|
|
216
|
+
const next = cron === '' ? undefined : nextRunAtMs(cron, now, rule.timeZone ?? hostTimeZone)
|
|
217
|
+
return withSchedule(refunded, next === undefined
|
|
218
|
+
? { enabled: false, endedAt: now, endedReason: 'no-target' }
|
|
219
|
+
: { enabled: true, nextRunAt: next, endedAt: undefined, endedReason: undefined }, now)
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Record what a due occurrence did to a task's rule: advance the run counter
|
|
225
|
+
* by the executions it opened, set the next target (or stop the rule), and
|
|
226
|
+
* note a terminal reason or a skipped occurrence. Nothing is written for a
|
|
227
|
+
* task that has no rule or has been archived mid-tick.
|
|
228
|
+
* @param tasks - current ledger.
|
|
229
|
+
* @param id - the task whose rule fired.
|
|
230
|
+
* @param progress - the occurrence's effect.
|
|
231
|
+
* @param now - clock instant (ms epoch).
|
|
232
|
+
* @returns the next ledger.
|
|
233
|
+
*/
|
|
234
|
+
export function applyScheduleProgress(
|
|
235
|
+
tasks: readonly TaskRecord[],
|
|
236
|
+
id: string,
|
|
237
|
+
progress: ScheduleProgress,
|
|
238
|
+
now: number,
|
|
239
|
+
): readonly TaskRecord[] {
|
|
240
|
+
return tasks.map(task => {
|
|
241
|
+
if (task.id !== id || task.archivedAt !== undefined || task.schedule === undefined) return task
|
|
242
|
+
const patch: Partial<ScheduleRule> = {
|
|
243
|
+
runCount: task.schedule.runCount + progress.consumed,
|
|
244
|
+
nextRunAt: progress.nextRunAt,
|
|
245
|
+
...(progress.lastTriggeredAt === undefined ? {} : { lastTriggeredAt: progress.lastTriggeredAt }),
|
|
246
|
+
...(progress.endedReason === undefined ? {} : { enabled: false, endedAt: now, endedReason: progress.endedReason }),
|
|
247
|
+
...(progress.skippedReason === undefined ? {} : { skippedAt: now, skippedReason: progress.skippedReason }),
|
|
248
|
+
}
|
|
249
|
+
return withSchedule(task, patch, now)
|
|
250
|
+
})
|
|
251
|
+
}
|
|
@@ -110,9 +110,9 @@ export function applyUpdateTask(
|
|
|
110
110
|
if ('reuseSession' in patch) next.reuseSession = patch.reuseSession === true ? true : undefined
|
|
111
111
|
// Team execution is a boolean opt-in with the same clear-by-false rule.
|
|
112
112
|
if ('teamRun' in patch) next.teamRun = patch.teamRun === true ? true : undefined
|
|
113
|
-
// The /goal opt-in is default-
|
|
114
|
-
// card to the default and stores nothing
|
|
115
|
-
if ('goalRun' in patch) next.goalRun = patch.goalRun ===
|
|
113
|
+
// The /goal opt-in is default-OFF: true joins the goal run, while false
|
|
114
|
+
// (or an explicit null) returns the card to the default and stores nothing.
|
|
115
|
+
if ('goalRun' in patch) next.goalRun = patch.goalRun === true ? true : undefined
|
|
116
116
|
// The acceptance opt-out is the mirror image: default OFF, so only true
|
|
117
117
|
// pins a card out of the gate; false (or an explicit null) returns it to
|
|
118
118
|
// inheriting the board-wide switch and stores nothing.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Goal acceptance time budgets: the one place the judge's per-call ceiling and
|
|
3
|
+
* the acceptance's total ceiling live.
|
|
4
|
+
*
|
|
5
|
+
* Both are user settings (seconds), so the bounds and the seconds to
|
|
6
|
+
* milliseconds conversion live here rather than in either half: the Host
|
|
7
|
+
* clamps a hand-edited profile value to the same range the schema declares, and
|
|
8
|
+
* the gate reads the effective values live, so raising a ceiling takes effect
|
|
9
|
+
* without a reload.
|
|
10
|
+
*
|
|
11
|
+
* The two numbers are one budget, not two independent knobs: one acceptance is
|
|
12
|
+
* {@link ACCEPTANCE_JUDGE_CALLS} serial judge calls (three criteria times two
|
|
13
|
+
* rounds), so a total budget that cannot fit the calls can only end the
|
|
14
|
+
* acceptance early. {@link DEFAULT_VERIFICATION_BUDGET_SECONDS} is therefore
|
|
15
|
+
* derived from the per-call default times the call count plus two retries.
|
|
16
|
+
*
|
|
17
|
+
* @module dsh-task-board/core/verification-budget
|
|
18
|
+
*/
|
|
19
|
+
import { CODING_CRITERIA, VERIFICATION_ROUNDS } from './verification.ts'
|
|
20
|
+
|
|
21
|
+
/** Judge calls one acceptance issues: every criterion, every round. */
|
|
22
|
+
export const ACCEPTANCE_JUDGE_CALLS: number = CODING_CRITERIA.length * VERIFICATION_ROUNDS
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Per-judge-call ceiling used when the row names none: two and a half minutes
|
|
26
|
+
* for a reasoning model reading an 80,000 character trace with a 16,384 token
|
|
27
|
+
* output cap. The former 120s constant was tight enough that a slow but healthy
|
|
28
|
+
* route produced a wall of per-call timeouts, and every one of them was charged
|
|
29
|
+
* as an acceptance anomaly against a budget that then failed the card.
|
|
30
|
+
*/
|
|
31
|
+
export const DEFAULT_VERIFICATION_CALL_TIMEOUT_SECONDS = 150
|
|
32
|
+
|
|
33
|
+
/** Shortest accepted per-call ceiling: below this no useful judge answers. */
|
|
34
|
+
export const VERIFICATION_CALL_TIMEOUT_MIN_SECONDS = 30
|
|
35
|
+
|
|
36
|
+
/** Longest accepted per-call ceiling, ten minutes. */
|
|
37
|
+
export const VERIFICATION_CALL_TIMEOUT_MAX_SECONDS = 600
|
|
38
|
+
|
|
39
|
+
/** Headroom the default total budget leaves beyond the mandatory calls. */
|
|
40
|
+
const DEFAULT_BUDGET_EXTRA_CALLS = 2
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Total ceiling of one acceptance attempt, covering evidence rendering and
|
|
44
|
+
* every judge call including retries. Derived from the per-call default rather
|
|
45
|
+
* than pinned, because a total budget smaller than
|
|
46
|
+
* {@link ACCEPTANCE_JUDGE_CALLS} times the per-call ceiling guarantees that no
|
|
47
|
+
* acceptance can ever finish.
|
|
48
|
+
*/
|
|
49
|
+
export const DEFAULT_VERIFICATION_BUDGET_SECONDS: number =
|
|
50
|
+
DEFAULT_VERIFICATION_CALL_TIMEOUT_SECONDS * (ACCEPTANCE_JUDGE_CALLS + DEFAULT_BUDGET_EXTRA_CALLS)
|
|
51
|
+
|
|
52
|
+
/** Shortest accepted total budget: long enough for at least one judge call. */
|
|
53
|
+
export const VERIFICATION_BUDGET_MIN_SECONDS = 120
|
|
54
|
+
|
|
55
|
+
/** Longest accepted total budget, thirty minutes. */
|
|
56
|
+
export const VERIFICATION_BUDGET_MAX_SECONDS = 1800
|
|
57
|
+
|
|
58
|
+
/** The default total budget in milliseconds, the face the abort timer takes. */
|
|
59
|
+
export const DEFAULT_VERIFICATION_BUDGET_MS = DEFAULT_VERIFICATION_BUDGET_SECONDS * 1_000
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Clamp a configured ceiling into the supported range. A value the schema
|
|
63
|
+
* would refuse (non-finite, below the floor, above the ceiling) still leaves
|
|
64
|
+
* the board judging: an out-of-range setting never disables acceptance.
|
|
65
|
+
* @param value - configured seconds, or undefined for the default.
|
|
66
|
+
* @param fallback - the default to use when nothing usable was configured.
|
|
67
|
+
* @param min - shortest accepted value.
|
|
68
|
+
* @param max - longest accepted value.
|
|
69
|
+
* @returns the effective value in seconds.
|
|
70
|
+
*/
|
|
71
|
+
function clampSeconds(value: number | undefined, fallback: number, min: number, max: number): number {
|
|
72
|
+
if (value === undefined || !Number.isFinite(value)) return fallback
|
|
73
|
+
return Math.min(Math.max(Math.trunc(value), min), max)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The effective per-judge-call ceiling.
|
|
78
|
+
* @param seconds - configured seconds, or undefined for the default.
|
|
79
|
+
* @returns the effective ceiling in seconds.
|
|
80
|
+
*/
|
|
81
|
+
export function normalizeVerificationCallTimeoutSeconds(seconds: number | undefined): number {
|
|
82
|
+
return clampSeconds(
|
|
83
|
+
seconds,
|
|
84
|
+
DEFAULT_VERIFICATION_CALL_TIMEOUT_SECONDS,
|
|
85
|
+
VERIFICATION_CALL_TIMEOUT_MIN_SECONDS,
|
|
86
|
+
VERIFICATION_CALL_TIMEOUT_MAX_SECONDS,
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The effective per-judge-call ceiling the timer face takes.
|
|
92
|
+
* @param seconds - configured seconds, or undefined for the default.
|
|
93
|
+
* @returns the effective ceiling in milliseconds.
|
|
94
|
+
*/
|
|
95
|
+
export function verificationCallTimeoutMs(seconds: number | undefined): number {
|
|
96
|
+
return normalizeVerificationCallTimeoutSeconds(seconds) * 1_000
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The effective total budget of one acceptance attempt.
|
|
101
|
+
* @param seconds - configured seconds, or undefined for the default.
|
|
102
|
+
* @returns the effective budget in seconds.
|
|
103
|
+
*/
|
|
104
|
+
export function normalizeVerificationBudgetSeconds(seconds: number | undefined): number {
|
|
105
|
+
return clampSeconds(seconds, DEFAULT_VERIFICATION_BUDGET_SECONDS, VERIFICATION_BUDGET_MIN_SECONDS, VERIFICATION_BUDGET_MAX_SECONDS)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The effective total budget the abort face takes.
|
|
110
|
+
* @param seconds - configured seconds, or undefined for the default.
|
|
111
|
+
* @returns the effective budget in milliseconds.
|
|
112
|
+
*/
|
|
113
|
+
export function verificationBudgetMs(seconds: number | undefined): number {
|
|
114
|
+
return normalizeVerificationBudgetSeconds(seconds) * 1_000
|
|
115
|
+
}
|