dsh-math-modeling-agent 0.1.1 → 0.2.0

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.
@@ -4,7 +4,9 @@ import { dirname, join, resolve } from 'node:path'
4
4
  import { fileURLToPath } from 'node:url'
5
5
  import { isDeepStrictEqual } from 'node:util'
6
6
 
7
- export const SCHEMA_VERSION = 1
7
+ export const SCHEMA_VERSION = 2
8
+ export const SUPPORTED_SCHEMA_VERSIONS = [1, 2]
9
+ export const INTERACTION_DECISIONS = Object.freeze({ D1: 'routing', D2: 'assumptions', D3: 'direction', D4: 'verdict' })
8
10
  export const MODE_DEFAULTS = Object.freeze({
9
11
  fast: Object.freeze({ attempts: 2, researchQueries: 0, computeSeconds: 60 }),
10
12
  standard: Object.freeze({ attempts: 12, researchQueries: 12, computeSeconds: 1800 }),
@@ -282,7 +284,7 @@ function deeplyEqual(left, right) { return isDeepStrictEqual(left, right) }
282
284
  function validateRunShape(run, diagnostics, prefix = 'run') {
283
285
  addDiagnostic(diagnostics, exactKeys(run, RUN_KEYS), `${prefix} must contain exactly the contracted fields`)
284
286
  if (!isObject(run)) return
285
- addDiagnostic(diagnostics, run.schemaVersion === SCHEMA_VERSION, `${prefix}.schemaVersion must equal ${SCHEMA_VERSION}`)
287
+ addDiagnostic(diagnostics, SUPPORTED_SCHEMA_VERSIONS.includes(run.schemaVersion), `${prefix}.schemaVersion must be one of ${SUPPORTED_SCHEMA_VERSIONS.join(',')}`)
286
288
  addDiagnostic(diagnostics, typeof run.taskId === 'string' && TASK_ID.test(run.taskId), `${prefix}.taskId must be kebab-case`)
287
289
  addDiagnostic(diagnostics, Object.hasOwn(MODE_DEFAULTS, run.mode), `${prefix}.mode is invalid`)
288
290
  addDiagnostic(diagnostics, STATUSES.includes(run.status), `${prefix}.status is invalid`)
@@ -297,7 +299,7 @@ function validateRunShape(run, diagnostics, prefix = 'run') {
297
299
  function validateLedgerShape(ledger, diagnostics) {
298
300
  addDiagnostic(diagnostics, exactKeys(ledger, LEDGER_KEYS), 'ledger must contain exactly the contracted fields')
299
301
  if (!isObject(ledger)) return
300
- addDiagnostic(diagnostics, ledger.schemaVersion === SCHEMA_VERSION, `ledger.schemaVersion must equal ${SCHEMA_VERSION}`)
302
+ addDiagnostic(diagnostics, SUPPORTED_SCHEMA_VERSIONS.includes(ledger.schemaVersion), `ledger.schemaVersion must be one of ${SUPPORTED_SCHEMA_VERSIONS.join(',')}`)
301
303
  addDiagnostic(diagnostics, typeof ledger.taskId === 'string' && TASK_ID.test(ledger.taskId), 'ledger.taskId must be kebab-case')
302
304
  addDiagnostic(diagnostics, isObject(ledger.scope), 'ledger.scope must be an object')
303
305
  for (const key of ['assumptions', 'claims', 'obligations', 'subproblems', 'candidates', 'issues']) addDiagnostic(diagnostics, Array.isArray(ledger[key]), `ledger.${key} must be an array`)
@@ -312,7 +314,7 @@ function validateEventShape(event, diagnostics, index) {
312
314
  ? ['schemaVersion', 'sequence', 'type', 'taskId', 'timestamp', 'snapshot']
313
315
  : ['schemaVersion', 'sequence', 'type', 'taskId', 'from', 'to', 'reason', 'evidenceIds', 'issueIds', 'timestamp', 'snapshot']
314
316
  addDiagnostic(diagnostics, exactKeys(event, expectedKeys), `${prefix} must contain exactly the contracted fields`)
315
- addDiagnostic(diagnostics, event.schemaVersion === SCHEMA_VERSION, `${prefix}.schemaVersion must equal ${SCHEMA_VERSION}`)
317
+ addDiagnostic(diagnostics, SUPPORTED_SCHEMA_VERSIONS.includes(event.schemaVersion), `${prefix}.schemaVersion must be one of ${SUPPORTED_SCHEMA_VERSIONS.join(',')}`)
316
318
  addDiagnostic(diagnostics, event.sequence === index, `${prefix}.sequence must equal ${index}`)
317
319
  addDiagnostic(diagnostics, typeof event.taskId === 'string' && TASK_ID.test(event.taskId), `${prefix}.taskId must be kebab-case`)
318
320
  addDiagnostic(diagnostics, event.type === expectedType, `${prefix}.type must be ${expectedType}`)
@@ -377,8 +379,8 @@ export async function initRun(root, { taskId, mode = 'standard', budget, budgets
377
379
  bestCandidateId: null, budget: effectiveBudget, createdAt: timestamp, updatedAt: timestamp,
378
380
  }
379
381
  const ledger = {
380
- schemaVersion: SCHEMA_VERSION, taskId, scope: { independentAuditPassed: false }, assumptions: [], claims: [],
381
- obligations: [], subproblems: [], candidates: [], issues: [],
382
+ schemaVersion: SCHEMA_VERSION, taskId, scope: { independentAuditPassed: false, interactions: [], decisionStack: [], robustnessExempt: false },
383
+ assumptions: [], claims: [], obligations: [], subproblems: [], candidates: [], issues: [],
382
384
  }
383
385
  const event = { schemaVersion: SCHEMA_VERSION, sequence: 0, type: 'RUN_INITIALIZED', taskId, timestamp, snapshot: clone(run) }
384
386
  const p = paths(root)
@@ -409,6 +411,51 @@ export async function initRun(root, { taskId, mode = 'standard', budget, budgets
409
411
  }, runtime)
410
412
  }
411
413
 
414
+ /** The v2 interaction contract: decision records, decision-stack backtracking, and the robustness gate. */
415
+ function hasDecisionRecord(scope, decisionPoint) {
416
+ return Array.isArray(scope?.interactions) && scope.interactions.some(entry =>
417
+ entry && (entry.decisionPoint === decisionPoint || entry.auto === true))
418
+ }
419
+ function decisionStackIds(scope) {
420
+ return Array.isArray(scope?.decisionStack)
421
+ ? scope.decisionStack.filter(entry => entry && typeof entry.id === 'string').map(entry => entry.id)
422
+ : []
423
+ }
424
+ function interactionContractViolations(run, ledger, to, reason) {
425
+ if (ledger.schemaVersion !== SCHEMA_VERSION) return []
426
+ const scope = ledger.scope ?? {}
427
+ const violations = []
428
+ const transitional = to !== undefined
429
+ const earlyStop = ['BLOCKED', 'CANCELLED'].includes(run.status)
430
+ if ((transitional && to === 'SCOPE_FROZEN') || (!transitional && run.status !== 'TRIAGE' && !earlyStop)) {
431
+ if (!hasDecisionRecord(scope, 'D1')) violations.push('D1 routing interaction record missing (ledger.scope.interactions)')
432
+ }
433
+ const enteringAttemptFromCandidates = transitional ? (to === 'ATTEMPT' && run.status === 'CANDIDATES_READY') : (run.currentAttempt >= 1)
434
+ if (enteringAttemptFromCandidates && !hasDecisionRecord(scope, 'D3')) violations.push('D3 direction-selection interaction record missing')
435
+ const enteringTerminalFromVerify = transitional
436
+ ? (FINAL_STATES.includes(to) && run.status === 'VERIFY')
437
+ : (FINAL_STATES.includes(run.status) && !['BLOCKED', 'CANCELLED'].includes(run.status))
438
+ if (enteringTerminalFromVerify && !hasDecisionRecord(scope, 'D4')) violations.push('D4 verdict interaction record missing')
439
+ if (transitional && (to === 'REVISE' || to === 'RESEARCH')) {
440
+ const ids = decisionStackIds(scope)
441
+ if (ids.length > 0 && !ids.some(id => typeof reason === 'string' && reason.includes(id))) {
442
+ violations.push(`REVISE/RESEARCH reason must reference a decisionStack entry id (available: ${ids.join(', ')})`)
443
+ }
444
+ }
445
+ const terminalAndExempt = transitional
446
+ ? (FINAL_STATES.includes(to) && !['BLOCKED', 'CANCELLED'].includes(to))
447
+ : (FINAL_STATES.includes(run.status) && !['BLOCKED', 'CANCELLED'].includes(run.status))
448
+ if (terminalAndExempt && scope.robustnessExempt !== true) {
449
+ const passed = Array.isArray(ledger.obligations) && ledger.obligations.some(o => o && o.kind === 'robustness' && o.status === 'PASS')
450
+ if (!passed) violations.push('terminal state requires a PASSED robustness-kind obligation (or scope.robustnessExempt: true)')
451
+ }
452
+ return violations
453
+ }
454
+ function enforceInteractionContract(run, ledger, to, reason) {
455
+ const violations = interactionContractViolations(run, ledger, to, reason)
456
+ if (violations.length > 0) throw new Error(`interaction contract violated: ${violations.join('; ')}`)
457
+ }
458
+
412
459
  function enforceSolvedGate(run, ledger) {
413
460
  if (ledger.claims.length < 1) throw new Error('SOLVED requires at least one claim')
414
461
  if (ledger.obligations.some(item => item.required !== false && item.status !== 'PASS')) throw new Error('required obligations remain open')
@@ -473,12 +520,13 @@ async function readEvents(path) {
473
520
 
474
521
  async function validateRunUnlocked(root, expectedTaskId) {
475
522
  const diagnostics = []
523
+ const warnings = []
476
524
  let run, ledger, events
477
525
  const p = paths(root)
478
526
  try { run = await readJson(p.run) } catch (error) { diagnostics.push(`run unreadable: ${error.message}`) }
479
527
  try { ledger = await readJson(p.ledger) } catch (error) { diagnostics.push(`ledger unreadable: ${error.message}`) }
480
528
  try { events = await readEvents(p.events) } catch (error) { diagnostics.push(error.message) }
481
- if (!run || !ledger || !events) return { valid: false, diagnostics }
529
+ if (!run || !ledger || !events) return { valid: false, diagnostics, warnings }
482
530
  validateRunShape(run, diagnostics)
483
531
  validateLedgerShape(ledger, diagnostics)
484
532
  validateJournal(events, diagnostics)
@@ -486,13 +534,34 @@ async function validateRunUnlocked(root, expectedTaskId) {
486
534
  const last = events.at(-1)
487
535
  if (last && run.eventSequence !== last.sequence) diagnostics.push('state/journal sequence mismatch')
488
536
  if (last && JSON.stringify(run) !== JSON.stringify(last.snapshot)) diagnostics.push('state/journal snapshot mismatch')
489
- return { valid: diagnostics.length === 0, diagnostics, run: clone(run) }
537
+ if (ledger.schemaVersion !== SCHEMA_VERSION) warnings.push(`legacy run (ledger schema v${ledger.schemaVersion}): interaction contract not enforced`)
538
+ else {
539
+ for (const violation of interactionContractViolations(run, ledger, undefined, undefined)) {
540
+ warnings.push(`interaction contract: ${violation}`)
541
+ }
542
+ }
543
+ return { valid: diagnostics.length === 0, diagnostics, warnings, run: clone(run) }
490
544
  }
491
545
 
492
546
  export async function validateRun(root, expectedTaskId, runtime = {}) {
493
547
  return withLock(root, MUTATION_LOCK_FILE, 'validate', () => validateRunUnlocked(root, expectedTaskId), runtime)
494
548
  }
495
549
 
550
+ /**
551
+ * Check the v2 interaction contract for a PROSPECTIVE transition without
552
+ * mutating anything. The agent workflow must call this before `transition`;
553
+ * the CLI `transition` command enforces it internally unless --skip-gate.
554
+ */
555
+ export async function gateTransition(root, { to, reason }, runtime = {}) {
556
+ return withLock(root, MUTATION_LOCK_FILE, 'gate', async () => {
557
+ const p = paths(root)
558
+ const run = await readJson(p.run)
559
+ const ledger = await readJson(p.ledger)
560
+ const violations = interactionContractViolations(run, ledger, to, reason)
561
+ return { allowed: violations.length === 0, violations, status: run.status }
562
+ }, runtime)
563
+ }
564
+
496
565
  export async function recoverRun(root, runtime = {}) {
497
566
  return withLock(root, MUTATION_LOCK_FILE, 'recover', async () => {
498
567
  const p = paths(root)
@@ -533,7 +602,21 @@ async function cli(argv) {
533
602
  const root = resolve(rootArg)
534
603
  const options = parseOptions(rest)
535
604
  if (command === 'init') return initRun(root, { taskId: options['task-id'], mode: options.mode ?? 'standard' })
536
- if (command === 'transition') return transitionRun(root, { to: options.to, reason: options.reason, evidenceIds: options.evidence ? options.evidence.split(',').filter(Boolean) : [], issueIds: options.issues ? options.issues.split(',').filter(Boolean) : [], patch: options.candidate ? { bestCandidateId: options.candidate } : undefined })
605
+ if (command === 'gate') {
606
+ const result = await gateTransition(root, { to: options.to, reason: options.reason })
607
+ if (!result.allowed) process.exitCode = 1
608
+ return result
609
+ }
610
+ if (command === 'transition') {
611
+ if (options['skip-gate'] !== 'true') {
612
+ const gate = await gateTransition(root, { to: options.to, reason: options.reason })
613
+ if (!gate.allowed) {
614
+ process.exitCode = 1
615
+ return { error: `interaction contract violated: ${gate.violations.join('; ')}`, gate }
616
+ }
617
+ }
618
+ return transitionRun(root, { to: options.to, reason: options.reason, evidenceIds: options.evidence ? options.evidence.split(',').filter(Boolean) : [], issueIds: options.issues ? options.issues.split(',').filter(Boolean) : [], patch: options.candidate ? { bestCandidateId: options.candidate } : undefined })
619
+ }
537
620
  if (command === 'validate') {
538
621
  const result = await validateRun(root, options['task-id'])
539
622
  if (!result.valid) process.exitCode = 1
@@ -541,7 +624,7 @@ async function cli(argv) {
541
624
  }
542
625
  if (command === 'recover') return recoverRun(root)
543
626
  if (command === 'status') return statusRun(root)
544
- throw new Error('usage: run-state.mjs <init|transition|validate|recover|status> <run-directory> [options]')
627
+ throw new Error('usage: run-state.mjs <init|transition|gate|validate|recover|status> <run-directory> [options]')
545
628
  }
546
629
  const invoked = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)
547
630
  if (invoked) cli(process.argv.slice(2)).then(result => process.stdout.write(`${JSON.stringify(result)}\n`)).catch(error => { process.stdout.write(`${JSON.stringify({ error: error.message })}\n`); process.exitCode = 1 })
@@ -1,92 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="1066" height="620" viewBox="0 0 1066 620" role="img" aria-label="MathModelingAgent 工作流总览">
2
- <defs>
3
- <filter id="shadow" x="-4%" y="-4%" width="108%" height="112%">
4
- <feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#0f172a" flood-opacity="0.06"/>
5
- </filter>
6
- </defs>
7
- <rect width="1066" height="620" rx="16" fill="#f8fafc"/>
8
- <text x="40" y="52" font-family="system-ui,-apple-system,'Segoe UI',sans-serif" font-size="30" font-weight="700" fill="#0f172a">MathModelingAgent</text>
9
- <text x="40" y="84" font-family="system-ui,-apple-system,'Segoe UI',sans-serif" font-size="15" fill="#475569">从题目到可复现结论 · 模型负责提出,证据负责裁决</text>
10
- <g font-family="system-ui,-apple-system,'Segoe UI',sans-serif" font-size="11" fill="#334155">
11
- <rect x="774" y="26" width="116" height="28" rx="14" fill="#ffffff" stroke="#e2e8f0"/>
12
- <text x="832" y="44" text-anchor="middle" fill="#475569">不绑定具体状态名</text>
13
- <rect x="900" y="26" width="122" height="28" rx="14" fill="#ffffff" stroke="#e2e8f0"/>
14
- <text x="961" y="44" text-anchor="middle" fill="#475569">不绑定文件结构</text>
15
- <rect x="1032" y="26" width="104" height="28" rx="14" fill="#ffffff" stroke="#e2e8f0"/>
16
- <text x="1084" y="44" text-anchor="middle" fill="#475569">工具可替换</text>
17
- </g>
18
- <g font-family="system-ui,-apple-system,'Segoe UI',sans-serif">
19
- <rect x="40" y="112" width="184" height="250" rx="14" fill="#ffffff" stroke="#e2e8f0" filter="url(#shadow)"/>
20
- <rect x="40" y="112" width="184" height="8" rx="4" fill="#34d399"/>
21
- <text x="56" y="156" font-size="17" font-weight="700" fill="#0f172a">题目与数据</text>
22
- <text x="56" y="178" font-size="11" font-weight="700" letter-spacing="1" fill="#34d399">INPUT</text>
23
- <circle cx="62" cy="208" r="4" fill="#34d399"/><text x="74" y="212" font-size="13" fill="#475569">题目 / PDF</text>
24
- <circle cx="62" cy="240" r="4" fill="#34d399"/><text x="74" y="244" font-size="13" fill="#475569">数据与附件</text>
25
- <circle cx="62" cy="272" r="4" fill="#34d399"/><text x="74" y="276" font-size="13" fill="#475569">目标与约束</text>
26
-
27
- <rect x="246" y="112" width="184" height="250" rx="14" fill="#ffffff" stroke="#e2e8f0" filter="url(#shadow)"/>
28
- <rect x="246" y="112" width="184" height="8" rx="4" fill="#60a5fa"/>
29
- <text x="262" y="156" font-size="17" font-weight="700" fill="#0f172a">理解与拆解</text>
30
- <text x="262" y="178" font-size="11" font-weight="700" letter-spacing="1" fill="#60a5fa">UNDERSTAND</text>
31
- <circle cx="268" cy="208" r="4" fill="#60a5fa"/><text x="280" y="212" font-size="13" fill="#475569">明确问题边界</text>
32
- <circle cx="268" cy="240" r="4" fill="#60a5fa"/><text x="280" y="244" font-size="13" fill="#475569">拆成可验证子问题</text>
33
- <circle cx="268" cy="272" r="4" fill="#60a5fa"/><text x="280" y="276" font-size="13" fill="#475569">登记关键假设</text>
34
-
35
- <rect x="452" y="112" width="184" height="250" rx="14" fill="#ffffff" stroke="#e2e8f0" filter="url(#shadow)"/>
36
- <rect x="452" y="112" width="184" height="8" rx="4" fill="#a78bfa"/>
37
- <text x="468" y="156" font-size="17" font-weight="700" fill="#0f172a">建模与执行</text>
38
- <text x="468" y="178" font-size="11" font-weight="700" letter-spacing="1" fill="#a78bfa">EXPLORE</text>
39
- <circle cx="474" cy="208" r="4" fill="#a78bfa"/><text x="486" y="212" font-size="13" fill="#475569">提出候选模型</text>
40
- <circle cx="474" cy="240" r="4" fill="#a78bfa"/><text x="486" y="244" font-size="13" fill="#475569">计算 / 搜索 / 推导</text>
41
- <circle cx="474" cy="272" r="4" fill="#a78bfa"/><text x="486" y="276" font-size="13" fill="#475569">保留过程产物</text>
42
-
43
- <rect x="658" y="112" width="184" height="250" rx="14" fill="#ffffff" stroke="#e2e8f0" filter="url(#shadow)"/>
44
- <rect x="658" y="112" width="184" height="8" rx="4" fill="#fb923c"/>
45
- <text x="674" y="156" font-size="17" font-weight="700" fill="#0f172a">证据验证</text>
46
- <text x="674" y="178" font-size="11" font-weight="700" letter-spacing="1" fill="#fb923c">VERIFY</text>
47
- <circle cx="680" cy="208" r="4" fill="#fb923c"/><text x="692" y="212" font-size="13" fill="#475569">主张 ↔ 验证义务</text>
48
- <circle cx="680" cy="240" r="4" fill="#fb923c"/><text x="692" y="244" font-size="13" fill="#475569">独立重算 / 反例攻击</text>
49
- <circle cx="680" cy="272" r="4" fill="#fb923c"/><text x="692" y="276" font-size="13" fill="#475569">证据不足就回去修正</text>
50
-
51
- <rect x="864" y="112" width="184" height="250" rx="14" fill="#ffffff" stroke="#e2e8f0" filter="url(#shadow)"/>
52
- <rect x="864" y="112" width="184" height="8" rx="4" fill="#f472b6"/>
53
- <text x="880" y="156" font-size="17" font-weight="700" fill="#0f172a">结果交付</text>
54
- <text x="880" y="178" font-size="11" font-weight="700" letter-spacing="1" fill="#f472b6">DELIVER</text>
55
- <circle cx="886" cy="208" r="4" fill="#f472b6"/><text x="898" y="212" font-size="13" fill="#475569">结论 + 局限</text>
56
- <circle cx="886" cy="240" r="4" fill="#f472b6"/><text x="898" y="244" font-size="13" fill="#475569">代码 / 数据 / 复现材料</text>
57
- <circle cx="886" cy="272" r="4" fill="#f472b6"/><text x="898" y="276" font-size="13" fill="#475569">审计 / 终态报告</text>
58
- </g>
59
- <g stroke="#94a3b8" stroke-width="2.5" fill="none" marker-end="url(#arrow)">
60
- <line x1="224" y1="237" x2="242" y2="237"/>
61
- <line x1="430" y1="237" x2="448" y2="237"/>
62
- <line x1="636" y1="237" x2="654" y2="237"/>
63
- <line x1="842" y1="237" x2="860" y2="237"/>
64
- </g>
65
- <defs><marker id="arrow" markerWidth="8" markerHeight="8" refX="6" refY="4" orient="auto"><path d="M0,0 L8,4 L0,8 Z" fill="#94a3b8"/></marker></defs>
66
- <path d="M750 362 C 750 430, 700 452, 544 452 L 544 366" fill="none" stroke="#6366f1" stroke-width="2" stroke-dasharray="6 5" marker-end="url(#arrowLoop)"/>
67
- <defs><marker id="arrowLoop" markerWidth="8" markerHeight="8" refX="6" refY="4" orient="auto"><path d="M0,0 L8,4 L0,8 Z" fill="#6366f1"/></marker></defs>
68
- <rect x="404" y="430" width="284" height="28" rx="14" fill="#eef2ff" stroke="#c7d2fe"/>
69
- <text x="546" y="448" text-anchor="middle" font-family="system-ui,-apple-system,'Segoe UI',sans-serif" font-size="13" fill="#4338ca">证据不够 → 修正,而不是硬通过</text>
70
-
71
- <rect x="40" y="478" width="1008" height="118" rx="14" fill="#ffffff" stroke="#e2e8f0"/>
72
- <text x="64" y="512" font-family="system-ui,-apple-system,'Segoe UI',sans-serif" font-size="15" font-weight="700" fill="#0f172a">可插拔能力</text>
73
- <text x="64" y="534" font-family="system-ui,-apple-system,'Segoe UI',sans-serif" font-size="12" fill="#64748b">实现可以换,语义不变:任何工具都只是为"建模"或"验证"提供证据。</text>
74
- <g font-family="system-ui,-apple-system,'Segoe UI',sans-serif" text-anchor="middle">
75
- <rect x="64" y="552" width="180" height="26" rx="8" fill="#f1f5f9"/>
76
- <text x="154" y="569" font-size="12" font-weight="600" fill="#0f172a">数值计算</text>
77
- <text x="154" y="582" font-size="10" fill="#64748b">Python / 其他运行时</text>
78
- <rect x="258" y="552" width="180" height="26" rx="8" fill="#f1f5f9"/>
79
- <text x="348" y="569" font-size="12" font-weight="600" fill="#0f172a">形式验证</text>
80
- <text x="348" y="582" font-size="10" fill="#64748b">Lean / theorem prover</text>
81
- <rect x="452" y="552" width="180" height="26" rx="8" fill="#f1f5f9"/>
82
- <text x="542" y="569" font-size="12" font-weight="600" fill="#0f172a">符号计算</text>
83
- <text x="542" y="582" font-size="10" fill="#64748b">Wolfram / CAS</text>
84
- <rect x="646" y="552" width="180" height="26" rx="8" fill="#f1f5f9"/>
85
- <text x="736" y="569" font-size="12" font-weight="600" fill="#0f172a">研究检索</text>
86
- <text x="736" y="582" font-size="10" fill="#64748b">文献 / Web</text>
87
- <rect x="840" y="552" width="180" height="26" rx="8" fill="#f1f5f9"/>
88
- <text x="930" y="569" font-size="12" font-weight="600" fill="#0f172a">终审</text>
89
- <text x="930" y="582" font-size="10" fill="#64748b">MCM / ICM audit</text>
90
- </g>
91
- <text x="1032" y="612" text-anchor="end" font-family="system-ui,-apple-system,'Segoe UI',sans-serif" font-size="10" fill="#94a3b8">README overview</text>
92
- </svg>