dsh-llm-verifier 0.1.13 → 0.1.16

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.
Files changed (90) hide show
  1. package/LICENSE +22 -22
  2. package/README.md +328 -301
  3. package/cordis.patch.yml +4 -4
  4. package/lib/caller-BWwSVNZf.js.map +1 -0
  5. package/lib/client.js +176 -14
  6. package/lib/client.js.map +1 -0
  7. package/lib/core.js.map +1 -0
  8. package/lib/index.js +400 -51
  9. package/lib/index.js.map +1 -0
  10. package/lib/types/auto.d.ts.map +1 -0
  11. package/lib/types/auto.js +8 -2
  12. package/lib/types/auto.js.map +1 -0
  13. package/lib/types/cache.d.ts.map +1 -0
  14. package/lib/types/cache.js.map +1 -0
  15. package/lib/types/caller.d.ts.map +1 -0
  16. package/lib/types/caller.js.map +1 -0
  17. package/lib/types/client-i18n.d.ts +27 -0
  18. package/lib/types/client-i18n.d.ts.map +1 -0
  19. package/lib/types/client-i18n.js +18 -0
  20. package/lib/types/client-i18n.js.map +1 -0
  21. package/lib/types/client.d.ts +15 -2
  22. package/lib/types/client.d.ts.map +1 -0
  23. package/lib/types/client.js +137 -10
  24. package/lib/types/client.js.map +1 -0
  25. package/lib/types/config.d.ts +4 -0
  26. package/lib/types/config.d.ts.map +1 -0
  27. package/lib/types/config.js +5 -3
  28. package/lib/types/config.js.map +1 -0
  29. package/lib/types/core.d.ts.map +1 -0
  30. package/lib/types/core.js.map +1 -0
  31. package/lib/types/engine.d.ts.map +1 -0
  32. package/lib/types/engine.js.map +1 -0
  33. package/lib/types/images.d.ts.map +1 -0
  34. package/lib/types/images.js.map +1 -0
  35. package/lib/types/index.d.ts +2 -0
  36. package/lib/types/index.d.ts.map +1 -0
  37. package/lib/types/index.js +162 -9
  38. package/lib/types/index.js.map +1 -0
  39. package/lib/types/plan-gate.d.ts +27 -0
  40. package/lib/types/plan-gate.d.ts.map +1 -0
  41. package/lib/types/plan-gate.js +60 -0
  42. package/lib/types/plan-gate.js.map +1 -0
  43. package/lib/types/router.d.ts +28 -32
  44. package/lib/types/router.d.ts.map +1 -0
  45. package/lib/types/router.js +41 -9
  46. package/lib/types/router.js.map +1 -0
  47. package/lib/types/session.d.ts +11 -0
  48. package/lib/types/session.d.ts.map +1 -0
  49. package/lib/types/session.js +41 -11
  50. package/lib/types/session.js.map +1 -0
  51. package/lib/types/statistics.d.ts.map +1 -0
  52. package/lib/types/statistics.js.map +1 -0
  53. package/lib/types/team-gate.d.ts +20 -0
  54. package/lib/types/team-gate.d.ts.map +1 -0
  55. package/lib/types/team-gate.js +59 -0
  56. package/lib/types/team-gate.js.map +1 -0
  57. package/lib/types/top-logprobs.d.ts.map +1 -0
  58. package/lib/types/top-logprobs.js.map +1 -0
  59. package/lib/types/topic-storage.d.ts.map +1 -0
  60. package/lib/types/topic-storage.js.map +1 -0
  61. package/package.json +114 -112
  62. package/src/auto.test.ts +95 -86
  63. package/src/auto.ts +140 -133
  64. package/src/cache.ts +115 -115
  65. package/src/caller.test.ts +34 -34
  66. package/src/caller.ts +151 -151
  67. package/src/client-i18n.ts +18 -0
  68. package/src/client.tsx +383 -213
  69. package/src/config.ts +189 -183
  70. package/src/core.test.ts +71 -71
  71. package/src/core.ts +205 -205
  72. package/src/engine.test.ts +136 -136
  73. package/src/engine.ts +140 -140
  74. package/src/images.ts +33 -33
  75. package/src/index.ts +394 -247
  76. package/src/parity.test.ts +85 -85
  77. package/src/plan-gate.test.ts +56 -0
  78. package/src/plan-gate.ts +66 -0
  79. package/src/router.test.ts +107 -89
  80. package/src/router.ts +361 -309
  81. package/src/session.test.ts +71 -36
  82. package/src/session.ts +132 -103
  83. package/src/statistics.test.ts +80 -80
  84. package/src/statistics.ts +328 -328
  85. package/src/team-gate.test.ts +92 -0
  86. package/src/team-gate.ts +80 -0
  87. package/src/top-logprobs.test.ts +84 -84
  88. package/src/top-logprobs.ts +153 -153
  89. package/src/topic-storage.test.ts +31 -31
  90. package/src/topic-storage.ts +42 -42
