dsh-vision-router 2.1.2 → 2.1.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.
@@ -20,9 +20,7 @@ const MAX_TURN_BUDGET_MS = 600_000
20
20
  const MAX_GUIDANCE_OVERRIDES = 14
21
21
  const MAX_GUIDANCE_CHARS = 2_000
22
22
  const MAX_NON_PROGRESS_ATTEMPTS = 3
23
- const MAX_EVIDENCE_SIGNATURE_CHARS = 4_096
24
23
  const BUDGETED_TIMEOUT_FIELDS = ['timeoutMs', 'visionTaskTimeoutMs', 'ocrTimeoutMs']
25
- const EVIDENCE_META_KEYS = new Set(['ok', 'code', 'retryable', 'reason', 'phase', 'next'])
26
24
  const GUIDANCE_KINDS = new Set([
27
25
  'code',
28
26
  'document',
@@ -40,25 +38,6 @@ const GUIDANCE_KINDS = new Set([
40
38
  'meme',
41
39
  ])
42
40
 
43
- const BUILTIN_MIXED_GUIDANCE = {
44
- zh: {
45
- document: '语义优先;只有逐字引用、合同/表单字段或表格数字确需保真时才使用 OCR。',
46
- ui: '优先用 vision_detect / vision_ground 验证元素、状态与位置。',
47
- code: '逐字核对代码;对可执行字符、缩进和符号做交叉验证。',
48
- chat: '按气泡顺序核对发言者、消息内容与时间/状态信息。',
49
- general: '聚焦尚未归类的另一部分可见内容,自行选择最能新增证据的视觉工具。',
50
- fallback: '聚焦这个分支新增或验证可见证据。',
51
- },
52
- en: {
53
- document: 'Prioritize semantics; use OCR only when verbatim quotes, contract/form fields, or table numbers require exact transcription.',
54
- ui: 'Prefer vision_detect / vision_ground to verify elements, states, and positions.',
55
- code: 'Verify code verbatim; cross-check executable characters, indentation, and symbols.',
56
- chat: 'Verify speakers, message text, and time/status information in bubble order.',
57
- general: 'Focus on the remaining unclassified visible content and choose the vision tool that adds the most useful evidence.',
58
- fallback: 'Add or verify visible evidence for this branch.',
59
- },
60
- }
61
-
62
41
  function isObject(value) {
63
42
  return value !== null && typeof value === 'object'
64
43
  }
@@ -98,36 +77,143 @@ export function producedStructuredEvidence(value) {
98
77
  return true
99
78
  }
100
79
 
101
- function meaningfulEvidenceValue(value) {
102
- if (value === undefined || value === null) return false
103
- if (typeof value === 'string') return value.trim() !== ''
104
- if (typeof value === 'number') return Number.isFinite(value)
105
- if (typeof value === 'boolean') return true
106
- if (Array.isArray(value)) return value.length > 0 && value.some(meaningfulEvidenceValue)
107
- if (!isObject(value) || value.ok === false) return false
108
- return Object.entries(value).some(([key, child]) => (
109
- !EVIDENCE_META_KEYS.has(key) && meaningfulEvidenceValue(child)
80
+ function finiteNumber(value) {
81
+ return typeof value === 'number' && Number.isFinite(value)
82
+ }
83
+
84
+ function positiveInteger(value) {
85
+ return Number.isInteger(value) && value > 0
86
+ }
87
+
88
+ function nonEmptyText(value) {
89
+ return typeof value === 'string' && value.trim() !== ''
90
+ }
91
+
92
+ function canonicalBox(value, width, height) {
93
+ if (!isObject(value) || Array.isArray(value)) return false
94
+ const { x1, y1, x2, y2 } = value
95
+ if (![x1, y1, x2, y2].every(Number.isInteger)) return false
96
+ if (x1 < 0 || y1 < 0 || x2 <= x1 || y2 <= y1) return false
97
+ if (positiveInteger(width) && x2 > width) return false
98
+ if (positiveInteger(height) && y2 > height) return false
99
+ return true
100
+ }
101
+
102
+ function structuredDescribeEvidence(value) {
103
+ if (!isObject(value) || Array.isArray(value)) return false
104
+ if (nonEmptyText(value.summary) || nonEmptyText(value.text)) return true
105
+ if (Array.isArray(value.layout) && value.layout.some((row) => (
106
+ isObject(row) && nonEmptyText(row.region) && nonEmptyText(row.content)
107
+ ))) return true
108
+ return Array.isArray(value.entities) && value.entities.some((entity) => (
109
+ isObject(entity) && nonEmptyText(entity.type) && nonEmptyText(entity.label)
110
+ ))
111
+ }
112
+
113
+ function groundEvidence(value) {
114
+ if (!isObject(value) || Array.isArray(value)) return false
115
+ const { width, height } = value
116
+ return positiveInteger(width) && positiveInteger(height) && canonicalBox(value, width, height)
117
+ }
118
+
119
+ function detectEvidence(value) {
120
+ if (!isObject(value) || Array.isArray(value)) return false
121
+ const { width, height, elements } = value
122
+ if (!positiveInteger(width) || !positiveInteger(height) || !Array.isArray(elements)) return false
123
+ return elements.every((element, index) => (
124
+ isObject(element) &&
125
+ element.number === index + 1 &&
126
+ nonEmptyText(element.label) &&
127
+ canonicalBox(element.box, width, height)
110
128
  ))
111
129
  }
112
130
 
131
+ function ocrEvidence(value) {
132
+ if (!isObject(value) || Array.isArray(value)) return false
133
+ if (value.engine !== 'tesseract' && value.engine !== 'vision') return false
134
+ return typeof value.text === 'string'
135
+ }
136
+
137
+ function colorEvidence(value) {
138
+ if (!Array.isArray(value)) return false
139
+ return value.every((entry) => (
140
+ isObject(entry) &&
141
+ typeof entry.hex === 'string' && /^#[0-9a-f]{6}$/i.test(entry.hex) &&
142
+ positiveInteger(entry.count) &&
143
+ finiteNumber(entry.share) && entry.share > 0 && entry.share <= 1
144
+ ))
145
+ }
146
+
147
+ function pixelDiffEvidence(value) {
148
+ if (!isObject(value) || Array.isArray(value)) return false
149
+ const { width, height, differingPixels, totalPixels, diffRatio, worstRegions } = value
150
+ if (!positiveInteger(width) || !positiveInteger(height) || totalPixels !== width * height) return false
151
+ if (!Number.isInteger(differingPixels) || differingPixels < 0 || differingPixels > totalPixels) return false
152
+ if (!finiteNumber(diffRatio) || diffRatio < 0 || diffRatio > 1) return false
153
+ if (Math.abs(diffRatio - differingPixels / totalPixels) > 0.000051) return false
154
+ if (!Array.isArray(worstRegions)) return false
155
+ return worstRegions.every((region) => (
156
+ isObject(region) &&
157
+ canonicalBox(region, width, height) &&
158
+ Number.isInteger(region.differing) && region.differing > 0 &&
159
+ positiveInteger(region.total) && region.differing <= region.total &&
160
+ finiteNumber(region.ratio) && region.ratio > 0 && region.ratio <= 1
161
+ ))
162
+ }
163
+
164
+ function longOcrEvidence(value) {
165
+ if (!isObject(value) || Array.isArray(value)) return false
166
+ if (typeof value.text !== 'string' || !positiveInteger(value.chunks)) return false
167
+ if (!isObject(value.engines) || Array.isArray(value.engines)) return false
168
+ if (![value.markdownPath, value.manifestPath, value.artifactsDir].every(nonEmptyText)) return false
169
+ const engineCounts = Object.values(value.engines)
170
+ if (engineCounts.length === 0 || !engineCounts.every((count) => Number.isInteger(count) && count >= 0)) return false
171
+ if (engineCounts.reduce((sum, count) => sum + count, 0) !== value.chunks) return false
172
+ if (nonEmptyText(value.text)) return true
173
+ const completed = (value.engines.tesseract ?? 0) + (value.engines.vision ?? 0)
174
+ return completed > 0
175
+ }
176
+
113
177
  /**
114
- * Evidence that is strong enough to advance the 1+x flow and consume an
115
- * explicit deep-dive call cap. Empty arrays/objects and metadata-only success
116
- * envelopes do not count. Negative facts such as { match: false } or
117
- * { count: 0 } still count because they are real observations, not emptiness.
178
+ * Decide whether one registered evidence tool completed a real observation.
179
+ *
180
+ * This deliberately follows each tool's output contract instead of treating
181
+ * arbitrary non-empty containers as evidence. Valid negative observations
182
+ * (for example zero detections, empty OCR text, or a 0 pixel diff) still count;
183
+ * transport failures, malformed envelopes, and describe's empty-content
184
+ * sentinel do not.
118
185
  */
119
- export function producedUsableStructuredEvidence(value) {
120
- if (value === undefined || value === null) return false
121
- if (typeof value === 'string') {
122
- const text = value.trim()
123
- if (text === '') return false
124
- const parsed = parseJson(text)
125
- if (parsed === undefined) return true
126
- return producedUsableStructuredEvidence(parsed)
186
+ export function producedUsableToolEvidence(toolName, value, args = {}) {
187
+ if (!STRUCTURED_EVIDENCE_TOOLS.has(toolName) || value === undefined || value === null) return false
188
+ const parsed = parseJson(value)
189
+ if (isObject(parsed) && !Array.isArray(parsed) && parsed.ok === false) return false
190
+
191
+ switch (toolName) {
192
+ case 'vision_describe': {
193
+ if (typeof value !== 'string') return false
194
+ const text = value.trim()
195
+ if (text === '' || text === '(the vision model returned empty content)') return false
196
+ if (args?.json === true) {
197
+ if (text.startsWith('vision_describe: the model did not produce valid JSON.')) return false
198
+ return structuredDescribeEvidence(parsed)
199
+ }
200
+ return true
201
+ }
202
+ case 'vision_ground':
203
+ return groundEvidence(parsed)
204
+ case 'vision_detect':
205
+ return detectEvidence(parsed)
206
+ case 'vision_ocr':
207
+ return ocrEvidence(parsed)
208
+ case 'vision_colors':
209
+ return colorEvidence(parsed)
210
+ case 'vision_pixel_diff':
211
+ return pixelDiffEvidence(parsed)
212
+ case 'vision_long_screenshot_ocr':
213
+ return longOcrEvidence(parsed)
214
+ default:
215
+ return false
127
216
  }
128
- if (Array.isArray(value)) return value.length > 0 && value.some(meaningfulEvidenceValue)
129
- if (!isObject(value) || value.ok === false) return false
130
- return meaningfulEvidenceValue(value)
131
217
  }
132
218
 
133
219
  /**
@@ -176,15 +262,6 @@ function languageOf(config) {
176
262
  return runtimeLanguageFor(config?.__visionRouterLocale, 'zh')
177
263
  }
178
264
 
179
- function guidanceFor(kind, overrides, language = 'zh') {
180
- for (let i = overrides.length - 1; i >= 0; i--) {
181
- const entry = overrides[i]
182
- if (entry.kind === kind) return entry.text
183
- }
184
- const copy = BUILTIN_MIXED_GUIDANCE[language] ?? BUILTIN_MIXED_GUIDANCE.zh
185
- return copy[kind] ?? copy.fallback
186
- }
187
-
188
265
  function turnBudgetMs(config) {
189
266
  const value = Number(config?.visionTurnBudgetMs)
190
267
  if (!Number.isFinite(value) || value <= 0) return DEFAULT_TURN_BUDGET_MS
@@ -234,9 +311,7 @@ function freshState(turn, _config) {
234
311
  bootstrapDone: false,
235
312
  bootstrapResult: undefined,
236
313
  successfulEvidenceCalls: 0,
237
- mixedBranches: [],
238
- completedBranches: new Set(),
239
- mixedAttemptSignatures: new Set(),
314
+ postBootstrapEvidenceCalls: 0,
240
315
  consecutiveNonProgressAttempts: 0,
241
316
  budgetExhausted: false,
242
317
  depthExhausted: false,
@@ -284,56 +359,13 @@ function bootstrapEvidence(raw) {
284
359
  return parsed.evidence
285
360
  }
286
361
 
287
- function normalizeMixedBranches(evidence, _depth) {
288
- if (!isObject(evidence) || evidence.visual_kind !== 'mixed' || !Array.isArray(evidence.mixed_of)) return []
289
- const allowed = ['ui', 'document', 'code', 'chat', 'general']
290
- const unique = []
291
- for (const kind of allowed) {
292
- if (evidence.mixed_of.includes(kind)) unique.push(kind)
293
- }
294
- return unique.slice(0, 2)
295
- }
296
-
297
- function evidenceAttemptSignature(name, args) {
298
- let encoded
299
- try {
300
- encoded = JSON.stringify(args ?? null)
301
- } catch {
302
- encoded = String(args ?? '')
303
- }
304
- if (encoded.length <= MAX_EVIDENCE_SIGNATURE_CHARS) return `${name}:${encoded}`
305
- const half = Math.floor(MAX_EVIDENCE_SIGNATURE_CHARS / 2)
306
- return `${name}:${encoded.length}:${encoded.slice(0, half)}:${encoded.slice(-half)}`
307
- }
308
-
309
- function inferBranchForTool(name, pending) {
310
- const prefer = (kinds) => kinds.find((kind) => pending.includes(kind))
311
- if (name === 'vision_detect' || name === 'vision_ground') return prefer(['ui', 'general'])
312
- if (name === 'vision_long_screenshot_ocr') return prefer(['document', 'chat', 'code', 'general'])
313
- if (name === 'vision_ocr') return prefer(['document', 'code', 'chat', 'general'])
314
- if (name === 'vision_colors' || name === 'vision_pixel_diff') return prefer(['general', 'ui'])
315
- return pending.length === 1 ? pending[0] : undefined
316
- }
317
-
318
- function recordEvidenceSuccess(state, name, args) {
319
- state.successfulEvidenceCalls += 1
320
- if (state.mixedBranches.length === 0) return true
321
- const pending = state.mixedBranches.filter((kind) => !state.completedBranches.has(kind))
322
- if (pending.length === 0) return true
323
- const signature = evidenceAttemptSignature(name, args)
324
- if (state.mixedAttemptSignatures.has(signature)) return false
325
- state.mixedAttemptSignatures.add(signature)
326
- const branch = inferBranchForTool(name, pending)
327
- if (!branch) return false
328
- state.completedBranches.add(branch)
329
- return true
330
- }
331
-
362
+ /**
363
+ * Structured 1+x has exactly one hard completion condition after bootstrap:
364
+ * at least one usable task-directed evidence observation. Mixed classifications
365
+ * remain model-visible advisory evidence; they do not create branch quotas.
366
+ */
332
367
  function missingEvidence(state) {
333
- if (state.mixedBranches.length > 0) {
334
- return state.mixedBranches.filter((kind) => !state.completedBranches.has(kind)).length
335
- }
336
- return state.successfulEvidenceCalls >= 1 ? 0 : 1
368
+ return state.postBootstrapEvidenceCalls >= 1 ? 0 : 1
337
369
  }
338
370
 
339
371
  function hasCoreFollowup(messages) {
@@ -347,6 +379,8 @@ function withoutEvidenceFollowups(messages) {
347
379
  const id = message && typeof message.id === 'string' ? message.id : ''
348
380
  return !(
349
381
  id.includes('vision-router-structured-followup-') ||
382
+ // Strip historical Round-0 mixed hard guards if one is still present in
383
+ // a persisted/compatibility message surface. Round 1 never emits them.
350
384
  id.includes('vision-router-structured-mixed-guard-') ||
351
385
  id.includes('vision-router-structured-evidence-guard-')
352
386
  )
@@ -374,30 +408,8 @@ function appendSyntheticGuardOnce(decision, baseMessages, state, message, stripE
374
408
  }
375
409
 
376
410
  function rearmEvidenceGuardAfterAttempt(state) {
377
- const mixedPrefix = `vision-router-structured-mixed-guard-${state.turn}-`
378
411
  const evidenceId = `vision-router-structured-evidence-guard-${state.turn}`
379
- for (const id of state.emittedGuardIds) {
380
- if (id === evidenceId || id.startsWith(mixedPrefix)) state.emittedGuardIds.delete(id)
381
- }
382
- }
383
-
384
- function mixedReminder(state, config) {
385
- const language = languageOf(config)
386
- const overrides = normalizeGuidanceOverrides(config?.guidanceOverrides)
387
- const pending = state.mixedBranches.filter((kind) => !state.completedBranches.has(kind))
388
- const done = state.mixedBranches.filter((kind) => state.completedBranches.has(kind))
389
- const separator = language === 'en' ? ': ' : ':'
390
- const lines = pending.map((kind) => `- ${kind}${separator}${guidanceFor(kind, overrides, language)}`)
391
- if (language === 'en') {
392
- const doneText = done.length > 0 ? `Verified branches: ${done.join(' + ')}. ` : ''
393
- return `${doneText}This mixed image still has unverified branches: ${pending.join(' + ')}. ` +
394
- 'Focus each vision call on one unfinished branch; answer only after each branch has produced at least one useful piece of evidence.\n' +
395
- lines.join('\n')
396
- }
397
- const doneText = done.length > 0 ? `已验证分支:${done.join(' + ')}。` : ''
398
- return `${doneText}混合图片仍有未验证分支:${pending.join(' + ')}。` +
399
- '每次视觉调用只聚焦一个尚未完成的分支;这些分支分别产生至少一次有效证据后再作答。\n' +
400
- lines.join('\n')
412
+ state.emittedGuardIds.delete(evidenceId)
401
413
  }
402
414
 
403
415
  function appendGuardMessage(decision, payload, state, config) {
@@ -421,12 +433,12 @@ function appendGuardMessage(decision, payload, state, config) {
421
433
  ? 'The visual turn time budget is exhausted. Do not call more vision tools; answer from the evidence already collected and state any remaining uncertainty.'
422
434
  : state.depthExhausted
423
435
  ? 'The configured deep-dive call cap has been reached. Do not call more vision tools; answer from the evidence already collected and state any remaining uncertainty.'
424
- : 'Repeated follow-up vision attempts did not produce or advance usable evidence. Do not keep retrying vision tools; answer from the evidence already collected and state the remaining uncertainty.'
436
+ : 'Repeated follow-up vision attempts did not produce usable evidence. Do not keep retrying vision tools; answer from the evidence already collected and state the remaining uncertainty.'
425
437
  : state.budgetExhausted
426
438
  ? '本轮视觉总时间预算已耗尽。不要再调用视觉工具;请基于已经获得的证据作答,并明确仍存在的不确定性。'
427
439
  : state.depthExhausted
428
440
  ? '已达到本轮设置的深挖次数上限。不要再调用视觉工具;请基于已经获得的证据作答,并明确仍存在的不确定性。'
429
- : '连续多次后续视觉调用都没有产出或推进可用证据。不要继续重复调用视觉工具;请基于已经获得的证据作答,并明确仍存在的不确定性。',
441
+ : '连续多次后续视觉调用都没有产出可用证据。不要继续重复调用视觉工具;请基于已经获得的证据作答,并明确仍存在的不确定性。',
430
442
  }],
431
443
  source: { kind: 'plugin', plugin: 'dsh-vision-router' },
432
444
  }
@@ -435,16 +447,10 @@ function appendGuardMessage(decision, payload, state, config) {
435
447
 
436
448
  if (!state.bootstrapDone || missingEvidence(state) === 0) return decision
437
449
 
438
- if (state.mixedBranches.length > 0) {
439
- const message = {
440
- role: 'user',
441
- id: `vision-router-structured-mixed-guard-${state.turn}-${state.successfulEvidenceCalls}`,
442
- content: [{ type: 'text', text: mixedReminder(state, config) }],
443
- source: { kind: 'plugin', plugin: 'dsh-vision-router' },
444
- }
445
- return appendSyntheticGuardOnce(decision, baseMessages, state, message, true)
446
- }
447
-
450
+ // Core owns bootstrap ordering and its one-shot follow-up guidance. This
451
+ // outer boundary owns the hard x>=1 completion rule. If Core's guidance is
452
+ // already present, do not duplicate it; if Core's legacy presentation latch
453
+ // stopped emitting it after an empty/failed attempt, re-assert the hard guard.
448
454
  if (hasCoreFollowup(baseMessages)) return decision
449
455
  const message = {
450
456
  role: 'user',
@@ -452,8 +458,8 @@ function appendGuardMessage(decision, payload, state, config) {
452
458
  content: [{
453
459
  type: 'text',
454
460
  text: language === 'en'
455
- ? 'The structured bootstrap is complete, but the previous follow-up tool did not produce usable evidence. Call at least one more vision tool that can add or verify evidence, then answer after it succeeds.'
456
- : '结构化预识别已经完成,但上一条后续工具没有产出可用证据。请再调用至少一个能新增或验证证据的视觉工具,成功后再作答。',
461
+ ? 'The structured bootstrap is complete, but no usable task-directed visual evidence has been produced yet. Call at least one vision tool that can add or verify evidence, then answer after it succeeds.'
462
+ : '结构化预识别已经完成,但目前还没有产出可用的任务定向视觉证据。请至少调用一个能新增或验证证据的视觉工具,成功后再作答。',
457
463
  }],
458
464
  source: { kind: 'plugin', plugin: 'dsh-vision-router' },
459
465
  }
@@ -542,9 +548,11 @@ function wrapRegisteredTool(def, states, getConfig) {
542
548
  if (evidence !== undefined) {
543
549
  state.bootstrapDone = true
544
550
  state.bootstrapResult = result
545
- state.mixedBranches = normalizeMixedBranches(evidence, depthOf(getConfig()))
546
- state.completedBranches.clear()
547
- state.mixedAttemptSignatures.clear()
551
+ // Structured 1+x is explicitly ordered: x starts only after the
552
+ // bootstrap succeeds. Keep the all-turn successful-evidence count
553
+ // intact for the user's explicit call cap; post-bootstrap progress
554
+ // has its own counter so an early model call cannot satisfy x>=1.
555
+ state.postBootstrapEvidenceCalls = 0
548
556
  resetNonProgress(state)
549
557
  state.budgetExhausted = false
550
558
  refreshDepthExhaustion(state, getConfig())
@@ -565,7 +573,7 @@ function wrapRegisteredTool(def, states, getConfig) {
565
573
  if (state.bootstrapDone && missingEvidence(state) > 0 && state.nonProgressExhausted) {
566
574
  return failure(
567
575
  'VISION_NO_PROGRESS_LIMIT',
568
- `the structured follow-up stopped after ${MAX_NON_PROGRESS_ATTEMPTS} consecutive attempts without usable evidence progress`,
576
+ `the structured follow-up stopped after ${MAX_NON_PROGRESS_ATTEMPTS} consecutive attempts without usable evidence`,
569
577
  )
570
578
  }
571
579
 
@@ -577,18 +585,11 @@ function wrapRegisteredTool(def, states, getConfig) {
577
585
  )
578
586
  const code = resultCode(result)
579
587
  if (code === 'VISION_TURN_BUDGET_EXCEEDED') state.budgetExhausted = true
580
- if (producedUsableStructuredEvidence(result)) {
581
- const progressed = recordEvidenceSuccess(state, def.name, args)
588
+ if (producedUsableToolEvidence(def.name, result, args)) {
589
+ state.successfulEvidenceCalls += 1
590
+ if (state.bootstrapDone) state.postBootstrapEvidenceCalls += 1
582
591
  refreshDepthExhaustion(state, active)
583
- if (state.bootstrapDone && missingEvidence(state) > 0) {
584
- if (progressed) resetNonProgress(state)
585
- else {
586
- recordNonProgress(state)
587
- if (!state.nonProgressExhausted) rearmEvidenceGuardAfterAttempt(state)
588
- }
589
- } else {
590
- resetNonProgress(state)
591
- }
592
+ resetNonProgress(state)
592
593
  } else if (code !== 'VISION_TURN_BUDGET_EXCEEDED' && state.bootstrapDone && missingEvidence(state) > 0) {
593
594
  recordNonProgress(state)
594
595
  if (!state.nonProgressExhausted) rearmEvidenceGuardAfterAttempt(state)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vision-router",
3
- "version": "2.1.2",
3
+ "version": "2.1.3",
4
4
  "description": "Eyes for text-only DeepSeek Harness agents: built-in free vision chain (no key) + pixel-level vision tools (Q&A, grounding, crop, pixel diff, colors, OCR, SVG trace, cutout, screenshots). One-command install, no Python, image turns work like ordinary tool-calling turns.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -69,13 +69,13 @@
69
69
  "sharp": "^0.35.3"
70
70
  },
71
71
  "scripts": {
72
- "test": "node --test tests/adapter-prepare-call-compat.test.js tests/core.test.js tests/issue-374-delegated-call-config.test.js tests/grounding-coordinate-frame.test.js tests/grounding-coordinate-runtime.test.js tests/capability-advisory.test.js tests/vision-execution-policy.test.js tests/vision-backend-diagnostics.test.js tests/live-model-discovery.test.js tests/live-model-credential-resolution.test.js tests/vision-model-registry.test.js tests/live-model-client-stability.test.js tests/providers-persistence.test.js tests/provider-directory-fallback.test.js tests/model-catalog-policy.test.js tests/settings-ia-client-prelude.test.js tests/settings-ia-fj.test.js tests/settings-ia-acceptance-regression.test.js tests/settings-migration.test.js tests/settings-section-order.test.js tests/attachment-admission-policy.test.js tests/vision-resilience.test.js tests/vision-resilience-lifecycle.test.js tests/vision-breaker-readonly.test.js tests/alpha1-browser-lifecycle-integration.test.js tests/issue-367-remote-session-inject.test.js tests/client.test.js tests/issue-307-regression.test.js tests/issue-284-vision-mode-toggle.test.js tests/issue-284-vision-mode-hardening.test.js tests/issue-284-attachment-id-hint.test.js tests/issue-284-model-visibility.test.js tests/issue-284-vision-selection-effort.test.js tests/client-presentation-boundary.test.js tests/remote-settings-bridge.test.js tests/remote-settings-v2-contract.test.js tests/remote-settings-risk-confirmation.test.js tests/local-remote-settings-permission.test.js tests/rc6-real-settings-persistence.test.js tests/artifact-path-security.test.js tests/http-compat.test.js tests/catalog-corrections.test.js tests/update-check.test.js tests/update-check-signal-lifecycle.test.js tests/self-update.test.js tests/doctor.test.js tests/doctor-cli.test.js tests/legacy-session-repair.test.js tests/profile-pnpm-diagnostics.test.js tests/doctor-v2.test.js tests/doctor-cli-v2.test.js tests/doctor-runtime.test.js tests/dsh-host-capabilities.test.js tests/doctor-host-capabilities.test.js tests/session-repair-v2.test.js tests/bundle-defaults.test.js tests/file-logger.test.js tests/replay-delegation.test.js tests/twin-image-capability-fallback.test.js tests/twin-image-custom-wrapper.test.js tests/adversarial-hardening.test.js tests/vision-adversarial-hardening-v2.test.js tests/vision-capability-benchmark-visual-proof.test.js tests/wrapper-directory.test.js tests/logging-ui.test.js tests/manifest-dependencies.test.js tests/structured-bootstrap.test.js tests/structured-bootstrap-gate.test.js tests/local-ollama.test.js tests/ollama-cold-start.test.js tests/ollama-cold-start-loaded.test.js tests/zero-regression-gate.test.js tests/runtime-e2e.test.js tests/local-vision-stabilizer.test.js tests/local-connection-probe.test.js tests/repetition-guard.test.js tests/guide-scroll-gate.test.js tests/settings-scroll-jank-regression.test.js tests/android-attachment-compat.test.js tests/free-cloud-first.test.js tests/rc6-rc7-compat.test.js tests/native-image-coexistence.test.js tests/issue-276-session-image-ownership.test.js tests/issue-276-entry-policy-bridge.test.js tests/issue-276-policy-hardening.test.js tests/issue-289-native-nonintervention.test.js tests/pi-ai-bridge-wire-compat.test.js tests/mixed-router.test.js tests/depth-tier.test.js tests/depth-quota-behavior.test.js tests/structured-flow-hardening.test.js tests/structured-flow-adversarial.test.js tests/vision-budget-activation.test.js tests/runtime-boundary-fixes.test.js tests/runtime-i18n.test.js tests/tesseract-node24-boot.test.js tests/session-vision-state.test.js tests/session-vision-state-integration.test.js tests/image-resource-governor.test.js tests/resource-retention-lifecycle.test.js tests/pixel-diff-stream.test.js tests/large-image-resource-integration.test.js tests/structured-guard-idempotency.test.js tests/vision-routing-product.test.js tests/vision-runtime-performance.test.js tests/vision-routing-settings-prelude.test.js tests/v2-settings-ia-integration.test.js tests/vision-capability-router.test.js tests/vision-capability-reference.test.js tests/vision-capability-benchmark.test.js tests/vision-capability-grounding-proof-contract.test.js tests/vision-capability-probe.test.js tests/vision-capability-axis-freshness.test.js tests/vision-capability-shadow-health.test.js tests/vision-capability-benchmark-service.test.js tests/vision-capability-preflight-hardening.test.js tests/vision-capability-benchmark-contract.test.js tests/vision-capability-benchmark-client.test.js tests/vision-capability-benchmark-client-productization.test.js tests/vision-capability-adapter-route.test.js tests/vision-background-benchmark.test.js tests/vision-background-lifecycle.test.js tests/vision-background-benchmark-productization.test.js tests/vision-capability-benchmark-budget.test.js tests/vision-image-input-verdict.test.js tests/vision-capability-failure-classification.test.js tests/vision-capability-gate-client.test.js tests/v2-product-review-fixes.test.js tests/qa-top5-reliability.test.js tests/qa-screenshot-runtime.test.js tests/qa-runtime-identity.test.js tests/qa-turn-budget-cancellation.test.js tests/qa-cancellation-publication.test.js tests/qa-reliability-tail.test.js tests/qa-endpoint-route-alias.test.js tests/settings-native-card-semantics.test.js tests/settings-native-card-lazy-data.test.js tests/settings-invalid-staged-dirty.test.js tests/vision-toggle-root-hardening.test.js && node --test tests/vision-backend-runtime-policy.test.js",
72
+ "test": "node --test tests/adapter-prepare-call-compat.test.js tests/core.test.js tests/issue-374-delegated-call-config.test.js tests/grounding-coordinate-frame.test.js tests/grounding-coordinate-runtime.test.js tests/capability-advisory.test.js tests/vision-execution-policy.test.js tests/vision-backend-diagnostics.test.js tests/live-model-discovery.test.js tests/live-model-credential-resolution.test.js tests/vision-model-registry.test.js tests/live-model-client-stability.test.js tests/providers-persistence.test.js tests/provider-directory-fallback.test.js tests/model-catalog-policy.test.js tests/settings-ia-client-prelude.test.js tests/settings-ia-fj.test.js tests/settings-ia-acceptance-regression.test.js tests/settings-migration.test.js tests/settings-section-order.test.js tests/attachment-admission-policy.test.js tests/vision-resilience.test.js tests/vision-resilience-lifecycle.test.js tests/vision-breaker-readonly.test.js tests/alpha1-browser-lifecycle-integration.test.js tests/issue-367-remote-session-inject.test.js tests/vision-mode-toggle-injection.test.js tests/client.test.js tests/issue-307-regression.test.js tests/issue-284-vision-mode-toggle.test.js tests/issue-284-vision-mode-hardening.test.js tests/issue-284-attachment-id-hint.test.js tests/issue-284-model-visibility.test.js tests/issue-284-vision-selection-effort.test.js tests/client-presentation-boundary.test.js tests/clipboard-image-paste-compat.test.js tests/remote-settings-bridge.test.js tests/remote-settings-v2-contract.test.js tests/remote-settings-risk-confirmation.test.js tests/local-remote-settings-permission.test.js tests/rc6-real-settings-persistence.test.js tests/artifact-path-security.test.js tests/http-compat.test.js tests/catalog-corrections.test.js tests/update-check.test.js tests/update-check-signal-lifecycle.test.js tests/self-update.test.js tests/doctor.test.js tests/doctor-cli.test.js tests/legacy-session-repair.test.js tests/profile-pnpm-diagnostics.test.js tests/doctor-v2.test.js tests/doctor-cli-v2.test.js tests/doctor-runtime.test.js tests/dsh-host-capabilities.test.js tests/doctor-host-capabilities.test.js tests/session-repair-v2.test.js tests/bundle-defaults.test.js tests/file-logger.test.js tests/replay-delegation.test.js tests/twin-image-capability-fallback.test.js tests/twin-image-custom-wrapper.test.js tests/adversarial-hardening.test.js tests/vision-adversarial-hardening-v2.test.js tests/vision-capability-benchmark-visual-proof.test.js tests/wrapper-directory.test.js tests/logging-ui.test.js tests/manifest-dependencies.test.js tests/structured-bootstrap.test.js tests/structured-bootstrap-gate.test.js tests/vision-model-guidance-contract.test.js tests/local-ollama.test.js tests/ollama-cold-start.test.js tests/ollama-cold-start-loaded.test.js tests/zero-regression-gate.test.js tests/runtime-e2e.test.js tests/local-vision-stabilizer.test.js tests/local-connection-probe.test.js tests/repetition-guard.test.js tests/guide-scroll-gate.test.js tests/settings-scroll-jank-regression.test.js tests/android-attachment-compat.test.js tests/free-cloud-first.test.js tests/rc6-rc7-compat.test.js tests/native-image-coexistence.test.js tests/issue-276-session-image-ownership.test.js tests/issue-276-entry-policy-bridge.test.js tests/issue-276-policy-hardening.test.js tests/issue-289-native-nonintervention.test.js tests/pi-ai-bridge-wire-compat.test.js tests/mixed-router.test.js tests/depth-tier.test.js tests/depth-quota-behavior.test.js tests/structured-flow-hardening.test.js tests/structured-flow-adversarial.test.js tests/vision-budget-activation.test.js tests/runtime-boundary-fixes.test.js tests/runtime-i18n.test.js tests/tesseract-node24-boot.test.js tests/session-vision-state.test.js tests/session-vision-state-integration.test.js tests/image-resource-governor.test.js tests/resource-retention-lifecycle.test.js tests/pixel-diff-stream.test.js tests/large-image-resource-integration.test.js tests/structured-guard-idempotency.test.js tests/vision-routing-product.test.js tests/vision-runtime-performance.test.js tests/vision-routing-settings-prelude.test.js tests/v2-settings-ia-integration.test.js tests/vision-capability-router.test.js tests/vision-capability-reference.test.js tests/vision-capability-benchmark.test.js tests/vision-capability-grounding-proof-contract.test.js tests/vision-capability-probe.test.js tests/vision-capability-axis-freshness.test.js tests/vision-capability-shadow-health.test.js tests/vision-capability-benchmark-service.test.js tests/vision-capability-preflight-hardening.test.js tests/vision-capability-benchmark-contract.test.js tests/vision-capability-benchmark-client.test.js tests/vision-capability-benchmark-client-productization.test.js tests/vision-capability-adapter-route.test.js tests/vision-background-benchmark.test.js tests/vision-background-lifecycle.test.js tests/vision-background-benchmark-productization.test.js tests/vision-capability-benchmark-budget.test.js tests/vision-image-input-verdict.test.js tests/vision-capability-failure-classification.test.js tests/vision-capability-gate-client.test.js tests/v2-product-review-fixes.test.js tests/qa-top5-reliability.test.js tests/qa-screenshot-runtime.test.js tests/qa-runtime-identity.test.js tests/qa-turn-budget-cancellation.test.js tests/qa-cancellation-publication.test.js tests/qa-reliability-tail.test.js tests/qa-endpoint-route-alias.test.js tests/settings-native-card-semantics.test.js tests/settings-native-card-lazy-data.test.js tests/settings-invalid-staged-dirty.test.js tests/vision-toggle-root-hardening.test.js && node --test tests/vision-backend-runtime-policy.test.js",
73
73
  "test:core": "node --test tests/core.test.js tests/runtime-e2e.test.js tests/structured-bootstrap.test.js tests/structured-flow-hardening.test.js tests/structured-flow-adversarial.test.js tests/vision-execution-policy.test.js tests/vision-backend-runtime-policy.test.js",
74
74
  "test:routing": "node --test tests/issue-374-delegated-call-config.test.js tests/twin-image-capability-fallback.test.js tests/twin-image-custom-wrapper.test.js tests/vision-routing-product.test.js tests/vision-capability-router.test.js tests/vision-capability-probe.test.js tests/vision-capability-shadow-health.test.js tests/vision-runtime-performance.test.js tests/vision-background-benchmark.test.js tests/vision-image-input-verdict.test.js",
75
75
  "test:session": "node --test tests/session-vision-state.test.js tests/session-vision-state-integration.test.js tests/replay-delegation.test.js tests/legacy-session-repair.test.js tests/session-repair-v2.test.js tests/issue-276-session-image-ownership.test.js",
76
76
  "test:resources": "node --test tests/artifact-path-security.test.js tests/image-resource-governor.test.js tests/resource-retention-lifecycle.test.js tests/pixel-diff-stream.test.js tests/large-image-resource-integration.test.js tests/qa-turn-budget-cancellation.test.js tests/qa-cancellation-publication.test.js",
77
77
  "test:compat": "node --test tests/adapter-prepare-call-compat.test.js tests/attachment-admission-policy.test.js tests/rc6-rc7-compat.test.js tests/rc6-real-settings-persistence.test.js tests/android-attachment-compat.test.js tests/http-compat.test.js tests/pi-ai-bridge-wire-compat.test.js tests/native-image-coexistence.test.js tests/tesseract-node24-boot.test.js tests/dsh-host-capabilities.test.js",
78
- "test:web": "node --test tests/alpha1-browser-lifecycle-integration.test.js tests/issue-367-remote-session-inject.test.js tests/client.test.js tests/client-presentation-boundary.test.js tests/settings-ia-client-prelude.test.js tests/settings-ia-acceptance-regression.test.js tests/remote-settings-bridge.test.js tests/remote-settings-v2-contract.test.js tests/local-remote-settings-permission.test.js tests/v2-settings-ia-integration.test.js tests/logging-ui.test.js",
78
+ "test:web": "node --test tests/alpha1-browser-lifecycle-integration.test.js tests/issue-367-remote-session-inject.test.js tests/vision-mode-toggle-injection.test.js tests/client.test.js tests/client-presentation-boundary.test.js tests/clipboard-image-paste-compat.test.js tests/settings-ia-client-prelude.test.js tests/settings-ia-acceptance-regression.test.js tests/remote-settings-bridge.test.js tests/remote-settings-v2-contract.test.js tests/local-remote-settings-permission.test.js tests/v2-settings-ia-integration.test.js tests/logging-ui.test.js",
79
79
  "test:contract": "node --test tests/manifest-dependencies.test.js tests/bundle-defaults.test.js tests/zero-regression-gate.test.js tests/issue-289-native-nonintervention.test.js tests/issue-374-delegated-call-config.test.js tests/runtime-boundary-fixes.test.js tests/runtime-i18n.test.js tests/adapter-prepare-call-compat.test.js tests/rc6-rc7-compat.test.js tests/attachment-admission-policy.test.js tests/dsh-host-capabilities.test.js tests/doctor-host-capabilities.test.js tests/structured-bootstrap.test.js tests/structured-bootstrap-gate.test.js",
80
80
  "test:grounding": "node --test tests/grounding-coordinate-frame.test.js tests/grounding-coordinate-runtime.test.js tests/vision-capability-grounding-proof-contract.test.js",
81
81
  "test:stress": "node scripts/image-resource-stress.mjs && node --test tests/large-image-resource-integration.test.js tests/resource-retention-lifecycle.test.js"