@qvac/harness 1.0.0-rc.2 → 1.0.0-rc.3

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 (58) hide show
  1. package/README.md +12 -12
  2. package/bench/README.md +94 -0
  3. package/bench/bfcl/checker.ts +313 -0
  4. package/bench/bfcl/data.ts +93 -0
  5. package/bench/bfcl/observe.ts +93 -0
  6. package/bench/bfcl/run.ts +213 -0
  7. package/bench/bfcl/tools.ts +72 -0
  8. package/bench/bridge/bridge.ts +392 -0
  9. package/bench/bridge/openai.ts +150 -0
  10. package/bench/bridge/python/qvac_bench/__init__.py +3 -0
  11. package/bench/bridge/python/qvac_bench/airline.py +42 -0
  12. package/bench/bridge/python/qvac_bench/approval.py +26 -0
  13. package/bench/bridge/python/qvac_bench/bfcl_multi_turn.py +152 -0
  14. package/bench/bridge/python/qvac_bench/client.py +88 -0
  15. package/bench/bridge/python/qvac_bench/harbor_agent.py +85 -0
  16. package/bench/bridge/python/qvac_bench/session.py +107 -0
  17. package/bench/bridge/python/qvac_bench/speed.py +30 -0
  18. package/bench/bridge/python/qvac_bench/tau2_agent.py +106 -0
  19. package/bench/bridge/python/qvac_bench/tools.py +11 -0
  20. package/bench/bridge/python/run_bfcl_multi_turn.py +95 -0
  21. package/bench/bridge/python/run_tau2.py +150 -0
  22. package/bench/bridge/python/smoke.py +47 -0
  23. package/bench/bridge/python/test_airline.py +28 -0
  24. package/bench/bridge/python/test_approval.py +57 -0
  25. package/bench/bridge/python/test_bfcl_multi_turn.py +185 -0
  26. package/bench/bridge/python/test_harbor_agent.py +107 -0
  27. package/bench/bridge/serve.ts +52 -0
  28. package/bench/bridge/transcript.ts +52 -0
  29. package/bench/bun-tsconfig.json +15 -0
  30. package/bench/cpu-main.mjs +216 -0
  31. package/bench/cpu.ts +457 -0
  32. package/bench/evidence/2026-09-28-review15.md +225 -0
  33. package/bench/evidence/redact-bun1.3.14.json +92 -0
  34. package/bench/evidence/redact-node24.11.json +92 -0
  35. package/bench/graph-metrics.ts +43 -0
  36. package/bench/harness.ts +73 -0
  37. package/bench/issue129-matrix.mjs +722 -0
  38. package/bench/redact-adversarial.ts +256 -0
  39. package/bench/repeat-cpu.mjs +281 -0
  40. package/bench/report.ts +1075 -0
  41. package/bench/skill-eval/harness.ts +30 -0
  42. package/bench/skill-eval/laws.ts +37 -0
  43. package/bench/skill-eval/mermaid/client.ts +132 -0
  44. package/bench/skill-eval/mermaid/validate.mjs +115 -0
  45. package/bench/skill-eval/pack.ts +61 -0
  46. package/bench/skill-eval/packs/diagrams/cases.ts +281 -0
  47. package/bench/skill-eval/packs/diagrams/index.ts +50 -0
  48. package/bench/skill-eval/packs/diagrams/score.ts +74 -0
  49. package/bench/skill-eval/packs/index.ts +5 -0
  50. package/bench/skill-eval/packs/weather/cases.ts +279 -0
  51. package/bench/skill-eval/packs/weather/fixtures/wttr.json +566 -0
  52. package/bench/skill-eval/packs/weather/fixtures.ts +84 -0
  53. package/bench/skill-eval/packs/weather/index.ts +55 -0
  54. package/bench/skill-eval/packs/weather/score.ts +92 -0
  55. package/bench/skill-eval/report.ts +194 -0
  56. package/bench/skill-eval/run.ts +316 -0
  57. package/bench/speed.ts +80 -0
  58. package/package.json +4 -13
