@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
|
@@ -10,9 +10,18 @@
|
|
|
10
10
|
import { createUserMessage, type FinishReason, type LlmRuntime } from '@deepseek-ai/dsh-llm'
|
|
11
11
|
import { openOneShotStream } from './llm-dispatch.ts'
|
|
12
12
|
import { createHash } from 'node:crypto'
|
|
13
|
+
import { DEFAULT_VERIFICATION_BUDGET_MS } from '../core/verification-budget.ts'
|
|
13
14
|
import {
|
|
14
15
|
CODING_CRITERIA,
|
|
15
16
|
EMPTY_WORK_BASELINE,
|
|
17
|
+
workSlotOfRound,
|
|
18
|
+
VERIFICATION_DETAIL_MAX_FINDINGS,
|
|
19
|
+
VERIFICATION_DETAIL_MAX_PROBLEM_CHARS,
|
|
20
|
+
VERIFICATION_DETAIL_MAX_QUOTE_CHARS,
|
|
21
|
+
assessAcceptanceValidity,
|
|
22
|
+
type AcceptanceEvidenceText,
|
|
23
|
+
type VerificationCriterionFinding,
|
|
24
|
+
type VerificationInvalidReason,
|
|
16
25
|
VERIFICATION_ROUNDS,
|
|
17
26
|
VERIFICATION_THRESHOLD,
|
|
18
27
|
acceptancePassed,
|
|
@@ -38,11 +47,21 @@ export const VERIFICATION_MAX_TRACE_CHARS = 80_000
|
|
|
38
47
|
export const VERIFICATION_MAX_ENTRY_CHARS = 4_000
|
|
39
48
|
/** Largest per-finding text kept from one judge answer. */
|
|
40
49
|
export const VERIFICATION_MAX_FINDING_CHARS = 400
|
|
41
|
-
/**
|
|
50
|
+
/** Largest verbatim citation kept from one finding. */
|
|
51
|
+
export const VERIFICATION_MAX_QUOTE_CHARS = VERIFICATION_DETAIL_MAX_QUOTE_CHARS
|
|
52
|
+
/** Largest requirement/observation/gap statement kept from one finding. */
|
|
53
|
+
export const VERIFICATION_MAX_PROBLEM_CHARS = VERIFICATION_DETAIL_MAX_PROBLEM_CHARS
|
|
54
|
+
/** Structured findings kept per judge answer and per acceptance. */
|
|
42
55
|
export const VERIFICATION_MAX_FINDINGS_PER_CALL = 3
|
|
43
|
-
export const VERIFICATION_MAX_FINDINGS =
|
|
44
|
-
/**
|
|
45
|
-
|
|
56
|
+
export const VERIFICATION_MAX_FINDINGS = VERIFICATION_DETAIL_MAX_FINDINGS
|
|
57
|
+
/**
|
|
58
|
+
* Default ceiling of ONE judge request. The effective ceiling is a row setting
|
|
59
|
+
* (`goalVerificationCallTimeoutSeconds`), read live by the gate, because 120s was
|
|
60
|
+
* tight for a reasoning model reading an 80,000 character trace with a 16,384
|
|
61
|
+
* token output cap: every timeout it produced was charged as an acceptance
|
|
62
|
+
* anomaly against a budget that then failed the card.
|
|
63
|
+
*/
|
|
64
|
+
export const VERIFICATION_CALL_TIMEOUT_MS = 150_000
|
|
46
65
|
export const VERIFICATION_MAX_TOKENS = 16_384
|
|
47
66
|
export const VERIFICATION_TEMPERATURE = 0.2
|
|
48
67
|
export const VERIFICATION_REQUEST_ATTEMPTS = 3
|
|
@@ -54,7 +73,25 @@ export const VERIFICATION_REDACT_PATTERNS: readonly string[] = [
|
|
|
54
73
|
]
|
|
55
74
|
|
|
56
75
|
/** Why one acceptance could not produce a verdict. */
|
|
57
|
-
export type VerificationErrorKind = 'route-unavailable' | 'timeout' | 'auth' | 'parse' | 'request' | 'aborted'
|
|
76
|
+
export type VerificationErrorKind = 'route-unavailable' | 'timeout' | 'auth' | 'parse' | 'request' | 'aborted' | 'budget'
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The time one acceptance attempt may still spend.
|
|
80
|
+
*
|
|
81
|
+
* `deadline` is the instant the whole attempt runs out; `callTimeoutMs` is the
|
|
82
|
+
* ceiling of a single judge call. The pair is what ties the retry budget to the
|
|
83
|
+
* acceptance budget: a call is not started unless its own ceiling still fits
|
|
84
|
+
* inside what is left, because starting one the outer budget will abort anyway
|
|
85
|
+
* only burns wall time and turns a slow machine into an anomaly.
|
|
86
|
+
*/
|
|
87
|
+
export interface AcceptanceBudget {
|
|
88
|
+
/** Absolute instant (ms epoch) the acceptance attempt runs out at. */
|
|
89
|
+
deadline: number
|
|
90
|
+
/** Ceiling of one judge call in milliseconds. */
|
|
91
|
+
callTimeoutMs: number
|
|
92
|
+
/** Clock the caller runs on, injected so tests drive it deterministically. */
|
|
93
|
+
now: () => number
|
|
94
|
+
}
|
|
58
95
|
|
|
59
96
|
/** An acceptance anomaly: never a quality verdict, always recorded as one. */
|
|
60
97
|
export class VerificationError extends Error {
|
|
@@ -239,16 +276,54 @@ interface JudgeAnswer {
|
|
|
239
276
|
usage: VerificationUsage
|
|
240
277
|
}
|
|
241
278
|
|
|
242
|
-
/**
|
|
279
|
+
/**
|
|
280
|
+
* One finding the judge reported, before its citation is resolved against the
|
|
281
|
+
* evidence the judge actually saw.
|
|
282
|
+
*/
|
|
243
283
|
interface RawFinding {
|
|
244
|
-
criterion
|
|
245
|
-
|
|
284
|
+
/** The criterion id this finding is attached to (always the caller's id). */
|
|
285
|
+
criterionId: string
|
|
286
|
+
/** The criterion name as the prompt offered it. */
|
|
287
|
+
criterionName: string
|
|
288
|
+
/** Which evidence block the citation claims to come from. */
|
|
289
|
+
location: VerificationCriterionFinding['location']
|
|
290
|
+
quote: string
|
|
291
|
+
requirement: string
|
|
292
|
+
observation: string
|
|
293
|
+
gap: string
|
|
246
294
|
body: string
|
|
247
295
|
action?: string
|
|
248
296
|
}
|
|
249
297
|
|
|
250
|
-
/**
|
|
251
|
-
function
|
|
298
|
+
/** How one cited slot maps onto the caller's own evidence under a swapped round. */
|
|
299
|
+
function locationOfSlot(slot: 'A' | 'B', round: number): VerificationCriterionFinding['location'] {
|
|
300
|
+
// The odd round swaps the two slots: citing the slot the work occupies is a
|
|
301
|
+
// citation of the work's own trajectory, and citing the other one is a
|
|
302
|
+
// citation of the empty-work baseline, which can never veto the work.
|
|
303
|
+
return slot === workSlotOfRound(round) ? 'trajectory' : 'baseline'
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Resolve one `location` (or legacy `evidence`) attribute into an evidence location. */
|
|
307
|
+
function readLocation(raw: string, round: number): VerificationCriterionFinding['location'] | undefined {
|
|
308
|
+
const value = raw.trim().toUpperCase()
|
|
309
|
+
if (value === 'TASK') return 'task'
|
|
310
|
+
if (value === 'CONTEXT' || value === 'WORKSPACE') return 'workspace'
|
|
311
|
+
if (value === 'TRAJECTORY_A' || value === 'A') return locationOfSlot('A', round)
|
|
312
|
+
if (value === 'TRAJECTORY_B' || value === 'B') return locationOfSlot('B', round)
|
|
313
|
+
return undefined
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Parse the `<finding ...>` elements out of one judge answer.
|
|
318
|
+
*
|
|
319
|
+
* A finding must name the criterion this prompt offered and cite an evidence
|
|
320
|
+
* location the prompt actually rendered; a finding for another criterion, a
|
|
321
|
+
* citation of a location that was never offered, and an empty body are dropped
|
|
322
|
+
* here rather than echoed back as if the judge had located something. The
|
|
323
|
+
* quote is carried verbatim: whether it really occurs in the evidence it names
|
|
324
|
+
* is decided by the caller, which is the only side holding the evidence text.
|
|
325
|
+
*/
|
|
326
|
+
function parseFindings(text: string, criterion: VerificationCriterion, round: number): RawFinding[] {
|
|
252
327
|
const pattern = /<finding\s+([^>]*)>([\s\S]*?)<\/finding>/giu
|
|
253
328
|
const attributes = /([A-Za-z_][A-Za-z0-9_]*)\s*=\s*"([^"]*)"/gu
|
|
254
329
|
const found: RawFinding[] = []
|
|
@@ -256,45 +331,77 @@ function parseFindings(text: string, criterion: VerificationCriterion): RawFindi
|
|
|
256
331
|
for (let match = pattern.exec(text); match !== null && found.length < VERIFICATION_MAX_FINDINGS_PER_CALL; match = pattern.exec(text)) {
|
|
257
332
|
const attrs = new Map<string, string>()
|
|
258
333
|
for (let attribute = attributes.exec(match[1] ?? ''); attribute !== null; attribute = attributes.exec(match[1] ?? '')) {
|
|
259
|
-
if (['criterion', 'evidence', '
|
|
334
|
+
if (['criterion', 'evidence', 'location', 'requirement', 'observation', 'gap', 'quote', 'action'].includes(attribute[1] ?? '')) {
|
|
335
|
+
attrs.set(attribute[1] ?? '', attribute[2] ?? '')
|
|
336
|
+
}
|
|
260
337
|
}
|
|
261
338
|
const named = attrs.get('criterion') ?? ''
|
|
262
|
-
// A finding must name the criterion this prompt offered
|
|
263
|
-
//
|
|
339
|
+
// A finding must name the criterion this prompt offered; anything else is
|
|
340
|
+
// dropped, never echoed.
|
|
264
341
|
if (named !== '' && named.toLowerCase() !== criterion.name.toLowerCase() && named.toLowerCase() !== criterion.id.toLowerCase()) continue
|
|
265
|
-
const
|
|
266
|
-
if (
|
|
342
|
+
const location = readLocation(attrs.get('location') ?? attrs.get('evidence') ?? '', round)
|
|
343
|
+
if (location === undefined) continue
|
|
344
|
+
const quote = (attrs.get('quote') ?? '').trim().slice(0, VERIFICATION_MAX_QUOTE_CHARS)
|
|
345
|
+
const requirement = (attrs.get('requirement') ?? '').trim().slice(0, VERIFICATION_MAX_PROBLEM_CHARS)
|
|
346
|
+
const observation = (attrs.get('observation') ?? '').trim().slice(0, VERIFICATION_MAX_PROBLEM_CHARS)
|
|
347
|
+
const gap = (attrs.get('gap') ?? '').trim().slice(0, VERIFICATION_MAX_PROBLEM_CHARS)
|
|
267
348
|
const body = (match[2] ?? '').trim().slice(0, VERIFICATION_MAX_FINDING_CHARS)
|
|
268
|
-
if (body === '') continue
|
|
349
|
+
if (body === '' && quote === '') continue
|
|
269
350
|
const action = (attrs.get('action') ?? '').trim().slice(0, 300)
|
|
270
|
-
const key =
|
|
351
|
+
const key = criterion.id + '|' + location + '|' + quote + '|' + body
|
|
271
352
|
if (seen.has(key)) continue
|
|
272
353
|
seen.add(key)
|
|
273
|
-
found.push({
|
|
354
|
+
found.push({
|
|
355
|
+
criterionId: criterion.id,
|
|
356
|
+
criterionName: criterion.name,
|
|
357
|
+
location,
|
|
358
|
+
quote,
|
|
359
|
+
requirement,
|
|
360
|
+
observation,
|
|
361
|
+
gap,
|
|
362
|
+
body,
|
|
363
|
+
...(action === '' ? {} : { action }),
|
|
364
|
+
})
|
|
274
365
|
}
|
|
275
366
|
return found
|
|
276
367
|
}
|
|
277
368
|
|
|
278
369
|
/** Render one finding as the single feedback line the agent reads. */
|
|
279
370
|
function renderFinding(finding: RawFinding): string {
|
|
280
|
-
const locator = '[' +
|
|
371
|
+
const locator = '[' + finding.criterionName + ' ' + finding.location + ']'
|
|
281
372
|
const action = finding.action === undefined ? '' : ' → 建议核实:' + finding.action
|
|
282
|
-
|
|
373
|
+
const detail = finding.body !== '' ? finding.body : finding.quote
|
|
374
|
+
return locator + ' ' + detail + action
|
|
283
375
|
}
|
|
284
376
|
|
|
285
|
-
/**
|
|
377
|
+
/**
|
|
378
|
+
* One judge request's outcome.
|
|
379
|
+
*
|
|
380
|
+
* Every attempt is admitted against the acceptance budget first: a call whose
|
|
381
|
+
* own ceiling no longer fits in the time left is refused before it is opened,
|
|
382
|
+
* so a slow route cannot spend the whole acceptance budget on calls the outer
|
|
383
|
+
* abort is going to kill anyway.
|
|
384
|
+
*/
|
|
286
385
|
async function judgeOnce(
|
|
287
386
|
llm: LlmRuntime,
|
|
288
387
|
route: VerificationRoute,
|
|
289
388
|
prompt: string,
|
|
290
389
|
signal: AbortSignal,
|
|
390
|
+
budget: AcceptanceBudget,
|
|
291
391
|
): Promise<{ text: string; usage: VerificationUsage }> {
|
|
292
392
|
const usage = emptyUsage()
|
|
293
393
|
let lastError: unknown
|
|
394
|
+
/** The outer abort is the acceptance budget running out, not a cancellation. */
|
|
395
|
+
const stopped = (): VerificationError => budget.now() >= budget.deadline
|
|
396
|
+
? new VerificationError('budget', 'the acceptance budget ran out before the judge answered', usage)
|
|
397
|
+
: new VerificationError('aborted', 'the acceptance was cancelled', usage)
|
|
294
398
|
for (let attempt = 0; attempt < VERIFICATION_REQUEST_ATTEMPTS; attempt++) {
|
|
295
|
-
if (signal.aborted) throw
|
|
399
|
+
if (signal.aborted) throw stopped()
|
|
400
|
+
if (budget.deadline - budget.now() <= budget.callTimeoutMs) {
|
|
401
|
+
throw new VerificationError('budget', 'the acceptance budget cannot fit another judge call', usage)
|
|
402
|
+
}
|
|
296
403
|
const timeout = new AbortController()
|
|
297
|
-
const timer = setTimeout(() => { timeout.abort(new Error('task-board verification: the judge request timed out')) },
|
|
404
|
+
const timer = setTimeout(() => { timeout.abort(new Error('task-board verification: the judge request timed out')) }, budget.callTimeoutMs)
|
|
298
405
|
const forward = (): void => { timeout.abort(signal.reason) }
|
|
299
406
|
signal.addEventListener('abort', forward, { once: true })
|
|
300
407
|
try {
|
|
@@ -333,7 +440,7 @@ async function judgeOnce(
|
|
|
333
440
|
finishReason = chunk.type === 'finish' ? chunk.reason : undefined
|
|
334
441
|
}
|
|
335
442
|
}
|
|
336
|
-
if (signal.aborted) throw
|
|
443
|
+
if (signal.aborted) throw stopped()
|
|
337
444
|
if (timeout.signal.aborted) throw new VerificationError('timeout', 'the judge request timed out', usage)
|
|
338
445
|
if (finishReason?.kind === 'error' || finishReason?.kind === 'aborted') {
|
|
339
446
|
const failure = finishReason.failure
|
|
@@ -358,9 +465,9 @@ async function judgeOnce(
|
|
|
358
465
|
} catch (error) {
|
|
359
466
|
lastError = error
|
|
360
467
|
if (error instanceof VerificationError && (error.kind === 'aborted' || error.kind === 'auth')) throw error
|
|
361
|
-
if (signal.aborted) throw
|
|
468
|
+
if (signal.aborted) throw stopped()
|
|
362
469
|
if (timeout.signal.aborted) {
|
|
363
|
-
lastError = new VerificationError('timeout', 'the judge request timed out after ' + Math.round(
|
|
470
|
+
lastError = new VerificationError('timeout', 'the judge request timed out after ' + Math.round(budget.callTimeoutMs / 1000) + 's', usage)
|
|
364
471
|
}
|
|
365
472
|
const parseFailure = lastError instanceof VerificationError && lastError.kind === 'parse'
|
|
366
473
|
if (attempt + 1 >= VERIFICATION_REQUEST_ATTEMPTS || (!parseFailure && !retryable(lastError))) break
|
|
@@ -387,8 +494,8 @@ export interface AcceptanceResult {
|
|
|
387
494
|
* Run one acceptance: every criterion is judged twice, the odd round with the
|
|
388
495
|
* A/B slots swapped, and the per-round scores are mapped back and averaged per
|
|
389
496
|
* criterion.
|
|
390
|
-
* @param input - the judge route, evidence, threshold and cancellation.
|
|
391
|
-
* @returns the recorded attempt (quality verdict or
|
|
497
|
+
* @param input - the judge route, evidence, threshold, time budget and cancellation.
|
|
498
|
+
* @returns the recorded attempt (quality verdict, anomaly, or budget stop).
|
|
392
499
|
*/
|
|
393
500
|
export async function runAcceptance(input: {
|
|
394
501
|
llm: LlmRuntime
|
|
@@ -397,6 +504,10 @@ export async function runAcceptance(input: {
|
|
|
397
504
|
threshold: number
|
|
398
505
|
index: number
|
|
399
506
|
signal: AbortSignal
|
|
507
|
+
/** Instant (ms epoch) the attempt runs out at; defaults to now plus the default budget. */
|
|
508
|
+
deadline?: number
|
|
509
|
+
/** Ceiling of one judge call; defaults to {@link VERIFICATION_CALL_TIMEOUT_MS}. */
|
|
510
|
+
callTimeoutMs?: number
|
|
400
511
|
/** Host-observed workspace changes, rendered as the prompt's reference context. */
|
|
401
512
|
context?: string
|
|
402
513
|
/** How many changed files that context shows. */
|
|
@@ -405,6 +516,11 @@ export async function runAcceptance(input: {
|
|
|
405
516
|
}): Promise<AcceptanceResult> {
|
|
406
517
|
const now = input.now ?? Date.now
|
|
407
518
|
const startedAt = now()
|
|
519
|
+
const budget: AcceptanceBudget = {
|
|
520
|
+
deadline: input.deadline ?? startedAt + DEFAULT_VERIFICATION_BUDGET_MS,
|
|
521
|
+
callTimeoutMs: input.callTimeoutMs ?? VERIFICATION_CALL_TIMEOUT_MS,
|
|
522
|
+
now,
|
|
523
|
+
}
|
|
408
524
|
const usage = emptyUsage()
|
|
409
525
|
const evidenceSummary: VerificationEvidenceSummary = {
|
|
410
526
|
chars: input.evidence.trace.length,
|
|
@@ -417,6 +533,15 @@ export async function runAcceptance(input: {
|
|
|
417
533
|
}
|
|
418
534
|
const findings: string[] = []
|
|
419
535
|
const seenFindings = new Set<string>()
|
|
536
|
+
const structured: VerificationCriterionFinding[] = []
|
|
537
|
+
const seenStructured = new Set<string>()
|
|
538
|
+
// The exact evidence texts this acceptance handed to the judge, retained so
|
|
539
|
+
// the validity check can verify every citation against what was really shown.
|
|
540
|
+
const evidenceText: AcceptanceEvidenceText = {
|
|
541
|
+
task: input.evidence.problem,
|
|
542
|
+
trajectory: input.evidence.trace,
|
|
543
|
+
workspace: input.context ?? '',
|
|
544
|
+
}
|
|
420
545
|
try {
|
|
421
546
|
const perCriterion = new Map<string, { criterion: VerificationCriterion; work: number[]; baseline: number[] }>()
|
|
422
547
|
for (const criterion of CODING_CRITERIA) perCriterion.set(criterion.id, { criterion, work: [], baseline: [] })
|
|
@@ -425,8 +550,8 @@ export async function runAcceptance(input: {
|
|
|
425
550
|
const swapped = repeat % 2 === 1
|
|
426
551
|
const candidateA = swapped ? EMPTY_WORK_BASELINE : input.evidence.trace
|
|
427
552
|
const candidateB = swapped ? input.evidence.trace : EMPTY_WORK_BASELINE
|
|
428
|
-
const prompt = buildAcceptancePrompt(input.evidence.problem, candidateA, candidateB, criterion, undefined, input.context)
|
|
429
|
-
const answer = await judgeOnce(input.llm, input.route, prompt, input.signal)
|
|
553
|
+
const prompt = buildAcceptancePrompt(input.evidence.problem, candidateA, candidateB, criterion, undefined, input.context, workSlotOfRound(repeat))
|
|
554
|
+
const answer = await judgeOnce(input.llm, input.route, prompt, input.signal, budget)
|
|
430
555
|
const scoreA = extractScore(answer.text, 'score_A')
|
|
431
556
|
const scoreB = extractScore(answer.text, 'score_B')
|
|
432
557
|
if (answer.usage.usageIncomplete === true) usage.usageIncomplete = true
|
|
@@ -437,13 +562,23 @@ export async function runAcceptance(input: {
|
|
|
437
562
|
const bucket = perCriterion.get(criterion.id)!
|
|
438
563
|
bucket.work.push(swapped ? scoreB : scoreA)
|
|
439
564
|
bucket.baseline.push(swapped ? scoreA : scoreB)
|
|
440
|
-
for (const finding of parseFindings(answer.text, criterion)) {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
565
|
+
for (const finding of parseFindings(answer.text, criterion, repeat)) {
|
|
566
|
+
if (structured.length < VERIFICATION_MAX_FINDINGS) {
|
|
567
|
+
const key = finding.criterionId + '|' + finding.location + '|' + finding.quote + '|' + finding.body
|
|
568
|
+
if (!seenStructured.has(key)) {
|
|
569
|
+
seenStructured.add(key)
|
|
570
|
+
structured.push({
|
|
571
|
+
criterionId: finding.criterionId,
|
|
572
|
+
requirement: finding.requirement !== '' ? finding.requirement : finding.criterionName,
|
|
573
|
+
observation: finding.observation !== '' ? finding.observation : finding.body,
|
|
574
|
+
gap: finding.gap !== '' ? finding.gap : finding.body,
|
|
575
|
+
quote: finding.quote,
|
|
576
|
+
location: finding.location,
|
|
577
|
+
...(finding.action === undefined ? {} : { action: finding.action }),
|
|
578
|
+
})
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
const line = renderFinding(finding)
|
|
447
582
|
if (seenFindings.has(line)) continue
|
|
448
583
|
seenFindings.add(line)
|
|
449
584
|
if (findings.length < VERIFICATION_MAX_FINDINGS) findings.push(line)
|
|
@@ -459,12 +594,23 @@ export async function runAcceptance(input: {
|
|
|
459
594
|
const score = average(criteria.map(criterion => criterion.score))
|
|
460
595
|
const baseline = average(criteria.map(criterion => criterion.baseline))
|
|
461
596
|
const passed = acceptancePassed(score, baseline, criteria, input.threshold)
|
|
597
|
+
// A veto is only bookable when its evidence holds up; a rejecting attempt
|
|
598
|
+
// that cannot substantiate the rejection is recorded as INVALID so it can
|
|
599
|
+
// never consume the quality budget or fail the card.
|
|
600
|
+
const validity = passed
|
|
601
|
+
? { valid: true as const }
|
|
602
|
+
: assessAcceptanceValidity({
|
|
603
|
+
criteria,
|
|
604
|
+
findings: structured,
|
|
605
|
+
evidence: evidenceText,
|
|
606
|
+
omittedCharacters: input.evidence.omittedCharacters,
|
|
607
|
+
})
|
|
462
608
|
return {
|
|
463
609
|
attempt: {
|
|
464
610
|
index: input.index,
|
|
465
611
|
at: startedAt,
|
|
466
|
-
stage: 'quality',
|
|
467
|
-
passed,
|
|
612
|
+
stage: validity.valid ? 'quality' : 'invalid',
|
|
613
|
+
passed: validity.valid ? passed : false,
|
|
468
614
|
score,
|
|
469
615
|
baseline,
|
|
470
616
|
criteria,
|
|
@@ -474,6 +620,11 @@ export async function runAcceptance(input: {
|
|
|
474
620
|
route: input.route,
|
|
475
621
|
channel: 'explicit-tag',
|
|
476
622
|
rounds: VERIFICATION_ROUNDS,
|
|
623
|
+
...(structured.length === 0 ? {} : { criterionFindings: structured }),
|
|
624
|
+
...(validity.valid ? {} : {
|
|
625
|
+
invalidReason: (validity as { reason: VerificationInvalidReason }).reason,
|
|
626
|
+
error: classifyInvalid(validity as { reason: VerificationInvalidReason; detail: string }),
|
|
627
|
+
}),
|
|
477
628
|
},
|
|
478
629
|
}
|
|
479
630
|
} catch (error) {
|
|
@@ -484,7 +635,10 @@ export async function runAcceptance(input: {
|
|
|
484
635
|
attempt: {
|
|
485
636
|
index: input.index,
|
|
486
637
|
at: startedAt,
|
|
487
|
-
stage:
|
|
638
|
+
// A budget stop is its own stage: the time ran out before any verdict,
|
|
639
|
+
// which is neither a judgement of the work nor an anomaly of the
|
|
640
|
+
// judge route, so it is charged to no budget.
|
|
641
|
+
stage: kind === 'budget' ? 'budget' : 'exception',
|
|
488
642
|
passed: false,
|
|
489
643
|
score: 0,
|
|
490
644
|
baseline: 0,
|
|
@@ -518,6 +672,18 @@ function mergeUsage(target: VerificationUsage, source: VerificationUsage): Verif
|
|
|
518
672
|
}
|
|
519
673
|
}
|
|
520
674
|
|
|
675
|
+
/** Prefix one invalid-acceptance message with its class so the report stays actionable. */
|
|
676
|
+
function classifyInvalid(validity: { reason: VerificationInvalidReason; detail: string }): string {
|
|
677
|
+
const labels: Record<VerificationInvalidReason, string> = {
|
|
678
|
+
'missing-finding': '验收无效(未达标判据没有对应的结构化问题反馈)',
|
|
679
|
+
'unlocatable-quote': '验收无效(问题反馈引用不存在于本次裁判所见的证据中)',
|
|
680
|
+
'baseline-finding': '验收无效(问题反馈指向空工作基线而非本次工作)',
|
|
681
|
+
'vacuous-finding': '验收无效(问题反馈空泛,未说明实际观测与差异)',
|
|
682
|
+
'insufficient-evidence': '验收无效(裁判所见证据不足,无法支撑否决)',
|
|
683
|
+
}
|
|
684
|
+
return labels[validity.reason] + ': ' + validity.detail
|
|
685
|
+
}
|
|
686
|
+
|
|
521
687
|
/** Prefix one anomaly message with its class so the report stays actionable. */
|
|
522
688
|
function classifyMessage(kind: VerificationErrorKind, message: string): string {
|
|
523
689
|
const labels: Record<VerificationErrorKind, string> = {
|
|
@@ -527,6 +693,7 @@ function classifyMessage(kind: VerificationErrorKind, message: string): string {
|
|
|
527
693
|
parse: '验收异常(裁判回答无法解析)',
|
|
528
694
|
request: '验收异常(裁判请求失败)',
|
|
529
695
|
aborted: '验收异常(验收被取消)',
|
|
696
|
+
budget: '验收未完成(时间预算耗尽,未产生任何判定,不计入异常额度)',
|
|
530
697
|
}
|
|
531
698
|
return labels[kind] + ': ' + message
|
|
532
699
|
}
|