@@ -1,85 +1,85 @@
1
- import { execFileSync } from 'node:child_process'
2
- import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
3
- import { tmpdir } from 'node:os'
4
- import { join } from 'node:path'
5
- import { describe, expect, it } from 'vitest'
6
- import { ScoreCache } from './cache.ts'
7
- import { extractProgressScore, extractScore, pivotRoundPairs } from './core.ts'
8
- import { resolveConfig } from './config.ts'
9
-
10
- const PYTHON_ROOT = join(import.meta.dirname, '..', '..', 'llm-as-a-verifier')
11
- const hasPythonUpstream = existsSync(join(PYTHON_ROOT, 'llm_verifier'))
12
-
13
- function python(payload: unknown): unknown {
14
- const script = [
15
- 'import json,sys',
16
- 'sys.path.insert(0, sys.argv[1])',
17
- 'from llm_verifier.fine_grained_reward import extract_score',
18
- 'from llm_verifier.pivot_tournament import pivot_round_pairs',
19
- 'd=json.loads(sys.stdin.read())',
20
- "result = extract_score(d['text'], d['tokens'], d['positions'], d['tag']) if d['kind']=='score' else pivot_round_pairs(d['n'], d['pivots'])",
21
- 'print(json.dumps(result))',
22
- ].join(';')
23
- const source = payload as Record<string, unknown>
24
- const compatible = source.kind === 'score' ? { ...source, positions: (source.positions as Array<Array<{ token: string; logprob: number }>>).map(position => position.map(item => [item.token, item.logprob])) } : source
25
- const result = execFileSync('py', ['-3', '-c', script, PYTHON_ROOT], { input: JSON.stringify(compatible), encoding: 'utf8' })
26
- return JSON.parse(result)
27
- }
28
-
29
- describe('Python parity fixtures', () => {
30
- it.skipIf(!hasPythonUpstream)('matches literal and distribution score extraction', () => {
31
- const fixtures = [
32
- { text: '<score_A> A </score_A>', tokens: [], positions: [], tag: '<score_A>' },
33
- { text: '<score_A> A </score_A> then <score_A> T </score_A>', tokens: [], positions: [], tag: '<score_A>' },
34
- { text: '', tokens: ['<score_A>'], positions: [[], [{ token: 'A', logprob: Math.log(0.7) }, { token: 'T', logprob: Math.log(0.3) }]], tag: '<score_A>' },
35
- { text: '', tokens: ['<score_A'], positions: [[], [{ token: '>B', logprob: Math.log(0.8) }, { token: '>S', logprob: Math.log(0.2) }]], tag: '<score_A>' },
36
- ]
37
- for (const fixture of fixtures) {
38
- const ts = extractScore(fixture, fixture.tag)
39
- const py = python({ kind: 'score', ...fixture }) as number
40
- expect(ts).toBeCloseTo(py, 12)
41
- }
42
- })
43
- it.skipIf(!hasPythonUpstream)('matches pivot pair generation', () => {
44
- expect(pivotRoundPairs(7, [1, 4, 5])).toEqual(python({ kind: 'pivot', n: 7, pivots: [1, 4, 5] }))
45
- })
46
- it('matches reversed progress convention', () => {
47
- expect(extractProgressScore({ text: '<c1> T </c1>', tokens: [], positions: [] }, '<c1>')).toBe(1)
48
- })
49
- })
50
-
51
- describe('DSH model routing and cache', () => {
52
- it('accepts any non-empty DSH provider and model route', () => {
53
- expect(resolveConfig({ provider: 'openai', model: 'gpt-5' })).toMatchObject({ provider: 'openai', model: 'gpt-5' })
54
- expect(resolveConfig({ provider: 'anthropic', model: 'claude-sonnet' })).toMatchObject({ provider: 'anthropic', model: 'claude-sonnet' })
55
- expect(() => resolveConfig({ provider: '', model: 'gpt-5' })).toThrow(/provider must be non-empty/)
56
- expect(resolveConfig({ autoRouteMaxItemChars: 1000, autoRouteMaxInputChars: 2000, autoMaxModelCallsPerTask: 4, autoMaxModelCallsPerSession: 8 })).toMatchObject({ autoRouteMaxItemChars: 1000, autoRouteMaxInputChars: 2000, autoMaxModelCallsPerTask: 4, autoMaxModelCallsPerSession: 8 })
57
- expect(() => resolveConfig({ autoRouteMaxCandidates: 17 })).toThrow(/between 3 and 16/)
58
- expect(() => resolveConfig({ autoRouteMaxItemChars: 2000, autoRouteMaxInputChars: 3000 })).toThrow(/fit at least two/)
59
- })
60
- it('rejects malformed persisted entries and recreates them', async () => {
61
- const dir = mkdtempSync(join(tmpdir(), 'dsh-verifier-invalid-'))
62
- try {
63
- const file = join(dir, 'scores.json')
64
- writeFileSync(file, JSON.stringify({ version: 1, entries: { bad: { scoreA: 'NaN', scoreB: 0, usage: {}, createdAt: -1 } } }))
65
- const cache = new ScoreCache(file, 100)
66
- let creates = 0
67
- const result = await cache.getOrCreate('bad', async () => { creates++; return { scoreA: .8, scoreB: .2, usage: { calls: 1, attempts: 1, retries: 0, inputTokens: 1, cachedInputTokens: 0, outputTokens: 1, reasoningTokens: 0 }, scoringMode: 'explicit-tag' as const, createdAt: Date.now() } })
68
- expect(result.hit).toBe(false); expect(creates).toBe(1)
69
- } finally { rmSync(dir, { recursive: true, force: true }) }
70
- })
71
- it('persists successful results and avoids duplicate creation', async () => {
72
- const dir = mkdtempSync(join(tmpdir(), 'dsh-verifier-'))
73
- try {
74
- const file = join(dir, 'scores.json')
75
- const first = new ScoreCache(file, 100)
76
- let creates = 0
77
- const create = async () => { creates++; return { scoreA: 1, scoreB: 0, usage: { calls: 1, attempts: 1, retries: 0, inputTokens: 1, cachedInputTokens: 0, outputTokens: 1, reasoningTokens: 0 }, scoringMode: 'top-logprobs' as const, createdAt: Date.now() } }
78
- expect((await first.getOrCreate('key', create)).hit).toBe(false)
79
- expect((await first.getOrCreate('key', create)).hit).toBe(true)
80
- const second = new ScoreCache(file, 100)
81
- expect((await second.getOrCreate('key', create)).hit).toBe(true)
82
- expect(creates).toBe(1)
83
- } finally { rmSync(dir, { recursive: true, force: true }) }
84
- })
85
- })
1
+ import { execFileSync } from 'node:child_process'
2
+ import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
3
+ import { tmpdir } from 'node:os'
4
+ import { join } from 'node:path'
5
+ import { describe, expect, it } from 'vitest'
6
+ import { ScoreCache } from './cache.ts'
7
+ import { extractProgressScore, extractScore, pivotRoundPairs } from './core.ts'
8
+ import { resolveConfig } from './config.ts'
9
+
10
+ const PYTHON_ROOT = join(import.meta.dirname, '..', '..', 'llm-as-a-verifier')
11
+ const hasPythonUpstream = existsSync(join(PYTHON_ROOT, 'llm_verifier'))
12
+
13
+ function python(payload: unknown): unknown {
14
+ const script = [
15
+ 'import json,sys',
16
+ 'sys.path.insert(0, sys.argv[1])',
17
+ 'from llm_verifier.fine_grained_reward import extract_score',
18
+ 'from llm_verifier.pivot_tournament import pivot_round_pairs',
19
+ 'd=json.loads(sys.stdin.read())',
20
+ "result = extract_score(d['text'], d['tokens'], d['positions'], d['tag']) if d['kind']=='score' else pivot_round_pairs(d['n'], d['pivots'])",
21
+ 'print(json.dumps(result))',
22
+ ].join(';')
23
+ const source = payload as Record<string, unknown>
24
+ const compatible = source.kind === 'score' ? { ...source, positions: (source.positions as Array<Array<{ token: string; logprob: number }>>).map(position => position.map(item => [item.token, item.logprob])) } : source
25
+ const result = execFileSync('py', ['-3', '-c', script, PYTHON_ROOT], { input: JSON.stringify(compatible), encoding: 'utf8' })
26
+ return JSON.parse(result)
27
+ }
28
+
29
+ describe('Python parity fixtures', () => {
30
+ it.skipIf(!hasPythonUpstream)('matches literal and distribution score extraction', () => {
31
+ const fixtures = [
32
+ { text: '<score_A> A </score_A>', tokens: [], positions: [], tag: '<score_A>' },
33
+ { text: '<score_A> A </score_A> then <score_A> T </score_A>', tokens: [], positions: [], tag: '<score_A>' },
34
+ { text: '', tokens: ['<score_A>'], positions: [[], [{ token: 'A', logprob: Math.log(0.7) }, { token: 'T', logprob: Math.log(0.3) }]], tag: '<score_A>' },
35
+ { text: '', tokens: ['<score_A'], positions: [[], [{ token: '>B', logprob: Math.log(0.8) }, { token: '>S', logprob: Math.log(0.2) }]], tag: '<score_A>' },
36
+ ]
37
+ for (const fixture of fixtures) {
38
+ const ts = extractScore(fixture, fixture.tag)
39
+ const py = python({ kind: 'score', ...fixture }) as number
40
+ expect(ts).toBeCloseTo(py, 12)
41
+ }
42
+ })
43
+ it.skipIf(!hasPythonUpstream)('matches pivot pair generation', () => {
44
+ expect(pivotRoundPairs(7, [1, 4, 5])).toEqual(python({ kind: 'pivot', n: 7, pivots: [1, 4, 5] }))
45
+ })
46
+ it('matches reversed progress convention', () => {
47
+ expect(extractProgressScore({ text: '<c1> T </c1>', tokens: [], positions: [] }, '<c1>')).toBe(1)
48
+ })
49
+ })
50
+
51
+ describe('DSH model routing and cache', () => {
52
+ it('accepts any non-empty DSH provider and model route', () => {
53
+ expect(resolveConfig({ provider: 'openai', model: 'gpt-5' })).toMatchObject({ provider: 'openai', model: 'gpt-5' })
54
+ expect(resolveConfig({ provider: 'anthropic', model: 'claude-sonnet' })).toMatchObject({ provider: 'anthropic', model: 'claude-sonnet' })
55
+ expect(() => resolveConfig({ provider: '', model: 'gpt-5' })).toThrow(/provider must be non-empty/)
56
+ expect(resolveConfig({ autoRouteMaxItemChars: 1000, autoRouteMaxInputChars: 2000, autoMaxModelCallsPerTask: 4, autoMaxModelCallsPerSession: 8 })).toMatchObject({ autoRouteMaxItemChars: 1000, autoRouteMaxInputChars: 2000, autoMaxModelCallsPerTask: 4, autoMaxModelCallsPerSession: 8 })
57
+ expect(() => resolveConfig({ autoRouteMaxCandidates: 17 })).toThrow(/between 3 and 16/)
58
+ expect(() => resolveConfig({ autoRouteMaxItemChars: 2000, autoRouteMaxInputChars: 3000 })).toThrow(/fit at least two/)
59
+ })
60
+ it('rejects malformed persisted entries and recreates them', async () => {
61
+ const dir = mkdtempSync(join(tmpdir(), 'dsh-verifier-invalid-'))
62
+ try {
63
+ const file = join(dir, 'scores.json')
64
+ writeFileSync(file, JSON.stringify({ version: 1, entries: { bad: { scoreA: 'NaN', scoreB: 0, usage: {}, createdAt: -1 } } }))
65
+ const cache = new ScoreCache(file, 100)
66
+ let creates = 0
67
+ const result = await cache.getOrCreate('bad', async () => { creates++; return { scoreA: .8, scoreB: .2, usage: { calls: 1, attempts: 1, retries: 0, inputTokens: 1, cachedInputTokens: 0, outputTokens: 1, reasoningTokens: 0 }, scoringMode: 'explicit-tag' as const, createdAt: Date.now() } })
68
+ expect(result.hit).toBe(false); expect(creates).toBe(1)
69
+ } finally { rmSync(dir, { recursive: true, force: true }) }
70
+ })
71
+ it('persists successful results and avoids duplicate creation', async () => {
72
+ const dir = mkdtempSync(join(tmpdir(), 'dsh-verifier-'))
73
+ try {
74
+ const file = join(dir, 'scores.json')
75
+ const first = new ScoreCache(file, 100)
76
+ let creates = 0
77
+ const create = async () => { creates++; return { scoreA: 1, scoreB: 0, usage: { calls: 1, attempts: 1, retries: 0, inputTokens: 1, cachedInputTokens: 0, outputTokens: 1, reasoningTokens: 0 }, scoringMode: 'top-logprobs' as const, createdAt: Date.now() } }
78
+ expect((await first.getOrCreate('key', create)).hit).toBe(false)
79
+ expect((await first.getOrCreate('key', create)).hit).toBe(true)
80
+ const second = new ScoreCache(file, 100)
81
+ expect((await second.getOrCreate('key', create)).hit).toBe(true)
82
+ expect(creates).toBe(1)
83
+ } finally { rmSync(dir, { recursive: true, force: true }) }
84
+ })
85
+ })
@@ -0,0 +1,56 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { buildPlanPreReviewPrompt, parseVerdictLetter, planFromArguments } from './plan-gate.ts'
3
+
4
+ describe('plan-gate', () => {
5
+ it('reads the plan markdown out of exit_plan_mode arguments', () => {
6
+ expect(planFromArguments({ plan: ' # Plan\nStep 1 ' })).toBe('# Plan\nStep 1')
7
+ expect(planFromArguments({ plan: ' ' })).toBe('')
8
+ expect(planFromArguments({})).toBe('')
9
+ expect(planFromArguments({ plan: 42 })).toBe('')
10
+ expect(planFromArguments('plan')).toBe('')
11
+ expect(planFromArguments(null)).toBe('')
12
+ expect(planFromArguments(undefined)).toBe('')
13
+ })
14
+
15
+ it('builds plan review prompt with requirements and an exact verdict line', () => {
16
+ const prompt = buildPlanPreReviewPrompt('Implement Redis cache', '1. Connect redis 2. Cache queries', 5000)
17
+ expect(prompt).toContain('You are an expert independent technical plan verifier')
18
+ expect(prompt).toContain('Implement Redis cache')
19
+ expect(prompt).toContain('1. Connect redis 2. Cache queries')
20
+ expect(prompt).toContain('Verdict: <single uppercase letter A-T>')
21
+ })
22
+
23
+ it('parses a whole verdict line into the A-T score scale', () => {
24
+ const perfect = parseVerdictLetter('Verdict: A\nSummary: Outstanding plan.\nVerification is complete.')
25
+ expect(perfect?.verdict).toBe('A')
26
+ expect(perfect?.score).toBe(1.0)
27
+ expect(perfect?.feedback).toContain('Outstanding plan')
28
+
29
+ const failed = parseVerdictLetter('Verdict: T\nSummary: Completely unfeasible.')
30
+ expect(failed?.verdict).toBe('T')
31
+ expect(failed?.score).toBe(0.0)
32
+
33
+ const middle = parseVerdictLetter('Verdict: C\nSummary: Mostly good.')
34
+ expect(middle?.verdict).toBe('C')
35
+ expect(middle?.score).toBeCloseTo(1 - 2 / 19, 3)
36
+
37
+ expect(parseVerdictLetter('Summary: fine\n verdict: d \nNotes: ok')?.score).toBeCloseTo(1 - 3 / 19, 3)
38
+ })
39
+
40
+ it('reads a verdict line wrapped in markdown decoration', () => {
41
+ expect(parseVerdictLetter('**Verdict: A**\nSummary: ok')?.verdict).toBe('A')
42
+ expect(parseVerdictLetter('**Verdict**: B')?.score).toBeCloseTo(1 - 1 / 19, 3)
43
+ expect(parseVerdictLetter('- Verdict: C.')?.verdict).toBe('C')
44
+ expect(parseVerdictLetter('1. Verdict: D)')?.verdict).toBe('D')
45
+ expect(parseVerdictLetter('`Verdict: E`')?.verdict).toBe('E')
46
+ expect(parseVerdictLetter('### Verdict: T')?.score).toBe(0)
47
+ })
48
+
49
+ it('never scores prose, a missing verdict, or a trailing explanation', () => {
50
+ expect(parseVerdictLetter('Verdict: Failed\nSummary: the plan cannot work')).toBeUndefined()
51
+ expect(parseVerdictLetter('Verdict: Approved')).toBeUndefined()
52
+ expect(parseVerdictLetter('Verdict: A because the plan is great')).toBeUndefined()
53
+ expect(parseVerdictLetter('Summary: no verdict line at all')).toBeUndefined()
54
+ expect(parseVerdictLetter('')).toBeUndefined()
55
+ })
56
+ })
@@ -0,0 +1,66 @@
1
+ import { sanitizeVerifierText } from './session.ts'
2
+
3
+ export interface PlanVerdict {
4
+ verdict: string
5
+ score: number
6
+ feedback: string
7
+ }
8
+
9
+ /**
10
+ * Read the plan markdown out of one `exit_plan_mode` argument object.
11
+ * @param args - Parsed tool arguments; anything but `{ plan: string }` yields ''.
12
+ * @returns Trimmed plan markdown, or '' when the call carries no plan.
13
+ */
14
+ export function planFromArguments(args: unknown): string {
15
+ if (typeof args !== 'object' || args === null) return ''
16
+ const plan = (args as { plan?: unknown }).plan
17
+ return typeof plan === 'string' ? plan.trim() : ''
18
+ }
19
+
20
+ export function buildPlanPreReviewPrompt(problem: string, planText: string, maxChars = 20000): string {
21
+ return [
22
+ 'You are an expert independent technical plan verifier. A candidate implementation plan is about to be submitted to the human for approval.',
23
+ 'Evaluate whether the plan is sound, executable, and comprehensive.',
24
+ 'Scoring criteria:',
25
+ 'A: Flawless, thorough, with clear steps, rigorous verification, edge cases handled.',
26
+ 'B-J: Mostly sound, minor gaps or nuances missing.',
27
+ 'K-S: Noticeable architectural gaps, missing regression checks, or ambiguities.',
28
+ 'T: Fundamentally flawed, dangerous, or missing core requirements.',
29
+ '',
30
+ 'Task requirement:',
31
+ sanitizeVerifierText(problem, 4000),
32
+ '',
33
+ 'Proposed Plan:',
34
+ sanitizeVerifierText(planText, maxChars),
35
+ '',
36
+ 'Output format:',
37
+ 'The first line must be exactly "Verdict: <single uppercase letter A-T>" — one letter, nothing else on the line.',
38
+ 'Line 2: Summary: <One sentence assessment>',
39
+ 'Line 3+: Key strengths, blind spots, and verification guidance.',
40
+ ].join('\n\n')
41
+ }
42
+
43
+ /**
44
+ * Parse the `Verdict: <A-T>` line the judge prompts require.
45
+ *
46
+ * Markdown decoration around the required line is stripped first, so "**Verdict:
47
+ * A**", "- Verdict: B." or a numbered list marker still produce a grade — a
48
+ * formatting habit must never silently switch the gate off. Prose that is not a
49
+ * grade is still rejected: "Verdict: Failed" and "Verdict: Approved" carry no
50
+ * verdict letter, and an answer without a verdict line has no score at all —
51
+ * callers must skip such a review, never treat it as a pass. A is 1.0 and T is
52
+ * 0.0, matching the top-logprob A-T scale used by the judge prompts.
53
+ * @param text - Raw judge model output.
54
+ * @returns The parsed verdict, or undefined when no verdict line is present.
55
+ */
56
+ export function parseVerdictLetter(text: string): PlanVerdict | undefined {
57
+ for (const rawLine of text.split(/\r?\n/)) {
58
+ const line = rawLine.replace(/[*_`~#>]/g, ' ').replace(/^\s*(?:[-+]|\d+[.)])\s+/, ' ').trim()
59
+ const match = /^verdict\s*:\s*([A-Ta-t])\s*[.)]?$/i.exec(line)
60
+ if (match === null) continue
61
+ const verdict = (match[1] as string).toUpperCase()
62
+ const score = Math.max(0, Math.min(1, 1 - (verdict.charCodeAt(0) - 65) / 19))
63
+ return { verdict, score, feedback: text.trim() }
64
+ }
65
+ return undefined
66
+ }
@@ -1,89 +1,107 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { Session } from '@deepseek-ai/dsh-session'
3
- import { createToolResultMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
4
- import { analyzeStructuredRoute, AutoVerifierRouter, buildSemanticRoutePrompt, parseSemanticRoute, semanticDecision, semanticRouteHint, type RouterPolicy } from './router.ts'
5
-
6
- function session() {
7
- const value = Session.create('session-00000000-0000-4000-8000-000000000077' as never)
8
- value.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'Choose and implement the best solution' }], source: { kind: 'user' } }), { surfaceOp: 'append' })
9
- return value
10
- }
11
- function tool(value: ReturnType<typeof session>, name: string, id: string, text: string, turn = 1, step = 1) {
12
- value.append('tool/call', { turn, step, callId: id as never, name, arguments: '{}' })
13
- value.append('tool/result', { turn, step, message: createToolResultMessage({ callId: id as never, content: [{ type: 'text', text }], isError: false }) }, { surfaceOp: 'append' })
14
- }
15
- const policy: RouterPolicy = { mode: 'smart', minConfidence: .9, maxCandidates: 8, maxPerTask: 5, maxPerSession: 20, maxModelCallsPerTask: 48, maxModelCallsPerSession: 160, maxInputChars: 60000, maxItemChars: 20000 }
16
- const envelope = (count: number) => JSON.stringify({ protocol: 'dsh-verifier-candidates', version: 1, groupId: 'auth', candidates: Array.from({ length: count }, (_, i) => ({ id: String(i + 1), label: 'C' + (i + 1), status: 'completed', content: 'candidate ' + (i + 1) })) })
17
-
18
- describe('production structured routing', () => {
19
- it('requires a trusted versioned workflow envelope', () => {
20
- const value = session(); tool(value, 'workflow', 'w', envelope(3))
21
- expect(analyzeStructuredRoute(value.events)).toMatchObject({ kind: 'select', source: 'structured' })
22
- const untrusted = session(); tool(untrusted, 'workflow', 'w', JSON.stringify({ verifier_candidates: ['a', 'b'] }))
23
- expect(analyzeStructuredRoute(untrusted.events)).toBeUndefined()
24
- })
25
- it('does not guess that unrelated synchronous subagents are alternatives', () => {
26
- const value = session(); tool(value, 'subagent', 'a', 'frontend analysis', 1, 2); tool(value, 'subagent', 'b', 'backend analysis', 1, 2)
27
- expect(analyzeStructuredRoute(value.events)).toBeUndefined()
28
- expect(semanticRouteHint(value.events)).toBe(true)
29
- })
30
- it('deduplicates identical todo snapshots', () => {
31
- const value = session()
32
- const todos = [{ content: 'Implement', status: 'in_progress' as const }, { content: 'Test', status: 'pending' as const }]
33
- value.append('todo/write', { todos }); value.append('todo/write', { todos })
34
- expect(analyzeStructuredRoute(value.events)).toBeUndefined()
35
- value.append('todo/write', { todos: [{ content: 'Implement', status: 'completed' }, { content: 'Test', status: 'in_progress' }] })
36
- expect(analyzeStructuredRoute(value.events)).toMatchObject({ kind: 'track', evidenceSeqs: expect.any(Array) })
37
- })
38
- it('redacts and bounds trusted candidate content', () => {
39
- const value = session(); tool(value, 'workflow', 'w', JSON.stringify({ protocol: 'dsh-verifier-candidates', version: 1, groupId: 'g', candidates: [{ id: 'a', status: 'completed', content: 'token = abc ' + 'x'.repeat(100) }, { id: 'b', status: 'completed', content: 'password: secret ' + 'y'.repeat(100) }] }))
40
- const decision = analyzeStructuredRoute(value.events, 8, 40)
41
- expect(decision?.kind).toBe('compare')
42
- if (decision?.kind === 'compare') { expect(decision.candidates[0].content).not.toContain('abc'); expect(decision.candidates[0].content.length).toBeLessThan(80) }
43
- })
44
- })
45
-
46
- describe('semantic evidence references', () => {
47
- it('accepts exact strict JSON references and rejects prose/unknown keys', () => {
48
- const valid = JSON.stringify({ kind: 'compare', confidence: .95, reason: 'alternatives', candidateCallIds: ['a', 'b'], checkpointSeqs: [] })
49
- expect(parseSemanticRoute(valid)).toMatchObject({ kind: 'compare', candidateCallIds: ['a', 'b'] })
50
- expect(parseSemanticRoute('```json\n' + valid + '\n```')).toBeUndefined()
51
- expect(parseSemanticRoute(JSON.stringify({ ...JSON.parse(valid), extra: true }))).toBeUndefined()
52
- })
53
- it('resolves only real paired call ids', () => {
54
- const value = session(); tool(value, 'subagent', 'a', 'candidate A'); tool(value, 'subagent', 'b', 'candidate B')
55
- const parsed = parseSemanticRoute(JSON.stringify({ kind: 'compare', confidence: .95, reason: 'same task alternatives', candidateCallIds: ['a', 'b'], checkpointSeqs: [] }))!
56
- expect(semanticDecision(parsed, value.events)).toMatchObject({ kind: 'compare', source: 'semantic' })
57
- expect(semanticDecision({ ...parsed, candidateCallIds: ['a', 'missing'] }, value.events)).toBeUndefined()
58
- expect(buildSemanticRoutePrompt('problem', value.events, 5)).toContain('candidateCallIds')
59
- })
60
- it('resolves candidates emitted via PTC mode tool/code-dispatch', () => {
61
- const value = session()
62
- value.append('tool/code-dispatch', { subCallId: 'c-1' as never, name: 'subagent', arguments: '{}', isError: false, content: [{ type: 'text', text: 'candidate 1 content' }] })
63
- value.append('tool/code-dispatch', { subCallId: 'c-2' as never, name: 'subagent', arguments: '{}', isError: false, content: [{ type: 'text', text: 'candidate 2 content' }] })
64
- expect(semanticRouteHint(value.events)).toBe(true)
65
- const parsed = parseSemanticRoute(JSON.stringify({ kind: 'compare', confidence: 0.92, reason: 'PTC alternatives', candidateCallIds: ['c-1', 'c-2'], checkpointSeqs: [] }))!
66
- expect(semanticDecision(parsed, value.events)).toMatchObject({ kind: 'compare', source: 'semantic' })
67
- })
68
- })
69
-
70
- describe('transactional router state', () => {
71
- it('commits, requires final verification, and clears it only after final commit', () => {
72
- const value = session(); const agent = { id: value.id, session: value }; const router = new AutoVerifierRouter()
73
- const route = router.reserve(agent, 'compare', 'route', 4, policy)!
74
- expect(router.commit(agent, route, 9)).toBe(true); expect(router.finalRequired(agent)).toBe(9)
75
- const final = router.reserve(agent, 'final', 'final', 4, policy)!
76
- expect(router.commit(agent, final)).toBe(true); expect(router.finalRequired(agent)).toBeUndefined()
77
- })
78
- it('releases in-flight state after failure and strict remains blocked', () => {
79
- const value = session(); const agent = { id: value.id, session: value }; const router = new AutoVerifierRouter()
80
- const first = router.reserve(agent, 'compare', 'route-a', 4, { ...policy, mode: 'strict' })!
81
- router.fail(agent, first, true); expect(router.strictBlocked(agent)).toBe(true)
82
- expect(router.reserve(agent, 'compare', 'route-b', 4, { ...policy, mode: 'strict' })).toBeDefined()
83
- })
84
- it('enforces manual mode and unified model-call budgets', () => {
85
- const value = session(); const agent = { id: value.id, session: value }; const router = new AutoVerifierRouter()
86
- expect(router.reserve(agent, 'compare', 'manual', 1, { ...policy, mode: 'manual' })).toBeUndefined()
87
- expect(router.reserve(agent, 'select', 'expensive', 49, policy)).toBeUndefined()
88
- })
89
- })
1
+ import { describe, expect, it } from 'vitest'
2
+ import { Session } from '@deepseek-ai/dsh-session'
3
+ import { createToolResultMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
4
+ import { analyzeStructuredRoute, AutoVerifierRouter, buildSemanticRoutePrompt, parseSemanticRoute, semanticDecision, semanticRouteHint, type RouterPolicy } from './router.ts'
5
+
6
+ function session() {
7
+ const value = Session.create('session-00000000-0000-4000-8000-000000000077' as never)
8
+ value.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'Choose and implement the best solution' }], source: { kind: 'user' } }), { surfaceOp: 'append' })
9
+ return value
10
+ }
11
+ function tool(value: ReturnType<typeof session>, name: string, id: string, text: string, turn = 1, step = 1) {
12
+ value.append('tool/call', { turn, step, callId: id as never, name, arguments: '{}' })
13
+ value.append('tool/result', { turn, step, message: createToolResultMessage({ callId: id as never, content: [{ type: 'text', text }], isError: false }) }, { surfaceOp: 'append' })
14
+ }
15
+ const policy: RouterPolicy = { mode: 'smart', minConfidence: .9, maxCandidates: 8, maxPerTask: 5, maxPerSession: 20, maxModelCallsPerTask: 48, maxModelCallsPerSession: 160, maxInputChars: 60000, maxItemChars: 20000 }
16
+ const envelope = (count: number) => JSON.stringify({ protocol: 'dsh-verifier-candidates', version: 1, groupId: 'auth', candidates: Array.from({ length: count }, (_, i) => ({ id: String(i + 1), label: 'C' + (i + 1), status: 'completed', content: 'candidate ' + (i + 1) })) })
17
+
18
+ describe('production structured routing', () => {
19
+ it('requires a trusted versioned workflow envelope', () => {
20
+ const value = session(); tool(value, 'workflow', 'w', envelope(3))
21
+ expect(analyzeStructuredRoute(value.events)).toMatchObject({ kind: 'select', source: 'structured' })
22
+ const untrusted = session(); tool(untrusted, 'workflow', 'w', JSON.stringify({ verifier_candidates: ['a', 'b'] }))
23
+ expect(analyzeStructuredRoute(untrusted.events)).toBeUndefined()
24
+ })
25
+ it('does not guess that unrelated synchronous subagents are alternatives', () => {
26
+ const value = session(); tool(value, 'subagent', 'a', 'frontend analysis', 1, 2); tool(value, 'subagent', 'b', 'backend analysis', 1, 2)
27
+ expect(analyzeStructuredRoute(value.events)).toBeUndefined()
28
+ expect(semanticRouteHint(value.events)).toBe(true)
29
+ })
30
+ it('deduplicates identical todo snapshots', () => {
31
+ const value = session()
32
+ const todos = [{ content: 'Implement', status: 'in_progress' as const }, { content: 'Test', status: 'pending' as const }]
33
+ value.append('todo/write', { todos }); value.append('todo/write', { todos })
34
+ expect(analyzeStructuredRoute(value.events)).toBeUndefined()
35
+ value.append('todo/write', { todos: [{ content: 'Implement', status: 'completed' }, { content: 'Test', status: 'in_progress' }] })
36
+ expect(analyzeStructuredRoute(value.events)).toMatchObject({ kind: 'track', evidenceSeqs: expect.any(Array) })
37
+ })
38
+ it('redacts and bounds trusted candidate content', () => {
39
+ const value = session(); tool(value, 'workflow', 'w', JSON.stringify({ protocol: 'dsh-verifier-candidates', version: 1, groupId: 'g', candidates: [{ id: 'a', status: 'completed', content: 'token = abc ' + 'x'.repeat(100) }, { id: 'b', status: 'completed', content: 'password: secret ' + 'y'.repeat(100) }] }))
40
+ const decision = analyzeStructuredRoute(value.events, 8, 40)
41
+ expect(decision?.kind).toBe('compare')
42
+ if (decision?.kind === 'compare') { expect(decision.candidates[0].content).not.toContain('abc'); expect(decision.candidates[0].content.length).toBeLessThan(80) }
43
+ })
44
+ })
45
+
46
+ describe('semantic evidence references', () => {
47
+ it('accepts exact strict JSON references and rejects prose/unknown keys', () => {
48
+ const valid = JSON.stringify({ kind: 'compare', confidence: .95, reason: 'alternatives', candidateCallIds: ['a', 'b'], checkpointSeqs: [] })
49
+ expect(parseSemanticRoute(valid)).toMatchObject({ kind: 'compare', candidateCallIds: ['a', 'b'] })
50
+ expect(parseSemanticRoute('```json\n' + valid + '\n```')).toBeUndefined()
51
+ expect(parseSemanticRoute(JSON.stringify({ ...JSON.parse(valid), extra: true }))).toBeUndefined()
52
+ })
53
+ it('resolves only real paired call ids', () => {
54
+ const value = session(); tool(value, 'subagent', 'a', 'candidate A'); tool(value, 'subagent', 'b', 'candidate B')
55
+ const parsed = parseSemanticRoute(JSON.stringify({ kind: 'compare', confidence: .95, reason: 'same task alternatives', candidateCallIds: ['a', 'b'], checkpointSeqs: [] }))!
56
+ expect(semanticDecision(parsed, value.events)).toMatchObject({ kind: 'compare', source: 'semantic' })
57
+ expect(semanticDecision({ ...parsed, candidateCallIds: ['a', 'missing'] }, value.events)).toBeUndefined()
58
+ expect(buildSemanticRoutePrompt('problem', value.events, 5)).toContain('candidateCallIds')
59
+ })
60
+ it('resolves candidates emitted via PTC mode tool/code-dispatch', () => {
61
+ const value = session()
62
+ value.append('tool/code-dispatch', { subCallId: 'c-1' as never, name: 'subagent', arguments: '{}', isError: false, content: [{ type: 'text', text: 'candidate 1 content' }] })
63
+ value.append('tool/code-dispatch', { subCallId: 'c-2' as never, name: 'subagent', arguments: '{}', isError: false, content: [{ type: 'text', text: 'candidate 2 content' }] })
64
+ expect(semanticRouteHint(value.events)).toBe(true)
65
+ const parsed = parseSemanticRoute(JSON.stringify({ kind: 'compare', confidence: 0.92, reason: 'PTC alternatives', candidateCallIds: ['c-1', 'c-2'], checkpointSeqs: [] }))!
66
+ expect(semanticDecision(parsed, value.events)).toMatchObject({ kind: 'compare', source: 'semantic' })
67
+ })
68
+
69
+ it('resolves candidates emitted via Session V3 tool/ptc-dispatch', () => {
70
+ const value = session()
71
+ value.append('tool/ptc-dispatch' as never, { subCallId: 'ptc-1', name: 'subagent', arguments: '{}', isError: false, content: [{ type: 'text', text: 'candidate A from ptc' }] } as never)
72
+ value.append('tool/ptc-dispatch' as never, { subCallId: 'ptc-2', name: 'subagent', arguments: '{}', isError: false, content: [{ type: 'text', text: 'candidate B from ptc' }] } as never)
73
+ expect(semanticRouteHint(value.events)).toBe(true)
74
+ const parsed = parseSemanticRoute(JSON.stringify({ kind: 'compare', confidence: 0.95, reason: 'V3 PTC alternatives', candidateCallIds: ['ptc-1', 'ptc-2'], checkpointSeqs: [] }))!
75
+ expect(semanticDecision(parsed, value.events)).toMatchObject({ kind: 'compare', source: 'semantic' })
76
+ })
77
+
78
+ it('routes progress tracking based on changed team/task snapshots', () => {
79
+ const value = session()
80
+ value.append('team/task' as never, { task: { id: 'task-1', revision: 1, subject: 'Backend API', status: 'in_progress' } } as never)
81
+ value.append('team/task' as never, { task: { id: 'task-1', revision: 2, subject: 'Backend API', status: 'completed' } } as never)
82
+ expect(semanticRouteHint(value.events)).toBe(true)
83
+ const decision = analyzeStructuredRoute(value.events)
84
+ expect(decision).toMatchObject({ kind: 'track', source: 'structured', reason: 'changed durable team tasks' })
85
+ })
86
+ })
87
+
88
+ describe('transactional router state', () => {
89
+ it('commits, requires final verification, and clears it only after final commit', () => {
90
+ const value = session(); const agent = { id: value.id, session: value }; const router = new AutoVerifierRouter()
91
+ const route = router.reserve(agent, 'compare', 'route', 4, policy)!
92
+ expect(router.commit(agent, route, 9)).toBe(true); expect(router.finalRequired(agent)).toBe(9)
93
+ const final = router.reserve(agent, 'final', 'final', 4, policy)!
94
+ expect(router.commit(agent, final)).toBe(true); expect(router.finalRequired(agent)).toBeUndefined()
95
+ })
96
+ it('releases in-flight state after failure and strict remains blocked', () => {
97
+ const value = session(); const agent = { id: value.id, session: value }; const router = new AutoVerifierRouter()
98
+ const first = router.reserve(agent, 'compare', 'route-a', 4, { ...policy, mode: 'strict' })!
99
+ router.fail(agent, first, true); expect(router.strictBlocked(agent)).toBe(true)
100
+ expect(router.reserve(agent, 'compare', 'route-b', 4, { ...policy, mode: 'strict' })).toBeDefined()
101
+ })
102
+ it('enforces manual mode and unified model-call budgets', () => {
103
+ const value = session(); const agent = { id: value.id, session: value }; const router = new AutoVerifierRouter()
104
+ expect(router.reserve(agent, 'compare', 'manual', 1, { ...policy, mode: 'manual' })).toBeUndefined()
105
+ expect(router.reserve(agent, 'select', 'expensive', 49, policy)).toBeUndefined()
106
+ })
107
+ })