@@ -0,0 +1,30 @@
1
+ import process from 'process'
2
+ import { Harness } from '../../index.ts'
3
+ import type { ToolDefinition } from '../../lib/contract/index.ts'
4
+ import { createSkillTool } from '../../lib/tools/skill.ts'
5
+ import { configureSdkStorage } from '../../lib/sdk-storage.ts'
6
+ import { MODEL_CACHE_PATH_ENV, resolveStorage } from '../../lib/storage.ts'
7
+
8
+ export interface EvalHarnessOptions {
9
+ readonly root: string
10
+ readonly skillsDir: string
11
+ readonly tools: ReadonlyArray<ToolDefinition>
12
+ readonly ctxSize: number
13
+ }
14
+
15
+ // storage first: the skill tool captures the overlay dir from QVAC_CONFIG_PATH when it is built
16
+ export async function openEvalHarness(options: EvalHarnessOptions): Promise<Harness> {
17
+ const storage = resolveStorage('dev', {
18
+ root: options.root,
19
+ models: process.env[MODEL_CACHE_PATH_ENV]
20
+ })
21
+ configureSdkStorage(storage.sdk)
22
+
23
+ const { skillsDir } = options
24
+ const tools = [...options.tools, createSkillTool({ skillsDir })]
25
+ const harness = new Harness({ tools, skillsDir, contextSize: options.ctxSize })
26
+ await harness.ready()
27
+ await harness.clearKvCache()
28
+
29
+ return harness
30
+ }
@@ -0,0 +1,37 @@
1
+ import { check, type Laws } from 'asenion'
2
+
3
+ // asenion laws compare on whole words; the regex lists cover what a word law cannot say
4
+ export interface ReplyLaws {
5
+ readonly laws?: Laws | undefined
6
+ readonly all?: ReadonlyArray<RegExp> | undefined
7
+ readonly none?: ReadonlyArray<RegExp> | undefined
8
+ }
9
+
10
+ export function replyFailures(reply: string, laws: ReplyLaws | undefined): string[] {
11
+ if (reply.trim() === '') {
12
+ return ['reply: empty']
13
+ }
14
+
15
+ const failures: string[] = []
16
+ if (laws?.laws) {
17
+ for (const failure of check(reply, laws.laws).failures) {
18
+ failures.push(
19
+ `${failure.rule}${failure.term === undefined ? '' : ` "${failure.term}"`}: ${failure.detail}`
20
+ )
21
+ }
22
+ }
23
+
24
+ for (const pattern of laws?.all ?? []) {
25
+ if (!pattern.test(reply)) {
26
+ failures.push(`missing: ${pattern.source}`)
27
+ }
28
+ }
29
+
30
+ for (const pattern of laws?.none ?? []) {
31
+ if (pattern.test(reply)) {
32
+ failures.push(`forbidden: ${pattern.source}`)
33
+ }
34
+ }
35
+
36
+ return failures
37
+ }
@@ -0,0 +1,132 @@
1
+ import { spawn } from 'child_process'
2
+ import { fileURLToPath } from 'url'
3
+
4
+ const SIDECAR = fileURLToPath(new URL('./validate.mjs', import.meta.url))
5
+
6
+ // mermaid needs a DOM, so the parse runs under node even when the sweep runs under Bare
7
+ const NODE = 'node'
8
+
9
+ // a parse takes milliseconds: a request this old means the sidecar is gone or wedged
10
+ const PARSE_TIMEOUT_MS = 30_000
11
+
12
+ export interface Verdict {
13
+ readonly ok: boolean
14
+ readonly message?: string | undefined
15
+ }
16
+
17
+ export interface MermaidOracle {
18
+ // the version the sidecar loaded, so a sweep's summary records what judged it
19
+ readonly version: Promise<string>
20
+ parse(code: string): Promise<Verdict>
21
+ close(): void
22
+ }
23
+
24
+ interface Waiting {
25
+ resolve(verdict: Verdict): void
26
+ reject(reason: Error): void
27
+ }
28
+
29
+ interface SidecarMessage {
30
+ ready?: string
31
+ id?: number
32
+ ok?: boolean
33
+ message?: string
34
+ }
35
+
36
+ // one sidecar per sweep: mermaid takes ~1s to load; a dead one fails every request loudly
37
+ export function openMermaidOracle(): MermaidOracle {
38
+ const child = spawn(NODE, [SIDECAR], { stdio: ['pipe', 'pipe', 'inherit'] })
39
+ const waiting = new Map<number, Waiting>()
40
+ let ready: (version: string) => void
41
+ let notReady: (reason: Error) => void
42
+ const version = new Promise<string>((resolve, reject) => {
43
+ ready = resolve
44
+ notReady = reject
45
+ })
46
+ version.catch(noop)
47
+ let failure: Error | null = null
48
+ let closed = false
49
+ let next = 0
50
+ let pending = ''
51
+
52
+ child.stdout.setEncoding('utf8')
53
+ child.stdout.on('data', (chunk: string) => {
54
+ pending += chunk
55
+ for (;;) {
56
+ const newline = pending.indexOf('\n')
57
+ if (newline === -1) {
58
+ return
59
+ }
60
+
61
+ const line = pending.slice(0, newline)
62
+ pending = pending.slice(newline + 1)
63
+ if (line.trim()) {
64
+ receive(JSON.parse(line))
65
+ }
66
+ }
67
+ })
68
+ child.on('error', (err: Error) => fail(`mermaid oracle could not start node: ${err.message}`))
69
+ child.on('exit', (code: number | null, signal: string | null) => {
70
+ if (!closed) {
71
+ fail(`mermaid oracle exited (${code ?? signal})`)
72
+ }
73
+ })
74
+
75
+ function fail(why: string): void {
76
+ failure = new Error(why)
77
+ notReady(failure)
78
+ for (const entry of waiting.values()) {
79
+ entry.reject(failure)
80
+ }
81
+
82
+ waiting.clear()
83
+ }
84
+
85
+ function receive(message: SidecarMessage): void {
86
+ if (message.ready !== undefined) {
87
+ return ready(message.ready)
88
+ }
89
+
90
+ if (message.id === undefined) return
91
+ const entry = waiting.get(message.id)
92
+ waiting.delete(message.id)
93
+ entry?.resolve({ ok: message.ok === true, message: message.message })
94
+ }
95
+
96
+ return {
97
+ version,
98
+ parse(code) {
99
+ if (failure) {
100
+ return Promise.reject(failure)
101
+ }
102
+
103
+ const id = next++
104
+
105
+ return new Promise<Verdict>((resolve, reject) => {
106
+ const timer = setTimeout(() => {
107
+ waiting.delete(id)
108
+ reject(new Error(`mermaid oracle did not answer request ${id} in ${PARSE_TIMEOUT_MS}ms`))
109
+ }, PARSE_TIMEOUT_MS)
110
+ waiting.set(id, {
111
+ resolve: (verdict) => {
112
+ clearTimeout(timer)
113
+ resolve(verdict)
114
+ },
115
+ reject: (reason) => {
116
+ clearTimeout(timer)
117
+ reject(reason)
118
+ }
119
+ })
120
+ child.stdin.write(`${JSON.stringify({ id, code })}\n`)
121
+ })
122
+ },
123
+ close() {
124
+ closed = true
125
+ failure = new Error('mermaid oracle closed')
126
+ child.stdin.end()
127
+ child.kill()
128
+ }
129
+ }
130
+ }
131
+
132
+ function noop(): void {}
@@ -0,0 +1,115 @@
1
+ // the app's diagram acceptance test, out of process: mermaid needs a DOM and the eval runs on Bare
2
+ import { createRequire } from 'module'
3
+ import { JSDOM } from 'jsdom'
4
+
5
+ const MERMAID_CONFIG = {
6
+ startOnLoad: false,
7
+ securityLevel: 'strict',
8
+ suppressErrorRendering: true,
9
+ htmlLabels: false,
10
+ maxTextSize: 50000,
11
+ maxEdges: 500,
12
+ theme: 'base',
13
+ flowchart: { htmlLabels: false }
14
+ }
15
+
16
+ const DIRECTIVE_OPEN = '%%{'
17
+ const DIRECTIVE_CLOSE = '}%%'
18
+
19
+ const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', { pretendToBeVisual: true })
20
+ for (const key of ['window', 'document', 'Element', 'SVGElement', 'Node', 'HTMLElement']) {
21
+ const value = key === 'window' ? dom.window : dom.window[key]
22
+ Object.defineProperty(globalThis, key, { value, configurable: true, writable: true })
23
+ }
24
+
25
+ Object.defineProperty(globalThis, 'navigator', { value: dom.window.navigator, configurable: true })
26
+
27
+ const mermaid = (await import('mermaid')).default
28
+ mermaid.initialize(MERMAID_CONFIG)
29
+
30
+ const require = createRequire(import.meta.url)
31
+ send({ ready: require('mermaid/package.json').version })
32
+
33
+ let pending = ''
34
+ process.stdin.setEncoding('utf8')
35
+ process.stdin.on('data', (chunk) => {
36
+ pending += chunk
37
+ for (;;) {
38
+ const newline = pending.indexOf('\n')
39
+ if (newline === -1) {
40
+ return
41
+ }
42
+
43
+ const line = pending.slice(0, newline)
44
+ pending = pending.slice(newline + 1)
45
+ if (line.trim()) {
46
+ void answer(JSON.parse(line))
47
+ }
48
+ }
49
+ })
50
+
51
+ async function answer(request) {
52
+ send({ id: request.id, ...(await verdictFor(request.code)) })
53
+ }
54
+
55
+ async function verdictFor(code) {
56
+ try {
57
+ await mermaid.parse(sanitize(code))
58
+ return { ok: true }
59
+ } catch (error) {
60
+ // the first line is the diagnosis; mermaid echoes the whole block back after it
61
+ return { ok: false, message: firstLine(error) }
62
+ }
63
+ }
64
+
65
+ function firstLine(error) {
66
+ return String(error?.message ?? error)
67
+ .split('\n')[0]
68
+ .slice(0, 200)
69
+ }
70
+
71
+ function send(message) {
72
+ process.stdout.write(`${JSON.stringify(message)}\n`)
73
+ }
74
+
75
+ function sanitize(code) {
76
+ return stripFrontmatterConfig(stripDirectives(code))
77
+ }
78
+
79
+ function stripDirectives(code) {
80
+ let out = ''
81
+ let cursor = 0
82
+ for (;;) {
83
+ const start = code.indexOf(DIRECTIVE_OPEN, cursor)
84
+ if (start === -1) {
85
+ return out + code.slice(cursor)
86
+ }
87
+
88
+ const end = code.indexOf(DIRECTIVE_CLOSE, start + DIRECTIVE_OPEN.length)
89
+ if (end === -1) {
90
+ return out + code.slice(cursor)
91
+ }
92
+
93
+ out += code.slice(cursor, start)
94
+ cursor = end + DIRECTIVE_CLOSE.length
95
+ }
96
+ }
97
+
98
+ function stripFrontmatterConfig(code) {
99
+ const lines = code.split(/\r?\n/)
100
+ const open = lines.findIndex((line) => line.trim() !== '')
101
+ if (open === -1 || lines[open] !== '---') {
102
+ return code
103
+ }
104
+
105
+ const close = lines.indexOf('---', open + 1)
106
+ if (close === -1) {
107
+ return code
108
+ }
109
+
110
+ const hasConfig = lines
111
+ .slice(open + 1, close)
112
+ .some((line) => /^config\s*:/.test(line.trimStart()))
113
+
114
+ return hasConfig ? lines.slice(close + 1).join('\n') : code
115
+ }
@@ -0,0 +1,61 @@
1
+ import type { Message, ToolDefinition } from '../../lib/contract/index.ts'
2
+ import type { FetchFn } from '../../lib/tools/http-request.ts'
3
+
4
+ export interface EvalCase<Expect> {
5
+ readonly id: string
6
+ readonly category: string
7
+ readonly messages: ReadonlyArray<Message>
8
+ readonly expect: Expect
9
+ }
10
+
11
+ export interface ToolCall {
12
+ readonly name: string
13
+ readonly args: Record<string, unknown>
14
+ }
15
+
16
+ // what one run produced, before any pack-specific reading of it
17
+ export interface Observed {
18
+ readonly calls: ReadonlyArray<ToolCall>
19
+ readonly reply: string
20
+ }
21
+
22
+ export interface Judgement {
23
+ readonly failures: ReadonlyArray<string>
24
+ // the fake world could not answer a correct call: the row says nothing about the model
25
+ readonly gap: boolean
26
+ // every diagram in the reply parsed; null when the reply carried none to judge
27
+ readonly renders?: boolean | null | undefined
28
+ // the user-facing result when a pack defines one apart from its laws: the reply did the job
29
+ readonly outcome?: boolean | undefined
30
+ }
31
+
32
+ // the fake tools a pack stands up around the model, and the verdict they let it reach
33
+ export interface PackWorld<Expect> {
34
+ readonly tools: ReadonlyArray<ToolDefinition>
35
+ // called before every run so per-run traces start empty
36
+ reset(): void
37
+ judge(evalCase: EvalCase<Expect>, observed: Observed): Judgement | Promise<Judgement>
38
+ // persists anything --record captured
39
+ save(): void
40
+ // releases whatever world() opened, once the sweep is done
41
+ close?(): void
42
+ }
43
+
44
+ export interface WorldOptions {
45
+ // present only under --record: a pack may fetch what its fixtures lack
46
+ readonly live?: FetchFn | undefined
47
+ }
48
+
49
+ export interface SkillPack<Expect> {
50
+ // the skill directory name; also the @mention and the overlay slot variants are written to
51
+ readonly skill: string
52
+ // one case reported on its own column, usually the ticket's prompt
53
+ readonly focus: string
54
+ readonly cases: ReadonlyArray<EvalCase<Expect>>
55
+ world(options: WorldOptions): PackWorld<Expect>
56
+ }
57
+
58
+ export type AnyPack = SkillPack<unknown>
59
+
60
+ export const user = (content: string): Message => ({ role: 'user', content })
61
+ export const assistant = (content: string): Message => ({ role: 'assistant', content })
@@ -0,0 +1,281 @@
1
+ import { skillLoadedTurn } from '../../../../lib/capabilities/tool-turns.ts'
2
+ import type { ReplyLaws } from '../../laws.ts'
3
+ import { assistant, user, type EvalCase as Case } from '../../pack.ts'
4
+
5
+ export const CATEGORIES = [
6
+ 'flowchart',
7
+ 'sequence',
8
+ 'state',
9
+ 'er',
10
+ 'class',
11
+ 'gantt',
12
+ 'pie',
13
+ 'mindmap',
14
+ 'timeline',
15
+ 'hard-label',
16
+ 'iterate',
17
+ 'route-away',
18
+ 'long-context'
19
+ ] as const
20
+
21
+ export type Category = (typeof CATEGORIES)[number]
22
+
23
+ export interface Expect {
24
+ // the Mermaid keyword the first line inside the fence must be; '' when no diagram is wanted
25
+ readonly type: string
26
+ readonly blocks: number | readonly [number, number]
27
+ readonly reply?: ReplyLaws | undefined
28
+ }
29
+
30
+ export type EvalCase = Case<Expect>
31
+
32
+ export const SKILL = 'diagrams'
33
+ // QVAC-24436's own prompt, reported verbatim
34
+ export const TICKET_CASE = 'vegetable-inventory-state'
35
+
36
+ const FENCE_FIRST = /^```mermaid/
37
+ const ASCII_ART = /^[ \t]*[+|][-+|_ ]{4,}/m
38
+ const CANNOT = /\b(?:cannot|can't|unable to|I'm sorry|sorry,|as a text)\b/i
39
+ // QVAC-24436's second symptom: the model's own framing printed as the answer
40
+ export const LEAKED_TOOL = /"arguments"\s*:|rag_search|"error"\s*:|<tool_call>/
41
+ const STYLING = /%%\{|classDef|^\s*style\s/m
42
+
43
+ // every diagram case wants the same reply shape, so the laws live in one place
44
+ const DRAWN: ReplyLaws = { all: [FENCE_FIRST], none: [ASCII_ART, CANNOT, LEAKED_TOOL, STYLING] }
45
+ const NOT_DRAWN: ReplyLaws = { none: [/```mermaid/, LEAKED_TOOL] }
46
+
47
+ interface Drawn {
48
+ readonly category: Category
49
+ readonly prompt: string
50
+ readonly type: string
51
+ }
52
+
53
+ const draws = (id: string, spec: Drawn): EvalCase => ({
54
+ id,
55
+ category: spec.category,
56
+ messages: [user(spec.prompt)],
57
+ expect: { type: spec.type, blocks: 1, reply: DRAWN }
58
+ })
59
+
60
+ const FLOW = { type: 'flowchart' } as const
61
+ const STATE = { type: 'stateDiagram-v2' } as const
62
+
63
+ // QVAC-24502: the doc is already in the window, so the skill body and answer share what is left
64
+ const ONBOARDING_DOC = [
65
+ 'ACME onboarding runbook, revision 7.',
66
+ '',
67
+ 'A new hire is created in the HR system by the people team. That record syncs to the',
68
+ 'directory overnight. The following morning IT receives a ticket, provisions a laptop from',
69
+ 'stock, and images it with the standard build. The manager is emailed a checklist and must',
70
+ 'confirm the desk assignment before the laptop is released. On day one the hire collects',
71
+ 'the laptop from the front desk, signs the equipment register, and sets their password at',
72
+ 'first login. Access to production systems is not granted at this point: the hire completes',
73
+ 'security training first, which takes two days, and their manager then requests elevation.',
74
+ 'The security team reviews the request against the role matrix and either grants access,',
75
+ 'asks for more detail, or refuses it. A refusal returns to the manager with a reason.',
76
+ 'Once access is granted the hire is added to the on-call rota after their first month.'
77
+ ].join('\n')
78
+
79
+ export const CASES: ReadonlyArray<EvalCase> = [
80
+ // the ticket prompt, and the type it names
81
+ draws(TICKET_CASE, {
82
+ category: 'state',
83
+ prompt: 'Create a state diagram for vegetable inventory',
84
+ ...STATE
85
+ }),
86
+ draws('order-states', {
87
+ category: 'state',
88
+ prompt: 'show me the states an order goes through from checkout to delivery',
89
+ ...STATE
90
+ }),
91
+ draws('traffic-light', {
92
+ category: 'state',
93
+ prompt: 'diagram of a traffic light and how it changes',
94
+ ...STATE
95
+ }),
96
+
97
+ draws('coffee-flow', {
98
+ category: 'flowchart',
99
+ prompt: 'draw a flowchart of making coffee',
100
+ ...FLOW
101
+ }),
102
+ draws('login-flow', {
103
+ category: 'flowchart',
104
+ prompt: 'map out how a user logs in, including the failure case',
105
+ ...FLOW
106
+ }),
107
+ draws('deploy-pipeline', {
108
+ category: 'flowchart',
109
+ prompt: 'can you sketch our deploy pipeline left to right',
110
+ ...FLOW
111
+ }),
112
+ draws('support-triage', {
113
+ category: 'flowchart',
114
+ prompt: 'visualize how a support ticket gets triaged',
115
+ ...FLOW
116
+ }),
117
+ draws('org-chart', {
118
+ category: 'flowchart',
119
+ prompt: 'draw the reporting lines for a team of eight',
120
+ ...FLOW
121
+ }),
122
+
123
+ draws('rest-call', {
124
+ category: 'sequence',
125
+ prompt: 'show who calls whom when the app fetches a profile from the API',
126
+ type: 'sequenceDiagram'
127
+ }),
128
+ draws('oauth-handshake', {
129
+ category: 'sequence',
130
+ prompt: 'diagram the oauth handshake between browser, our server and the provider',
131
+ type: 'sequenceDiagram'
132
+ }),
133
+
134
+ draws('shop-schema', {
135
+ category: 'er',
136
+ prompt: 'draw the tables for a shop: customers, orders, line items',
137
+ type: 'erDiagram'
138
+ }),
139
+ draws('blog-schema', {
140
+ category: 'er',
141
+ prompt: 'diagram the relationships between posts, authors and comments',
142
+ type: 'erDiagram'
143
+ }),
144
+
145
+ draws('animal-classes', {
146
+ category: 'class',
147
+ prompt: 'show the class hierarchy for animals, dogs and cats',
148
+ type: 'classDiagram'
149
+ }),
150
+ draws('payment-classes', {
151
+ category: 'class',
152
+ prompt: 'diagram the payment classes and what they inherit from',
153
+ type: 'classDiagram'
154
+ }),
155
+
156
+ draws('sprint-plan', {
157
+ category: 'gantt',
158
+ prompt: 'give me a schedule for a three week sprint: design, build, test, release',
159
+ type: 'gantt'
160
+ }),
161
+ draws('house-move', {
162
+ category: 'gantt',
163
+ prompt: 'plan my house move over six weeks with durations',
164
+ type: 'gantt'
165
+ }),
166
+
167
+ draws('browser-share', {
168
+ category: 'pie',
169
+ prompt: 'chart the browser split: chrome 65, firefox 20, safari 15',
170
+ type: 'pie'
171
+ }),
172
+ draws('budget-split', {
173
+ category: 'pie',
174
+ prompt: 'show what share of my budget goes to rent, food and savings',
175
+ type: 'pie'
176
+ }),
177
+
178
+ draws('pm-mindmap', {
179
+ category: 'mindmap',
180
+ prompt: 'brainstorm project management as a mindmap',
181
+ type: 'mindmap'
182
+ }),
183
+ draws('launch-ideas', {
184
+ category: 'mindmap',
185
+ prompt: 'mindmap the ideas around our product launch',
186
+ type: 'mindmap'
187
+ }),
188
+
189
+ draws('apollo-timeline', {
190
+ category: 'timeline',
191
+ prompt: 'timeline of the apollo program from 1961 to 1972',
192
+ type: 'timeline'
193
+ }),
194
+ draws('company-history', {
195
+ category: 'timeline',
196
+ prompt: 'show the dated milestones of the company in order',
197
+ type: 'timeline'
198
+ }),
199
+
200
+ // the three failures the repair prompt names, in prompts whose natural labels contain them
201
+ draws('http-retry', {
202
+ category: 'hard-label',
203
+ prompt: 'draw how we send a request (HTTP) and retry it on failure',
204
+ ...FLOW
205
+ }),
206
+ draws('finish-step', {
207
+ category: 'hard-label',
208
+ prompt: 'flowchart: start, do the work, end',
209
+ ...FLOW
210
+ }),
211
+ draws('ratio-step', {
212
+ category: 'hard-label',
213
+ prompt: 'diagram a step that checks the ratio 2:1 before continuing',
214
+ ...FLOW
215
+ }),
216
+ draws('quoted-stage', {
217
+ category: 'hard-label',
218
+ prompt: 'draw a pipeline where one stage is called "review & sign-off"',
219
+ ...FLOW
220
+ }),
221
+
222
+ // a diagram already drawn, then an edit: the model must redraw the whole block
223
+ {
224
+ id: 'add-2fa',
225
+ category: 'iterate',
226
+ messages: [
227
+ user('draw a flowchart of the login flow'),
228
+ { role: 'tool', content: skillLoadedTurn(SKILL) },
229
+ assistant(
230
+ '```mermaid\nflowchart TD\n A["User submits credentials"] --> B{"Valid?"}\n B -->|yes| C["Start session"]\n B -->|no| D["Show error"]\n```\n\nThe login flow.'
231
+ ),
232
+ user('add a two-factor step between validating and starting the session')
233
+ ],
234
+ expect: { type: 'flowchart', blocks: 1, reply: DRAWN }
235
+ },
236
+ {
237
+ id: 'turn-horizontal',
238
+ category: 'iterate',
239
+ messages: [
240
+ user('draw a flowchart of making coffee'),
241
+ { role: 'tool', content: skillLoadedTurn(SKILL) },
242
+ assistant(
243
+ '```mermaid\nflowchart TD\n A["Boil water"] --> B["Add grounds"]\n B --> C["Brew"]\n C --> D["Pour"]\n```\n\nMaking coffee.'
244
+ ),
245
+ user('same thing but horizontal please')
246
+ ],
247
+ expect: { type: 'flowchart', blocks: 1, reply: DRAWN }
248
+ },
249
+
250
+ // the control every category needs: asks a diagram must NOT answer with a fence
251
+ {
252
+ id: 'onboarding-from-doc',
253
+ category: 'long-context',
254
+ messages: [user(`${ONBOARDING_DOC}\n\nGenerate a state diagram based on the doc above.`)],
255
+ expect: { type: 'stateDiagram-v2', blocks: 1, reply: DRAWN }
256
+ },
257
+ {
258
+ id: 'onboarding-flow-from-doc',
259
+ category: 'long-context',
260
+ messages: [user(`${ONBOARDING_DOC}\n\nDraw the onboarding process above as a flowchart.`)],
261
+ expect: { type: 'flowchart', blocks: 1, reply: DRAWN }
262
+ },
263
+ {
264
+ id: 'logo-request',
265
+ category: 'route-away',
266
+ messages: [user('design a logo for my coffee shop')],
267
+ expect: { type: '', blocks: 0, reply: NOT_DRAWN }
268
+ },
269
+ {
270
+ id: 'photo-request',
271
+ category: 'route-away',
272
+ messages: [user('make me a picture of a mountain at sunset')],
273
+ expect: { type: '', blocks: 0, reply: NOT_DRAWN }
274
+ },
275
+ {
276
+ id: 'revenue-plot',
277
+ category: 'route-away',
278
+ messages: [user('plot our monthly revenue for the last year')],
279
+ expect: { type: '', blocks: 0, reply: NOT_DRAWN }
280
+ }
281
+ ]