@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
package/src/core/verification.ts
CHANGED
|
@@ -42,11 +42,73 @@ export const MAX_QUALITY_ATTEMPTS = 2
|
|
|
42
42
|
/**
|
|
43
43
|
* Anomaly budget of one cycle. A timeout, an authentication failure or an
|
|
44
44
|
* unparseable judge answer is not a quality verdict, so it never consumes a
|
|
45
|
-
* quality attempt
|
|
46
|
-
*
|
|
45
|
+
* quality attempt.
|
|
46
|
+
*
|
|
47
|
+
* Reaching it does NOT judge the card: an unusable judge route is a property of
|
|
48
|
+
* the environment, not of the work, so a card must never be failed for it. The
|
|
49
|
+
* gate instead HOLDS the cycle open and runs no further judge call until the
|
|
50
|
+
* recorded anomalies are cleared by an explicit user action (see
|
|
51
|
+
* {@link withoutAcceptanceAnomalies}), which is what keeps the bound: one cycle
|
|
52
|
+
* spends at most this many judge rounds, and clearing them is a human decision.
|
|
47
53
|
*/
|
|
48
54
|
export const MAX_EXCEPTION_ATTEMPTS = 2
|
|
49
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Retries of an INVALID acceptance: a quality veto whose required evidence is
|
|
58
|
+
* missing, unlocatable, or aimed at the empty-work baseline is not a judgement
|
|
59
|
+
* of the work, so it is recorded in its own stage and spent against this
|
|
60
|
+
* bound instead of the quality budget. Reaching it stops the judge calls and
|
|
61
|
+
* HOLDS the cycle for a human, exactly like a spent anomaly budget: an
|
|
62
|
+
* evidence-free veto must never fail a card, and it must never re-run forever.
|
|
63
|
+
*/
|
|
64
|
+
export const MAX_INVALID_ATTEMPTS = 2
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Capacity bound of the per-attempt acceptance detail — the repair/review
|
|
68
|
+
* material the acceptance mechanism itself creates. It is deliberately the
|
|
69
|
+
* ONLY large acceptance-only material this mechanism persists: findings are
|
|
70
|
+
* capped in count and rendered size, so nothing here needs an extra full
|
|
71
|
+
* evidence copy. It is removed by the automatic cleanup once the execution
|
|
72
|
+
* passed and settled (see {@link withAcceptanceDetailCleared}).
|
|
73
|
+
*/
|
|
74
|
+
export const VERIFICATION_DETAIL_MAX_FINDINGS = 12
|
|
75
|
+
/** Longest verbatim citation kept from one judge finding. */
|
|
76
|
+
export const VERIFICATION_DETAIL_MAX_QUOTE_CHARS = 600
|
|
77
|
+
/** Longest rendered detail kept for one attempt, across every finding. */
|
|
78
|
+
export const VERIFICATION_DETAIL_MAX_CHARS = 16_000
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Shortest citation a veto may rest on. A one- or two-character quote matches
|
|
82
|
+
* almost any trace, so it cannot locate anything.
|
|
83
|
+
*/
|
|
84
|
+
export const VERIFICATION_MIN_QUOTE_CHARS = 12
|
|
85
|
+
/** Shortest requirement/observation/gap text that states a real difference. */
|
|
86
|
+
export const VERIFICATION_MIN_PROBLEM_CHARS = 4
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Longest criterion identifier one persisted finding may name. An identifier,
|
|
90
|
+
* not prose: the reader rejects anything longer instead of clipping it, because
|
|
91
|
+
* a mangled id would silently detach the finding from the criterion it judges.
|
|
92
|
+
*/
|
|
93
|
+
export const VERIFICATION_DETAIL_MAX_CRITERION_ID_CHARS = 64
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Longest requirement / observation / gap / suggested-action text one finding
|
|
97
|
+
* keeps. Shared by the WRITER (which clips the judge's attributes to it) and the
|
|
98
|
+
* READER (which clips a hand-edited document back to it), so the acceptance
|
|
99
|
+
* detail has one capacity bound rather than two that can drift.
|
|
100
|
+
*/
|
|
101
|
+
export const VERIFICATION_DETAIL_MAX_PROBLEM_CHARS = 400
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Attempts one execution's acceptance cleanup may spend. Cleanup is started
|
|
105
|
+
* only after the pass record and the settlement are durable, is idempotent,
|
|
106
|
+
* and a failure never revokes the pass; the bound keeps a permanently failing
|
|
107
|
+
* filesystem from retrying forever, and the Host retries the remainder on its
|
|
108
|
+
* next start.
|
|
109
|
+
*/
|
|
110
|
+
export const MAX_CLEANUP_ATTEMPTS = 3
|
|
111
|
+
|
|
50
112
|
/** The fixed baseline a session acceptance measures itself against. */
|
|
51
113
|
export const EMPTY_WORK_BASELINE = '(No useful work or verification was performed.)'
|
|
52
114
|
|
|
@@ -105,6 +167,21 @@ export const SCALE_DESCRIPTION = [
|
|
|
105
167
|
' T = clearly and completely failed (worst)',
|
|
106
168
|
].join('\n')
|
|
107
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Which trajectory slot holds the WORK during one round.
|
|
172
|
+
*
|
|
173
|
+
* The even round (0) puts the work in slot A and the empty-work baseline in
|
|
174
|
+
* slot B; the odd round (1) swaps them, which is how the acceptance cancels
|
|
175
|
+
* position bias. The judge prompt ALWAYS introduces the work as
|
|
176
|
+
* `location="TRAJECTORY_B"`, because that is the slot the work occupies in the
|
|
177
|
+
* prompt's own framing; the parser maps the cited slot back through this table.
|
|
178
|
+
* @param round - the round index (0-based).
|
|
179
|
+
* @returns the slot letter the work occupies in that round.
|
|
180
|
+
*/
|
|
181
|
+
export function workSlotOfRound(round: number): 'A' | 'B' {
|
|
182
|
+
return round % 2 === 1 ? 'B' : 'A'
|
|
183
|
+
}
|
|
184
|
+
|
|
108
185
|
/** A-T letters in scale order (A = best). */
|
|
109
186
|
export const LETTERS: readonly string[] = Array.from({ length: GRANULARITY }, (_, index) => String.fromCharCode(65 + index))
|
|
110
187
|
|
|
@@ -201,8 +278,15 @@ export function buildAcceptancePrompt(
|
|
|
201
278
|
criterion: VerificationCriterion,
|
|
202
279
|
groundTruthNote: string = DEFAULT_GROUND_TRUTH_NOTE,
|
|
203
280
|
context?: string,
|
|
281
|
+
/**
|
|
282
|
+
* The trajectory slot that holds the WORK this round. The judge is told the
|
|
283
|
+
* slot explicitly, so the citation it returns names the work whichever way
|
|
284
|
+
* the A/B slots were arranged. Defaults to A (round 0's arrangement).
|
|
285
|
+
*/
|
|
286
|
+
workSlot: 'A' | 'B' = 'A',
|
|
204
287
|
): string {
|
|
205
288
|
const reference = context?.trim() ?? ''
|
|
289
|
+
const workSlotTag = workSlot === 'A' ? 'Trajectory A' : 'Trajectory B'
|
|
206
290
|
const token = reference === ''
|
|
207
291
|
? evidenceNonce(problem, traceA, traceB)
|
|
208
292
|
: evidenceNonce(problem, reference, traceA, traceB)
|
|
@@ -217,6 +301,10 @@ export function buildAcceptancePrompt(
|
|
|
217
301
|
'**Rating Scale:**\n' + SCALE_DESCRIPTION,
|
|
218
302
|
'**Evaluation Guideline — ' + criterion.name + ':**\n' + criterion.description,
|
|
219
303
|
'Score each trajectory ONLY on this specific criterion ("' + criterion.name + '"). Ignore other aspects that are not relevant to it.',
|
|
304
|
+
'If and only if the score you give the WORK under this criterion makes it fail the criterion, you MUST justify it with one finding element per failing observation, in exactly this form (replace the placeholders, keep the attribute names and the quoting):',
|
|
305
|
+
'<finding criterion="' + criterion.name + '" location="' + workSlot + '" requirement="the task requirement this criterion demands" observation="what you actually observed" gap="why the observation does not satisfy the requirement" quote="a verbatim passage copied exactly from the block named by location">optional suggested action</finding>',
|
|
306
|
+
'Before citing the task you MUST name the failing criterion, restate the requirement it imposes, describe the observation that falls short, explain the difference, and cite a verbatim passage that occurs in the evidence. ' + workSlotTag + ' is the WORK under review and is the only trajectory a veto may cite: a passage quoted from the other trajectory, which is the empty-work baseline ' + EMPTY_WORK_BASELINE + ', is not evidence about the work and must never be used.',
|
|
307
|
+
'A failure with no such citation is not a usable verdict: if the evidence you were given is truncated, incomplete, or simply does not contain the output needed to judge this criterion, say so in your prose and cite nothing rather than inventing a basis to veto.',
|
|
220
308
|
'Reason it through first, then END your reply with exactly these two lines and nothing after them. Replace each placeholder with a single letter A-T, keeping the spaces around the letter exactly as shown:\n<score_A> LETTER_A_TO_T </score_A>\n<score_B> LETTER_A_TO_T </score_B>',
|
|
221
309
|
'Begin your analysis now.',
|
|
222
310
|
].join('\n\n')
|
|
@@ -329,13 +417,72 @@ export interface VerificationEvidenceSummary {
|
|
|
329
417
|
workspaceFiles?: number
|
|
330
418
|
}
|
|
331
419
|
|
|
332
|
-
/**
|
|
420
|
+
/**
|
|
421
|
+
* One judge finding, structured against the requirement it vetoes.
|
|
422
|
+
*
|
|
423
|
+
* The acceptance only counts a criterion as vetoed when the judge names that
|
|
424
|
+
* criterion, states the requirement, the observation and the difference, and
|
|
425
|
+
* cites a passage that really occurs in THIS acceptance's evidence (the task
|
|
426
|
+
* text, the candidate's own trajectory, or the host's workspace record). A
|
|
427
|
+
* finding that names the empty-work baseline, another criterion, or a passage
|
|
428
|
+
* that is not in the reviewed evidence is dropped here, so it can never become
|
|
429
|
+
* the evidence for a veto.
|
|
430
|
+
*/
|
|
431
|
+
export interface VerificationCriterionFinding {
|
|
432
|
+
/** Criterion id this finding vetoes. */
|
|
433
|
+
criterionId: string
|
|
434
|
+
/** The requirement the criterion demands of the work. */
|
|
435
|
+
requirement: string
|
|
436
|
+
/** What was actually observed in the evidence. */
|
|
437
|
+
observation: string
|
|
438
|
+
/** The difference: why that observation does not satisfy the requirement. */
|
|
439
|
+
gap: string
|
|
440
|
+
/** Verbatim citation from the evidence; verified to occur in it. */
|
|
441
|
+
quote: string
|
|
442
|
+
/**
|
|
443
|
+
* Which part of the reviewed evidence the citation was located in.
|
|
444
|
+
* `baseline` means the judge cited the empty-work baseline, which is never
|
|
445
|
+
* the work's own evidence and can never veto a criterion.
|
|
446
|
+
*/
|
|
447
|
+
location: 'task' | 'trajectory' | 'workspace' | 'baseline' | 'unknown'
|
|
448
|
+
/** Optional suggested action from the judge. */
|
|
449
|
+
action?: string
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** Why an acceptance produced no valid quality verdict. */
|
|
453
|
+
export type VerificationInvalidReason =
|
|
454
|
+
/** The failing criteria have no structured finding of their own. */
|
|
455
|
+
| 'missing-finding'
|
|
456
|
+
/** A finding's citation does not occur in the evidence the judge saw. */
|
|
457
|
+
| 'unlocatable-quote'
|
|
458
|
+
/** A finding vetoes the empty-work baseline instead of the work. */
|
|
459
|
+
| 'baseline-finding'
|
|
460
|
+
/** A finding's text is empty or a generic evaluation. */
|
|
461
|
+
| 'vacuous-finding'
|
|
462
|
+
/** The judged evidence was truncated and cannot substantiate the veto. */
|
|
463
|
+
| 'insufficient-evidence'
|
|
464
|
+
|
|
465
|
+
/** One recorded acceptance attempt: a quality verdict, an anomaly, a budget stop, or an invalid veto. */
|
|
333
466
|
export interface VerificationAttempt {
|
|
334
467
|
/** 1-based ordinal within its own stage. */
|
|
335
468
|
index: number
|
|
336
469
|
/** When the attempt ran (ms epoch). */
|
|
337
470
|
at: number
|
|
338
|
-
|
|
471
|
+
/**
|
|
472
|
+
* `budget` records an acceptance the time budget ended before it could
|
|
473
|
+
* reach a verdict. It is neither a quality verdict nor an environment
|
|
474
|
+
* anomaly: the judge route may be perfectly healthy, the machine simply had
|
|
475
|
+
* no time left, so it is charged to no budget at all and cleared by the same
|
|
476
|
+
* explicit user action that clears anomalies.
|
|
477
|
+
*
|
|
478
|
+
* `invalid` records an acceptance that produced no usable quality verdict
|
|
479
|
+
* because the veto's required evidence was missing, unlocatable, or aimed at
|
|
480
|
+
* the empty-work baseline. It is neither a judgement of the work nor an
|
|
481
|
+
* environment anomaly: it consumes its own bounded retry budget, fails
|
|
482
|
+
* nothing, and is cleared by the same explicit user action that clears
|
|
483
|
+
* anomalies.
|
|
484
|
+
*/
|
|
485
|
+
stage: 'quality' | 'exception' | 'budget' | 'invalid'
|
|
339
486
|
/** Quality: whether the work passed. A recorded anomaly is never a pass. */
|
|
340
487
|
passed: boolean
|
|
341
488
|
/** Total score of the task's own work (0 for an anomaly). */
|
|
@@ -353,6 +500,14 @@ export interface VerificationAttempt {
|
|
|
353
500
|
rounds: number
|
|
354
501
|
/** Anomaly reason; absent on a quality verdict. */
|
|
355
502
|
error?: string
|
|
503
|
+
/**
|
|
504
|
+
* Structured, locatable findings of a QUALITY attempt, keyed by criterion.
|
|
505
|
+
* A veto is only counted when the failing criterion appears here with a
|
|
506
|
+
* verified citation; otherwise the attempt is recorded as invalid instead.
|
|
507
|
+
*/
|
|
508
|
+
criterionFindings?: VerificationCriterionFinding[]
|
|
509
|
+
/** Why an invalid attempt carried no usable verdict; absent on other stages. */
|
|
510
|
+
invalidReason?: VerificationInvalidReason
|
|
356
511
|
}
|
|
357
512
|
|
|
358
513
|
/** Persisted acceptance state of one execution. */
|
|
@@ -367,16 +522,84 @@ export interface ExecutionVerification {
|
|
|
367
522
|
* out of the gate (`TaskRecord.skipVerification`), `goal-unavailable` when
|
|
368
523
|
* the run never became a goal run (so no completion gate could ever fire),
|
|
369
524
|
* and `team-member` for a teammate execution (the Lead's acceptance covers
|
|
370
|
-
* the team's aggregated evidence)
|
|
525
|
+
* the team's aggregated evidence), and `goal-disabled` when the GLOBAL
|
|
526
|
+
* native /goal switch was off at start, so this execution never became a
|
|
527
|
+
* goal run and its completion gate can never fire.
|
|
371
528
|
*/
|
|
372
|
-
applicability: 'enforced' | 'disabled' | 'skipped' | 'goal-unavailable' | 'team-member'
|
|
529
|
+
applicability: 'enforced' | 'disabled' | 'skipped' | 'goal-unavailable' | 'team-member' | 'goal-disabled'
|
|
373
530
|
/** Set when the cycle is spent and the execution must fail. */
|
|
374
531
|
failedReason?: string
|
|
375
532
|
failedAt?: number
|
|
533
|
+
/**
|
|
534
|
+
* Retention record of the acceptance-only detail this execution created.
|
|
535
|
+
*
|
|
536
|
+
* An execution that PASSED and settled has its bulky acceptance-only material
|
|
537
|
+
* (attempt findings, invalid diagnostics, judge metadata beyond the audit
|
|
538
|
+
* summary) removed automatically; this record is the lightweight audit trail
|
|
539
|
+
* that proves the cleanup happened and preserves the minimal credential
|
|
540
|
+
* (verdict, scores, time, model, evidence hash) the completion gate needs.
|
|
541
|
+
* Absent means nothing has been cleaned for this execution.
|
|
542
|
+
*/
|
|
543
|
+
cleanup?: AcceptanceCleanupRecord
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** Permanent, lightweight acceptance credential kept after a passed execution settles. */
|
|
547
|
+
export interface AcceptanceAuditRecord {
|
|
548
|
+
/** Stage of the attempt the credential describes. */
|
|
549
|
+
stage: VerificationAttempt['stage']
|
|
550
|
+
/** Whether the attempt passed. */
|
|
551
|
+
passed: boolean
|
|
552
|
+
/** When the attempt ran (ms epoch). */
|
|
553
|
+
at: number
|
|
554
|
+
/** Total score of the task's own work. */
|
|
555
|
+
score: number
|
|
556
|
+
/** Empty-work baseline score. */
|
|
557
|
+
baseline: number
|
|
558
|
+
/** Per-criterion scores and thresholds. */
|
|
559
|
+
criteria: VerificationCriterionScore[]
|
|
560
|
+
/** Digest of the judged evidence; binds the pass record to what was reviewed. */
|
|
561
|
+
evidenceHash: string
|
|
562
|
+
/** Judge route of the attempt. */
|
|
563
|
+
route: VerificationRoute
|
|
564
|
+
/** Anomaly or invalid reason, when the attempt carried one. */
|
|
565
|
+
error?: string
|
|
566
|
+
/** One-line problem summary kept for the audit trail. */
|
|
567
|
+
findingSummary?: string
|
|
568
|
+
/** Token accounting of the attempt. */
|
|
569
|
+
usage: VerificationUsage
|
|
376
570
|
}
|
|
377
571
|
|
|
378
|
-
/**
|
|
379
|
-
|
|
572
|
+
/**
|
|
573
|
+
* State of the automatic acceptance-detail cleanup of one execution.
|
|
574
|
+
*
|
|
575
|
+
* `state` is `pending` until the cleanup actually removed the detail (or found
|
|
576
|
+
* nothing left to remove); `failed` records the last failure so a Host restart
|
|
577
|
+
* can retry it. The pass record and the settlement are durable before a cleanup
|
|
578
|
+
* starts, so a failure here never revokes a valid pass.
|
|
579
|
+
*/
|
|
580
|
+
export interface AcceptanceCleanupRecord {
|
|
581
|
+
state: 'pending' | 'cleaned' | 'failed'
|
|
582
|
+
/** Attempts spent; bounded by {@link MAX_CLEANUP_ATTEMPTS}. */
|
|
583
|
+
attempts: number
|
|
584
|
+
/** When the cleanup first ran (ms epoch). */
|
|
585
|
+
startedAt: number
|
|
586
|
+
/** When the detail was actually removed (ms epoch); absent while pending. */
|
|
587
|
+
cleanedAt?: number
|
|
588
|
+
/** Why the last attempt failed; absent when none did. */
|
|
589
|
+
lastError?: string
|
|
590
|
+
/** Minimal credential kept for every attempt this execution recorded. */
|
|
591
|
+
audit: AcceptanceAuditRecord[]
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* UI/board phase of one execution's acceptance.
|
|
596
|
+
*
|
|
597
|
+
* `invalid` is its own phase, not a variant of `failed`: the cycle spent its
|
|
598
|
+
* invalid-acceptance budget without a usable veto, so the judge is no longer
|
|
599
|
+
* called and a human decision is required. A reader must never read it as a
|
|
600
|
+
* quality rejection.
|
|
601
|
+
*/
|
|
602
|
+
export type VerificationPhase = 'off' | 'executing' | 'verifying' | 'repairing' | 'passed' | 'failed' | 'invalid'
|
|
380
603
|
|
|
381
604
|
/** Quality attempts spent by this cycle. */
|
|
382
605
|
export function qualityAttempts(verification: ExecutionVerification | undefined): VerificationAttempt[] {
|
|
@@ -388,6 +611,133 @@ export function exceptionAttempts(verification: ExecutionVerification | undefine
|
|
|
388
611
|
return verification === undefined ? [] : verification.attempts.filter(attempt => attempt.stage === 'exception')
|
|
389
612
|
}
|
|
390
613
|
|
|
614
|
+
/**
|
|
615
|
+
* Attempts the time budget ended before a verdict, charged to no budget. They
|
|
616
|
+
* are kept so the report can say WHY an acceptance produced nothing, and they
|
|
617
|
+
* are cleared by the same explicit user action that clears anomalies.
|
|
618
|
+
*/
|
|
619
|
+
export function budgetAttempts(verification: ExecutionVerification | undefined): VerificationAttempt[] {
|
|
620
|
+
return verification === undefined ? [] : verification.attempts.filter(attempt => attempt.stage === 'budget')
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Invalid acceptances spent by this cycle: a veto whose required evidence was
|
|
625
|
+
* missing, unlocatable, or aimed at the empty-work baseline. They are NOT
|
|
626
|
+
* quality verdicts, so they never close the cycle and never fail the card; the
|
|
627
|
+
* bound is what keeps an evidence-free judge from re-running forever.
|
|
628
|
+
*/
|
|
629
|
+
export function invalidAttempts(verification: ExecutionVerification | undefined): VerificationAttempt[] {
|
|
630
|
+
return verification === undefined ? [] : verification.attempts.filter(attempt => attempt.stage === 'invalid')
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/** Whether this cycle still has invalid-acceptance budget left. */
|
|
634
|
+
export function hasInvalidBudget(verification: ExecutionVerification | undefined): boolean {
|
|
635
|
+
return invalidAttempts(verification).length < MAX_INVALID_ATTEMPTS
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* The acceptance block with every non-quality attempt dropped: the explicit
|
|
640
|
+
* reset a user performs after fixing the environment (issue #1828).
|
|
641
|
+
*
|
|
642
|
+
* Only attempts that are not a quality verdict are dropped. A quality verdict
|
|
643
|
+
* is the board's own judgement of the work and survives every reset, so this
|
|
644
|
+
* can never buy an extra acceptance or re-open a judged failure; it only lets
|
|
645
|
+
* the environment be judged again.
|
|
646
|
+
* @param verification - the persisted acceptance state.
|
|
647
|
+
* @returns the cleared block, or undefined when there is nothing to clear.
|
|
648
|
+
*/
|
|
649
|
+
export function withoutAcceptanceAnomalies(verification: ExecutionVerification): ExecutionVerification | undefined {
|
|
650
|
+
const kept = verification.attempts.filter(attempt => attempt.stage === 'quality')
|
|
651
|
+
if (kept.length === verification.attempts.length) return undefined
|
|
652
|
+
const cleared: ExecutionVerification = { ...verification, attempts: kept }
|
|
653
|
+
delete cleared.inFlight
|
|
654
|
+
delete cleared.failedReason
|
|
655
|
+
delete cleared.failedAt
|
|
656
|
+
delete cleared.cleanup
|
|
657
|
+
return cleared
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* The one-line problem summary a cleaned execution keeps: the failing criteria
|
|
662
|
+
* with their scores, plus how many locatable findings were removed. This is the
|
|
663
|
+
* lightweight audit material that survives the detail cleanup.
|
|
664
|
+
* @param attempt - the attempt whose detail is being removed.
|
|
665
|
+
* @returns the bounded summary line.
|
|
666
|
+
*/
|
|
667
|
+
export function findingSummaryOf(attempt: VerificationAttempt): string | undefined {
|
|
668
|
+
const failed = attempt.criteria.filter(criterion => !criterion.passed)
|
|
669
|
+
const parts: string[] = []
|
|
670
|
+
if (failed.length > 0) {
|
|
671
|
+
parts.push('未达标判据:' + failed.map(criterion => criterion.name + ' ' + percent(criterion.score)).join('、'))
|
|
672
|
+
}
|
|
673
|
+
const located = (attempt.criterionFindings ?? []).length
|
|
674
|
+
if (located > 0) parts.push('已移除的可定位问题 ' + located + ' 条')
|
|
675
|
+
if (attempt.error !== undefined && attempt.error !== '') parts.push(attempt.error)
|
|
676
|
+
if (parts.length === 0) return undefined
|
|
677
|
+
return parts.join(';')
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* The audit credential of one attempt: everything the completion gate, the
|
|
682
|
+
* report and a later review need, and nothing that makes the ledger heavy.
|
|
683
|
+
* @param attempt - the attempt being condensed.
|
|
684
|
+
* @returns the lightweight audit record.
|
|
685
|
+
*/
|
|
686
|
+
export function auditOf(attempt: VerificationAttempt): AcceptanceAuditRecord {
|
|
687
|
+
const summary = findingSummaryOf(attempt)
|
|
688
|
+
return {
|
|
689
|
+
stage: attempt.stage,
|
|
690
|
+
passed: attempt.passed,
|
|
691
|
+
at: attempt.at,
|
|
692
|
+
score: attempt.score,
|
|
693
|
+
baseline: attempt.baseline,
|
|
694
|
+
criteria: attempt.criteria.map(criterion => ({ ...criterion })),
|
|
695
|
+
evidenceHash: attempt.evidence.hash,
|
|
696
|
+
route: { ...attempt.route },
|
|
697
|
+
...(attempt.error === undefined ? {} : { error: attempt.error }),
|
|
698
|
+
...(summary === undefined ? {} : { findingSummary: summary }),
|
|
699
|
+
usage: { ...attempt.usage },
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Remove this execution's OWN acceptance-only detail while keeping the light
|
|
705
|
+
* audit record — the acceptance mechanism's side of requirement 2.
|
|
706
|
+
*
|
|
707
|
+
* Only material the acceptance mechanism itself created is removed: the
|
|
708
|
+
* per-attempt findings, the invalid diagnostics and the token/evidence detail
|
|
709
|
+
* beyond the summary. Everything a human needs to see the verdict survives
|
|
710
|
+
* (stage, pass flag, time, scores, evidence hash, judge route, usage totals),
|
|
711
|
+
* so the completion gate's pass record and a later review are unaffected, and
|
|
712
|
+
* NO user file, task artifact, raw session history or other execution's record
|
|
713
|
+
* is ever touched by this function.
|
|
714
|
+
* @param verification - the execution's acceptance block.
|
|
715
|
+
* @param now - cleanup instant (ms epoch).
|
|
716
|
+
* @returns the cleaned block, or undefined when there is nothing to remove.
|
|
717
|
+
*/
|
|
718
|
+
export function withAcceptanceDetailCleared(verification: ExecutionVerification, now: number): ExecutionVerification | undefined {
|
|
719
|
+
if (verification.cleanup?.state === 'cleaned') return undefined
|
|
720
|
+
const audit = verification.attempts.map(auditOf)
|
|
721
|
+
const cleaned: ExecutionVerification = {
|
|
722
|
+
...verification,
|
|
723
|
+
// The findings are the acceptance's own large material; the verdict, the
|
|
724
|
+
// scores and the audit credential stay.
|
|
725
|
+
attempts: verification.attempts.map(attempt => ({
|
|
726
|
+
...attempt,
|
|
727
|
+
findings: [],
|
|
728
|
+
criterionFindings: [],
|
|
729
|
+
})),
|
|
730
|
+
cleanup: {
|
|
731
|
+
state: 'cleaned',
|
|
732
|
+
attempts: (verification.cleanup?.attempts ?? 0) + 1,
|
|
733
|
+
startedAt: verification.cleanup?.startedAt ?? now,
|
|
734
|
+
cleanedAt: now,
|
|
735
|
+
audit,
|
|
736
|
+
},
|
|
737
|
+
}
|
|
738
|
+
return cleaned
|
|
739
|
+
}
|
|
740
|
+
|
|
391
741
|
/** The quality attempt that passed, when one did. */
|
|
392
742
|
export function passedAttempt(verification: ExecutionVerification | undefined): VerificationAttempt | undefined {
|
|
393
743
|
return qualityAttempts(verification).find(attempt => attempt.passed)
|
|
@@ -406,6 +756,43 @@ export function verificationRequired(verification: ExecutionVerification | undef
|
|
|
406
756
|
return verification !== undefined && verification.applicability === 'enforced'
|
|
407
757
|
}
|
|
408
758
|
|
|
759
|
+
/**
|
|
760
|
+
* Whether this execution's acceptance gate was never opened: acceptance was
|
|
761
|
+
* enforced, the judge recorded nothing at all, and no cycle ever closed. Zero
|
|
762
|
+
* attempts is therefore a fact about the RUN, not a verdict about the work — a
|
|
763
|
+
* session that narrated completion instead of calling
|
|
764
|
+
* `update_goal(action: complete)` produces exactly this shape, and its failure
|
|
765
|
+
* reason must stay distinguishable from a quality verdict (issue #1837).
|
|
766
|
+
* @param verification - the execution's persisted acceptance state.
|
|
767
|
+
* @returns true when no acceptance attempt ever ran.
|
|
768
|
+
*/
|
|
769
|
+
export function verificationNeverInvoked(verification: ExecutionVerification | undefined): boolean {
|
|
770
|
+
return verification !== undefined
|
|
771
|
+
&& verification.applicability === 'enforced'
|
|
772
|
+
&& verification.attempts.length === 0
|
|
773
|
+
&& verification.failedReason === undefined
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Whether this execution's acceptance produced no usable veto at all: it was
|
|
778
|
+
* enforced, the judge recorded attempts, and every one of them is INVALID.
|
|
779
|
+
*
|
|
780
|
+
* Such a run must never be described as an evidence-backed quality veto (issue
|
|
781
|
+
* #1837's sibling): the judge could not locate its own rejection, so the run
|
|
782
|
+
* ended for lack of a matching pass rather than because the work was judged
|
|
783
|
+
* and rejected. The settlement reason names that difference.
|
|
784
|
+
* @param verification - the execution's persisted acceptance state.
|
|
785
|
+
* @returns true when nothing but invalid acceptances were recorded.
|
|
786
|
+
*/
|
|
787
|
+
export function verificationOnlyInvalid(verification: ExecutionVerification | undefined): boolean {
|
|
788
|
+
return verification !== undefined
|
|
789
|
+
&& verification.applicability === 'enforced'
|
|
790
|
+
&& verification.failedReason === undefined
|
|
791
|
+
&& passedAttempt(verification) === undefined
|
|
792
|
+
&& verification.attempts.length > 0
|
|
793
|
+
&& invalidAttempts(verification).length === verification.attempts.length
|
|
794
|
+
}
|
|
795
|
+
|
|
409
796
|
/** Whether this cycle still has quality budget left. */
|
|
410
797
|
export function hasQualityBudget(verification: ExecutionVerification | undefined): boolean {
|
|
411
798
|
return qualityAttempts(verification).length < MAX_QUALITY_ATTEMPTS
|
|
@@ -424,10 +811,136 @@ export function verificationPhase(verification: ExecutionVerification | undefine
|
|
|
424
811
|
if (passedAttempt(verification) !== undefined) return 'passed'
|
|
425
812
|
if (verification.inFlight === true) return 'verifying'
|
|
426
813
|
const quality = qualityAttempts(verification)
|
|
427
|
-
if (quality.length === 0)
|
|
814
|
+
if (quality.length === 0) {
|
|
815
|
+
// A cycle whose only recorded attempts are invalid is HELD, not failed: the
|
|
816
|
+
// judge produced no usable veto, so no quality verdict exists and a human
|
|
817
|
+
// must look at it instead of the board failing the card.
|
|
818
|
+
return hasInvalidBudget(verification) ? 'executing' : 'invalid'
|
|
819
|
+
}
|
|
428
820
|
return hasQualityBudget(verification) ? 'repairing' : 'failed'
|
|
429
821
|
}
|
|
430
822
|
|
|
823
|
+
/** The text of each evidence part one acceptance handed to the judge. */
|
|
824
|
+
export interface AcceptanceEvidenceText {
|
|
825
|
+
/** The task statement block. */
|
|
826
|
+
task: string
|
|
827
|
+
/** The candidate's own trajectory block (the WORK). */
|
|
828
|
+
trajectory: string
|
|
829
|
+
/** The host's rendered workspace-change block; empty when none was served. */
|
|
830
|
+
workspace: string
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/** The input the program-level validity check reads from one finished attempt. */
|
|
834
|
+
export interface AcceptanceValidityInput {
|
|
835
|
+
/** Per-criterion scores the attempt recorded. */
|
|
836
|
+
criteria: readonly VerificationCriterionScore[]
|
|
837
|
+
/** Structured findings the judge's answer produced. */
|
|
838
|
+
findings: readonly VerificationCriterionFinding[]
|
|
839
|
+
/** The exact evidence texts the judge was shown. */
|
|
840
|
+
evidence: AcceptanceEvidenceText
|
|
841
|
+
/** Characters the evidence cap dropped from the trajectory. */
|
|
842
|
+
omittedCharacters: number
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/** The verdict of the program-level validity check. */
|
|
846
|
+
export type AcceptanceValidity =
|
|
847
|
+
| { valid: true }
|
|
848
|
+
| { valid: false; reason: VerificationInvalidReason; detail: string }
|
|
849
|
+
|
|
850
|
+
/** Whether a text carries a real statement rather than filler. */
|
|
851
|
+
function substantive(text: string): boolean {
|
|
852
|
+
return text.trim().length >= VERIFICATION_MIN_PROBLEM_CHARS
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Program-level validity of a REJECTING acceptance, decided before any quality
|
|
857
|
+
* verdict is booked (requirement 1: the rule cannot live in a prompt alone).
|
|
858
|
+
*
|
|
859
|
+
* A veto is only usable when every criterion that failed has its own structured
|
|
860
|
+
* finding carrying the requirement, the observation, the difference and a
|
|
861
|
+
* citation that really occurs in the evidence the judge saw. A citation aimed at
|
|
862
|
+
* the empty-work baseline, a citation that is not in the reviewed evidence, an
|
|
863
|
+
* empty or generic statement, and a truncated review that cannot substantiate
|
|
864
|
+
* the veto are all invalid acceptances: no quality verdict, no quality budget,
|
|
865
|
+
* and no card failure.
|
|
866
|
+
* @param input - the attempt's scores, findings and the evidence it judged.
|
|
867
|
+
* @returns valid, or the reason and detail of the invalidity.
|
|
868
|
+
*/
|
|
869
|
+
export function assessAcceptanceValidity(input: AcceptanceValidityInput): AcceptanceValidity {
|
|
870
|
+
const failed = input.criteria.filter(criterion => !criterion.passed)
|
|
871
|
+
const truncated = input.omittedCharacters > 0
|
|
872
|
+
if (failed.length === 0) {
|
|
873
|
+
// Every criterion passed yet the total rule still rejected the work (it did
|
|
874
|
+
// not beat the baseline). That is still a veto and still owes locatable
|
|
875
|
+
// evidence, but there is no specific criterion to attach it to.
|
|
876
|
+
const locating = input.findings.filter(finding => validFinding(finding, input.evidence) === undefined)
|
|
877
|
+
if (locating.length > 0) return { valid: true }
|
|
878
|
+
return truncated
|
|
879
|
+
? { valid: false, reason: 'insufficient-evidence', detail: 'the review was truncated and no verifiable citation supports the total-score veto' }
|
|
880
|
+
: { valid: false, reason: 'missing-finding', detail: 'the total score was rejected without any locatable finding' }
|
|
881
|
+
}
|
|
882
|
+
for (const criterion of failed) {
|
|
883
|
+
const own = input.findings.filter(finding => finding.criterionId === criterion.id)
|
|
884
|
+
if (own.length === 0) {
|
|
885
|
+
return truncated
|
|
886
|
+
? {
|
|
887
|
+
valid: false,
|
|
888
|
+
reason: 'insufficient-evidence',
|
|
889
|
+
detail: criterion.name + ': the evidence was truncated and the judge reported no citation of its own, so this criterion cannot be judged',
|
|
890
|
+
}
|
|
891
|
+
: {
|
|
892
|
+
valid: false,
|
|
893
|
+
reason: 'missing-finding',
|
|
894
|
+
detail: criterion.name + ': the criterion failed with no structured finding of its own',
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
let usable = false
|
|
898
|
+
let lastReason: { reason: VerificationInvalidReason; detail: string } | undefined
|
|
899
|
+
for (const finding of own) {
|
|
900
|
+
const problem = validFinding(finding, input.evidence)
|
|
901
|
+
if (problem === undefined) {
|
|
902
|
+
usable = true
|
|
903
|
+
break
|
|
904
|
+
}
|
|
905
|
+
lastReason = problem
|
|
906
|
+
}
|
|
907
|
+
if (!usable) {
|
|
908
|
+
const detail = (lastReason?.detail ?? criterion.name) + ' (' + criterion.name + ')'
|
|
909
|
+
return { valid: false, reason: lastReason?.reason ?? 'vacuous-finding', detail }
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return { valid: true }
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Why one finding cannot support a veto, or undefined when it can.
|
|
917
|
+
*
|
|
918
|
+
* The citation is checked against the evidence text of the LOCATION the finding
|
|
919
|
+
* names, so a quote invented by the judge, a quote taken from an A/B slot that
|
|
920
|
+
* held the empty-work baseline, and a quote the reviewed window never contained
|
|
921
|
+
* are all rejected.
|
|
922
|
+
*/
|
|
923
|
+
function validFinding(finding: VerificationCriterionFinding, evidence: AcceptanceEvidenceText): { reason: VerificationInvalidReason; detail: string } | undefined {
|
|
924
|
+
if (!substantive(finding.requirement) || !substantive(finding.observation) || !substantive(finding.gap)) {
|
|
925
|
+
return { reason: 'vacuous-finding', detail: 'the finding states no requirement, observation or difference' }
|
|
926
|
+
}
|
|
927
|
+
if (finding.location === 'baseline') {
|
|
928
|
+
return { reason: 'baseline-finding', detail: 'the citation is taken from the empty-work baseline, which is not the work under review' }
|
|
929
|
+
}
|
|
930
|
+
const quote = finding.quote.trim()
|
|
931
|
+
if (quote.length < VERIFICATION_MIN_QUOTE_CHARS) {
|
|
932
|
+
return { reason: 'unlocatable-quote', detail: 'the citation is too short to locate anything in the evidence' }
|
|
933
|
+
}
|
|
934
|
+
const haystack = finding.location === 'task' ? evidence.task
|
|
935
|
+
: finding.location === 'workspace' ? evidence.workspace
|
|
936
|
+
: finding.location === 'trajectory' ? evidence.trajectory
|
|
937
|
+
: ''
|
|
938
|
+
if (haystack === '' || !haystack.includes(quote)) {
|
|
939
|
+
return { reason: 'unlocatable-quote', detail: 'the citation does not occur in the evidence the judge reviewed' }
|
|
940
|
+
}
|
|
941
|
+
return undefined
|
|
942
|
+
}
|
|
943
|
+
|
|
431
944
|
/**
|
|
432
945
|
* The acceptance rule, mirrored from the verifier's `sessionAccepted`:
|
|
433
946
|
* the work must BEAT the empty-work baseline, reach the total threshold, and
|
|
@@ -455,7 +968,7 @@ function readAttempt(value: unknown): VerificationAttempt | undefined {
|
|
|
455
968
|
if (typeof value !== 'object' || value === null) return undefined
|
|
456
969
|
const row = value as Record<string, unknown>
|
|
457
970
|
if (typeof row.index !== 'number' || typeof row.at !== 'number') return undefined
|
|
458
|
-
if (row.stage !== 'quality' && row.stage !== 'exception') return undefined
|
|
971
|
+
if (row.stage !== 'quality' && row.stage !== 'exception' && row.stage !== 'budget' && row.stage !== 'invalid') return undefined
|
|
459
972
|
if (typeof row.passed !== 'boolean') return undefined
|
|
460
973
|
if (typeof row.score !== 'number' || !Number.isFinite(row.score)) return undefined
|
|
461
974
|
if (typeof row.baseline !== 'number' || !Number.isFinite(row.baseline)) return undefined
|
|
@@ -490,7 +1003,26 @@ function readAttempt(value: unknown): VerificationAttempt | undefined {
|
|
|
490
1003
|
const routeRow = route as Record<string, unknown>
|
|
491
1004
|
if (typeof routeRow.provider !== 'string' || typeof routeRow.model !== 'string') return undefined
|
|
492
1005
|
if (routeRow.reasoningEffort !== undefined && typeof routeRow.reasoningEffort !== 'string') return undefined
|
|
493
|
-
|
|
1006
|
+
// Bounded on READ as well as write: a hand-edited document must not be able to
|
|
1007
|
+
// persist unbounded prose in the one legacy field the renderer prints.
|
|
1008
|
+
const findings = (Array.isArray(row.findings) ? row.findings : [])
|
|
1009
|
+
.filter((item): item is string => typeof item === 'string')
|
|
1010
|
+
.slice(0, VERIFICATION_DETAIL_MAX_FINDINGS)
|
|
1011
|
+
.map(item => clipRead(item, VERIFICATION_DETAIL_MAX_PROBLEM_CHARS))
|
|
1012
|
+
const criterionFindings: VerificationCriterionFinding[] = []
|
|
1013
|
+
if (Array.isArray(row.criterionFindings)) {
|
|
1014
|
+
if (row.criterionFindings.length > VERIFICATION_DETAIL_MAX_FINDINGS) return undefined
|
|
1015
|
+
for (const entry of row.criterionFindings) {
|
|
1016
|
+
const finding = readCriterionFinding(entry)
|
|
1017
|
+
if (finding === undefined) return undefined
|
|
1018
|
+
criterionFindings.push(finding)
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
const invalidReason = row.invalidReason
|
|
1022
|
+
if (invalidReason !== undefined
|
|
1023
|
+
&& invalidReason !== 'missing-finding' && invalidReason !== 'unlocatable-quote'
|
|
1024
|
+
&& invalidReason !== 'baseline-finding' && invalidReason !== 'vacuous-finding'
|
|
1025
|
+
&& invalidReason !== 'insufficient-evidence') return undefined
|
|
494
1026
|
return {
|
|
495
1027
|
index: row.index,
|
|
496
1028
|
at: row.at,
|
|
@@ -524,6 +1056,52 @@ function readAttempt(value: unknown): VerificationAttempt | undefined {
|
|
|
524
1056
|
channel: 'explicit-tag',
|
|
525
1057
|
rounds: typeof row.rounds === 'number' ? row.rounds : VERIFICATION_ROUNDS,
|
|
526
1058
|
...(typeof row.error === 'string' ? { error: row.error } : {}),
|
|
1059
|
+
...(criterionFindings.length === 0 ? {} : { criterionFindings }),
|
|
1060
|
+
...(typeof invalidReason === 'string' ? { invalidReason } : {}),
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
/**
|
|
1065
|
+
* Bound one persisted finding's text to what the acceptance itself would have
|
|
1066
|
+
* written.
|
|
1067
|
+
*
|
|
1068
|
+
* The reader is the only other door into the ledger: a hand-edited or
|
|
1069
|
+
* corrupted document could otherwise carry a megabyte of prose in a field the
|
|
1070
|
+
* writer caps at a few hundred characters, and every later render, audit
|
|
1071
|
+
* summary and cleanup pass would pay for it. Clipping on READ (never rejecting)
|
|
1072
|
+
* keeps the block usable and its capacity bounded: the acceptance-only material
|
|
1073
|
+
* the cleanup removes is bounded by construction on both sides.
|
|
1074
|
+
* @param value - the raw persisted text.
|
|
1075
|
+
* @param max - the cap the writer applies to that field.
|
|
1076
|
+
* @returns the bounded text.
|
|
1077
|
+
*/
|
|
1078
|
+
function clipRead(value: string, max: number): string {
|
|
1079
|
+
return value.length <= max ? value : value.slice(0, max)
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/**
|
|
1083
|
+
* Repair one persisted structured finding, or undefined when it is unusable.
|
|
1084
|
+
* A malformed finding drops the WHOLE acceptance block (fail closed): a
|
|
1085
|
+
* persisted veto that cannot be read back could otherwise be counted as
|
|
1086
|
+
* evidence it no longer holds.
|
|
1087
|
+
*/
|
|
1088
|
+
function readCriterionFinding(value: unknown): VerificationCriterionFinding | undefined {
|
|
1089
|
+
if (typeof value !== 'object' || value === null) return undefined
|
|
1090
|
+
const row = value as Record<string, unknown>
|
|
1091
|
+
if (typeof row.criterionId !== 'string' || row.criterionId === '') return undefined
|
|
1092
|
+
if (typeof row.requirement !== 'string' || typeof row.observation !== 'string' || typeof row.gap !== 'string') return undefined
|
|
1093
|
+
if (typeof row.quote !== 'string') return undefined
|
|
1094
|
+
if (row.criterionId.length > VERIFICATION_DETAIL_MAX_CRITERION_ID_CHARS) return undefined
|
|
1095
|
+
const location = row.location
|
|
1096
|
+
if (location !== 'task' && location !== 'trajectory' && location !== 'workspace' && location !== 'baseline' && location !== 'unknown') return undefined
|
|
1097
|
+
return {
|
|
1098
|
+
criterionId: row.criterionId,
|
|
1099
|
+
requirement: clipRead(row.requirement, VERIFICATION_DETAIL_MAX_PROBLEM_CHARS),
|
|
1100
|
+
observation: clipRead(row.observation, VERIFICATION_DETAIL_MAX_PROBLEM_CHARS),
|
|
1101
|
+
gap: clipRead(row.gap, VERIFICATION_DETAIL_MAX_PROBLEM_CHARS),
|
|
1102
|
+
quote: clipRead(row.quote, VERIFICATION_DETAIL_MAX_QUOTE_CHARS),
|
|
1103
|
+
location,
|
|
1104
|
+
...(typeof row.action === 'string' ? { action: clipRead(row.action, VERIFICATION_DETAIL_MAX_PROBLEM_CHARS) } : {}),
|
|
527
1105
|
}
|
|
528
1106
|
}
|
|
529
1107
|
|
|
@@ -569,7 +1147,8 @@ export function normalizeVerification(value: unknown): ExecutionVerification | u
|
|
|
569
1147
|
effortFallback = { requested: fallback.requested, ...(fallback.resolved === undefined ? {} : { resolved: fallback.resolved }) }
|
|
570
1148
|
}
|
|
571
1149
|
const applicability = row.applicability
|
|
572
|
-
if (applicability !== 'enforced' && applicability !== 'disabled' && applicability !== 'skipped'
|
|
1150
|
+
if (applicability !== 'enforced' && applicability !== 'disabled' && applicability !== 'skipped'
|
|
1151
|
+
&& applicability !== 'goal-unavailable' && applicability !== 'team-member' && applicability !== 'goal-disabled') return undefined
|
|
573
1152
|
const attempts: VerificationAttempt[] = []
|
|
574
1153
|
if (!Array.isArray(row.attempts)) return undefined
|
|
575
1154
|
for (const entry of row.attempts) {
|
|
@@ -578,6 +1157,8 @@ export function normalizeVerification(value: unknown): ExecutionVerification | u
|
|
|
578
1157
|
attempts.push(attempt)
|
|
579
1158
|
}
|
|
580
1159
|
if (typeof row.failedReason === 'string' && row.failedReason === '') return undefined
|
|
1160
|
+
const cleanup = readCleanupRecord(row.cleanup)
|
|
1161
|
+
if (row.cleanup !== undefined && cleanup === undefined) return undefined
|
|
581
1162
|
return {
|
|
582
1163
|
contract: {
|
|
583
1164
|
enabled: contractRow.enabled,
|
|
@@ -593,6 +1174,100 @@ export function normalizeVerification(value: unknown): ExecutionVerification | u
|
|
|
593
1174
|
applicability,
|
|
594
1175
|
...(typeof row.failedReason === 'string' ? { failedReason: row.failedReason } : {}),
|
|
595
1176
|
...(typeof row.failedAt === 'number' && Number.isFinite(row.failedAt) ? { failedAt: row.failedAt } : {}),
|
|
1177
|
+
...(cleanup === undefined ? {} : { cleanup }),
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
/**
|
|
1182
|
+
* Repair the persisted cleanup record, or undefined when it is unusable. The
|
|
1183
|
+
* record is the audit trail the UI reads to tell "the judge gave its evidence
|
|
1184
|
+
* and it was cleaned up" apart from "the judge produced no evidence at all",
|
|
1185
|
+
* so a malformed one drops the whole block (fail closed).
|
|
1186
|
+
*/
|
|
1187
|
+
function readCleanupRecord(value: unknown): AcceptanceCleanupRecord | undefined {
|
|
1188
|
+
if (typeof value !== 'object' || value === null) return undefined
|
|
1189
|
+
const row = value as Record<string, unknown>
|
|
1190
|
+
if (row.state !== 'pending' && row.state !== 'cleaned' && row.state !== 'failed') return undefined
|
|
1191
|
+
if (typeof row.attempts !== 'number' || !Number.isInteger(row.attempts) || row.attempts < 0) return undefined
|
|
1192
|
+
if (typeof row.startedAt !== 'number' || !Number.isFinite(row.startedAt)) return undefined
|
|
1193
|
+
if (row.cleanedAt !== undefined && (typeof row.cleanedAt !== 'number' || !Number.isFinite(row.cleanedAt))) return undefined
|
|
1194
|
+
if (row.lastError !== undefined && typeof row.lastError !== 'string') return undefined
|
|
1195
|
+
if (!Array.isArray(row.audit)) return undefined
|
|
1196
|
+
const audit: AcceptanceAuditRecord[] = []
|
|
1197
|
+
for (const entry of row.audit) {
|
|
1198
|
+
const parsed = readAuditRecord(entry)
|
|
1199
|
+
if (parsed === undefined) return undefined
|
|
1200
|
+
audit.push(parsed)
|
|
1201
|
+
}
|
|
1202
|
+
return {
|
|
1203
|
+
state: row.state,
|
|
1204
|
+
attempts: row.attempts,
|
|
1205
|
+
startedAt: row.startedAt,
|
|
1206
|
+
...(typeof row.cleanedAt === 'number' ? { cleanedAt: row.cleanedAt } : {}),
|
|
1207
|
+
...(typeof row.lastError === 'string' ? { lastError: row.lastError } : {}),
|
|
1208
|
+
audit,
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
/** Repair one persisted audit credential, or undefined when it is unusable. */
|
|
1213
|
+
function readAuditRecord(value: unknown): AcceptanceAuditRecord | undefined {
|
|
1214
|
+
if (typeof value !== 'object' || value === null) return undefined
|
|
1215
|
+
const row = value as Record<string, unknown>
|
|
1216
|
+
if (row.stage !== 'quality' && row.stage !== 'exception' && row.stage !== 'budget' && row.stage !== 'invalid') return undefined
|
|
1217
|
+
if (typeof row.passed !== 'boolean') return undefined
|
|
1218
|
+
if (typeof row.at !== 'number' || !Number.isFinite(row.at)) return undefined
|
|
1219
|
+
if (typeof row.score !== 'number' || !Number.isFinite(row.score)) return undefined
|
|
1220
|
+
if (typeof row.baseline !== 'number' || !Number.isFinite(row.baseline)) return undefined
|
|
1221
|
+
if (!Array.isArray(row.criteria)) return undefined
|
|
1222
|
+
const criteria: VerificationCriterionScore[] = []
|
|
1223
|
+
for (const entry of row.criteria) {
|
|
1224
|
+
if (typeof entry !== 'object' || entry === null) return undefined
|
|
1225
|
+
const criterion = entry as Record<string, unknown>
|
|
1226
|
+
if (typeof criterion.id !== 'string' || typeof criterion.name !== 'string') return undefined
|
|
1227
|
+
if (typeof criterion.score !== 'number' || !Number.isFinite(criterion.score)) return undefined
|
|
1228
|
+
if (typeof criterion.baseline !== 'number' || !Number.isFinite(criterion.baseline)) return undefined
|
|
1229
|
+
if (typeof criterion.threshold !== 'number' || !Number.isFinite(criterion.threshold)) return undefined
|
|
1230
|
+
criteria.push({
|
|
1231
|
+
id: criterion.id,
|
|
1232
|
+
name: criterion.name,
|
|
1233
|
+
score: criterion.score,
|
|
1234
|
+
baseline: criterion.baseline,
|
|
1235
|
+
threshold: criterion.threshold,
|
|
1236
|
+
passed: criterion.passed === true,
|
|
1237
|
+
})
|
|
1238
|
+
}
|
|
1239
|
+
if (typeof row.evidenceHash !== 'string') return undefined
|
|
1240
|
+
const route = row.route
|
|
1241
|
+
if (typeof route !== 'object' || route === null) return undefined
|
|
1242
|
+
const routeRow = route as Record<string, unknown>
|
|
1243
|
+
if (typeof routeRow.provider !== 'string' || typeof routeRow.model !== 'string') return undefined
|
|
1244
|
+
const usage = row.usage
|
|
1245
|
+
if (typeof usage !== 'object' || usage === null) return undefined
|
|
1246
|
+
const usageRow = usage as Record<string, unknown>
|
|
1247
|
+
if (typeof usageRow.calls !== 'number' || typeof usageRow.inputTokens !== 'number'
|
|
1248
|
+
|| typeof usageRow.outputTokens !== 'number' || typeof usageRow.reasoningTokens !== 'number') return undefined
|
|
1249
|
+
return {
|
|
1250
|
+
stage: row.stage,
|
|
1251
|
+
passed: row.passed,
|
|
1252
|
+
at: row.at,
|
|
1253
|
+
score: row.score,
|
|
1254
|
+
baseline: row.baseline,
|
|
1255
|
+
criteria,
|
|
1256
|
+
evidenceHash: row.evidenceHash,
|
|
1257
|
+
route: {
|
|
1258
|
+
provider: routeRow.provider,
|
|
1259
|
+
model: routeRow.model,
|
|
1260
|
+
...(typeof routeRow.reasoningEffort === 'string' ? { reasoningEffort: routeRow.reasoningEffort } : {}),
|
|
1261
|
+
},
|
|
1262
|
+
...(typeof row.error === 'string' ? { error: row.error } : {}),
|
|
1263
|
+
...(typeof row.findingSummary === 'string' ? { findingSummary: row.findingSummary } : {}),
|
|
1264
|
+
usage: {
|
|
1265
|
+
calls: usageRow.calls,
|
|
1266
|
+
inputTokens: usageRow.inputTokens,
|
|
1267
|
+
outputTokens: usageRow.outputTokens,
|
|
1268
|
+
reasoningTokens: usageRow.reasoningTokens,
|
|
1269
|
+
...(usageRow.usageIncomplete === true ? { usageIncomplete: true } : {}),
|
|
1270
|
+
},
|
|
596
1271
|
}
|
|
597
1272
|
}
|
|
598
1273
|
|
|
@@ -600,6 +1275,8 @@ export function normalizeVerification(value: unknown): ExecutionVerification | u
|
|
|
600
1275
|
export interface VerificationTotals {
|
|
601
1276
|
quality: number
|
|
602
1277
|
exceptions: number
|
|
1278
|
+
/** Invalid acceptances: no usable veto, so no quality verdict and no card failure. */
|
|
1279
|
+
invalid: number
|
|
603
1280
|
calls: number
|
|
604
1281
|
inputTokens: number
|
|
605
1282
|
outputTokens: number
|
|
@@ -609,10 +1286,11 @@ export interface VerificationTotals {
|
|
|
609
1286
|
|
|
610
1287
|
/** Sum the attempts of one execution. */
|
|
611
1288
|
export function verificationTotals(verification: ExecutionVerification | undefined): VerificationTotals {
|
|
612
|
-
const totals: VerificationTotals = { quality: 0, exceptions: 0, calls: 0, inputTokens: 0, outputTokens: 0, reasoningTokens: 0, usageIncomplete: false }
|
|
1289
|
+
const totals: VerificationTotals = { quality: 0, exceptions: 0, invalid: 0, calls: 0, inputTokens: 0, outputTokens: 0, reasoningTokens: 0, usageIncomplete: false }
|
|
613
1290
|
if (verification === undefined) return totals
|
|
614
1291
|
for (const attempt of verification.attempts) {
|
|
615
1292
|
if (attempt.stage === 'quality') totals.quality += 1
|
|
1293
|
+
else if (attempt.stage === 'invalid') totals.invalid += 1
|
|
616
1294
|
else totals.exceptions += 1
|
|
617
1295
|
totals.calls += attempt.usage.calls
|
|
618
1296
|
totals.inputTokens += attempt.usage.inputTokens
|
|
@@ -639,9 +1317,10 @@ export function qualityFeedback(attempt: VerificationAttempt, remaining: number)
|
|
|
639
1317
|
'总分 ' + percent(attempt.score) + '(阈值 ' + percent(attempt.criteria[0]?.threshold ?? VERIFICATION_THRESHOLD) + '),空工作基线 ' + percent(attempt.baseline) + '。',
|
|
640
1318
|
'未达标判据:' + (failed.length === 0 ? '无(总分或基线比较未通过)' : failed.map(criterion => criterion.name + ' ' + percent(criterion.score)).join('、')),
|
|
641
1319
|
]
|
|
642
|
-
|
|
1320
|
+
const located = locatedFindingLines(attempt)
|
|
1321
|
+
if (located.length > 0) {
|
|
643
1322
|
lines.push('可定位问题:')
|
|
644
|
-
for (const
|
|
1323
|
+
for (const line of located) lines.push('- ' + line)
|
|
645
1324
|
} else {
|
|
646
1325
|
lines.push('本次裁判没有报告可定位的问题;请直接对照任务要求核对证据中的真实输出。')
|
|
647
1326
|
}
|
|
@@ -649,6 +1328,67 @@ export function qualityFeedback(attempt: VerificationAttempt, remaining: number)
|
|
|
649
1328
|
return lines.join('\n')
|
|
650
1329
|
}
|
|
651
1330
|
|
|
1331
|
+
/**
|
|
1332
|
+
* The feedback one INVALID acceptance returns.
|
|
1333
|
+
*
|
|
1334
|
+
* The claim is refused (the board still has no pass record, so completion may
|
|
1335
|
+
* not take effect) but the agent is NOT handed a repair instruction: there is
|
|
1336
|
+
* no located problem to repair, and telling it to "fix" an evidence-free veto
|
|
1337
|
+
* would be exactly the blind repair the requirement forbids. The text says what
|
|
1338
|
+
* was missing, what happens next, and what the agent should do instead (make
|
|
1339
|
+
* the missing output itself observable, or let a human look).
|
|
1340
|
+
* @param attempt - the invalid attempt.
|
|
1341
|
+
* @param remaining - invalid-acceptance attempts left after this one.
|
|
1342
|
+
* @returns the model-facing text.
|
|
1343
|
+
*/
|
|
1344
|
+
export function invalidAcceptanceFeedback(attempt: VerificationAttempt, remaining: number): string {
|
|
1345
|
+
const lines = [
|
|
1346
|
+
'[任务看板 · 验收无效] 第 ' + attempt.index + ' 次验收没有产生可用的质量判定。',
|
|
1347
|
+
'原因:' + invalidReasonText(attempt.invalidReason) + (attempt.error === undefined || attempt.error === '' ? '' : '(' + attempt.error + ')'),
|
|
1348
|
+
'本次未消耗质量验收额度,也不把本次执行判为失败。',
|
|
1349
|
+
]
|
|
1350
|
+
if (attempt.invalidReason === 'insufficient-evidence') {
|
|
1351
|
+
lines.push('裁判看到的证据不完整(本次轨迹被截断 ' + attempt.evidence.omittedCharacters + ' 字符),无法据此否决;这属于验收无效,不代表执行者没有完成工作。')
|
|
1352
|
+
lines.push('请把关键结论的证据直接放进你能控制的输出里(例如把最终验证命令的输出贴出来),或说明还需要哪些证据;再次调用 update_goal(action: complete) 会重新验收。')
|
|
1353
|
+
} else {
|
|
1354
|
+
lines.push('验收没有拿到任务要求、实际观测、差异与可定位引用俱全的证据,因此本次否决无效。')
|
|
1355
|
+
lines.push('请不要据此盲目修改:先确认你的交付是否真的满足了任务要求,并确保关键产物/输出可见;再次调用 update_goal(action: complete) 会重新验收。')
|
|
1356
|
+
}
|
|
1357
|
+
if (remaining > 0) {
|
|
1358
|
+
lines.push('无效验收重试额度还剩 ' + remaining + ' 次;用尽后看板会停止调用裁判并等待人工处理。')
|
|
1359
|
+
} else {
|
|
1360
|
+
lines.push('无效验收重试额度已用尽:看板将停止调用裁判并把本次执行留在等待人工处理的状态。')
|
|
1361
|
+
}
|
|
1362
|
+
return lines.join('\n')
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
/**
|
|
1366
|
+
* The reason one execution records when its invalid-acceptance budget is spent.
|
|
1367
|
+
*
|
|
1368
|
+
* Deliberately NOT a quality failure: no veto was ever substantiated, so the
|
|
1369
|
+
* card stays open for a human and the reason says the acceptance is INVALID
|
|
1370
|
+
* rather than rejected.
|
|
1371
|
+
* @param used - invalid attempts spent.
|
|
1372
|
+
* @returns the user- and model-facing text.
|
|
1373
|
+
*/
|
|
1374
|
+
export function invalidHoldReason(used: number): string {
|
|
1375
|
+
return 'goal 验收无效(连续 ' + used + ' 次未产生可用判定):验收没有给出与任务要求对应、且可定位到本次证据的否决依据,'
|
|
1376
|
+
+ '本次执行未判失败、未消耗质量验收额度。看板已停止调用裁判,等待人工处理:修复验收环境或证据可见性后,'
|
|
1377
|
+
+ '用 task_board_manage(action=reset-verification) 清除记录的无效验收,再重新完成目标;也可以直接重跑该卡片。'
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
/** Human-readable text of one invalid reason. */
|
|
1381
|
+
export function invalidReasonText(reason: VerificationInvalidReason | undefined): string {
|
|
1382
|
+
switch (reason) {
|
|
1383
|
+
case 'missing-finding': return '未达标的判据没有对应的结构化问题反馈'
|
|
1384
|
+
case 'unlocatable-quote': return '问题反馈引用的位置不存在于本次裁判所见的证据中'
|
|
1385
|
+
case 'baseline-finding': return '问题反馈指向空工作基线,而不是本次执行的工作'
|
|
1386
|
+
case 'vacuous-finding': return '问题反馈为空或空泛评价,没有说明实际观测与差异'
|
|
1387
|
+
case 'insufficient-evidence': return '裁判看到的证据不足(例如轨迹被截断),无法支撑否决'
|
|
1388
|
+
default: return '未产生可用的质量判定'
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
652
1392
|
/**
|
|
653
1393
|
* The reason one exhausted acceptance cycle records as the execution's final
|
|
654
1394
|
* failure. Unlike {@link qualityFeedback} — which the fixing agent reads while
|
|
@@ -669,15 +1409,43 @@ export function finalFailureReason(attempt: VerificationAttempt, used: number, t
|
|
|
669
1409
|
';未达标判据:' + (failed.length === 0 ? '总分或基线比较未通过' : failed.map(criterion => criterion.name + ' ' + percent(criterion.score)).join('、')) +
|
|
670
1410
|
'),本次 execution 判失败。',
|
|
671
1411
|
]
|
|
672
|
-
|
|
1412
|
+
const located = locatedFindingLines(attempt)
|
|
1413
|
+
if (located.length > 0) {
|
|
673
1414
|
lines.push('验收报告记录的可定位问题:')
|
|
674
|
-
for (const
|
|
1415
|
+
for (const line of located) lines.push('- ' + line)
|
|
675
1416
|
} else {
|
|
676
1417
|
lines.push('本次验收没有记录可定位的问题;请对照任务要求与验收面板中的判据得分核对证据中的真实输出。')
|
|
677
1418
|
}
|
|
678
1419
|
return lines.join('\n')
|
|
679
1420
|
}
|
|
680
1421
|
|
|
1422
|
+
/**
|
|
1423
|
+
* The locatable problem lines of one attempt, structured findings first.
|
|
1424
|
+
*
|
|
1425
|
+
* A structured finding renders the requirement, the observed fact, the gap and
|
|
1426
|
+
* the citation in a fixed order, so the fixing agent reads WHAT was required,
|
|
1427
|
+
* WHAT was seen, WHY it falls short and WHERE to look — never a bare complaint.
|
|
1428
|
+
* @param attempt - the attempt whose findings are rendered.
|
|
1429
|
+
* @returns one line per finding, bounded.
|
|
1430
|
+
*/
|
|
1431
|
+
export function locatedFindingLines(attempt: VerificationAttempt): string[] {
|
|
1432
|
+
const structured = attempt.criterionFindings ?? []
|
|
1433
|
+
if (structured.length > 0) {
|
|
1434
|
+
return structured.map(finding => {
|
|
1435
|
+
const locator = '[' + finding.criterionId + ' ' + finding.location + '] "' + clip(finding.quote, 200) + '"'
|
|
1436
|
+
const action = finding.action === undefined || finding.action === '' ? '' : ' → 建议核实:' + finding.action
|
|
1437
|
+
return locator + ' 要求:' + finding.requirement + ' 观测:' + finding.observation + ' 差异:' + finding.gap + action
|
|
1438
|
+
})
|
|
1439
|
+
}
|
|
1440
|
+
return [...attempt.findings]
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
/** Bound one text for a single-line rendering. */
|
|
1444
|
+
function clip(text: string, max: number): string {
|
|
1445
|
+
const value = text.replace(/s+/g, ' ').trim()
|
|
1446
|
+
return value.length <= max ? value : value.slice(0, max) + '…'
|
|
1447
|
+
}
|
|
1448
|
+
|
|
681
1449
|
/** `0.42` style percentage. */
|
|
682
1450
|
export function percent(value: number): string {
|
|
683
1451
|
return (value * 100).toFixed(1) + '%'
|