dsh-math-modeling-agent 0.2.6 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -108,7 +108,7 @@ D3 方向/D4 裁决),`run-state.mjs gate` 在每次状态转移前强制校
108
108
  安装:
109
109
 
110
110
  ```bash
111
- dsh plugin --profile web add github:yohanchen1/MathModelingAgent#v0.2.6
111
+ dsh plugin --profile web add github:yohanchen1/MathModelingAgent#v0.2.7
112
112
  dsh --profile web --dump-config # 检查组合层(应看到 dsh-math-modeling-agent-skills 行)
113
113
  dsh web # 重启以加载插件
114
114
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-math-modeling-agent",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "Evidence-driven mathematical modeling and verification skills for DeepSeek Harness",
5
5
  "type": "module",
6
6
  "files": [
@@ -68,6 +68,16 @@ After the user answers, problem-brief.md is updated and the delta is shown.
68
68
  4. The D-R interaction record goes into ledger.scope.interactions; the gate
69
69
  refuses CANDIDATES_READY without it.
70
70
 
71
+ ## Honest boundary of the gate
72
+
73
+ The gate is a MECHANISM guarantee, not an anti-fraud guarantee: it proves a
74
+ record exists, never that the record is true — every interaction record is
75
+ written by the agent itself. Falsified records are therefore out of the
76
+ gate's reach; the defense against them is the blind verifier subagent
77
+ (subagent-dispatch.md), which re-derives key claims from data alone, and the
78
+ artifact checks (research/sources.jsonl, attempts/N/report.md) that tie each
79
+ record to real files on disk.
80
+
71
81
  ## Rules
72
82
 
73
83
  - D0/D1/D-R: once per task. D2/D3: once per subproblem. D2': only when a
@@ -81,7 +91,13 @@ After the user answers, problem-brief.md is updated and the delta is shown.
81
91
  ATTEMPT-from-CANDIDATES_READY without D3, terminals-from-VERIFY without D4,
82
92
  and REVISE/RESEARCH (from VERIFY) whose reason fails to reference a
83
93
  `decisionStack` entry id. The CLI `transition` command enforces the same
84
- gate (escape hatch `--skip-gate true` is for mechanism testing only).
94
+ gate and has NO escape hatch. Every D4 record carries the attempt number it
95
+ adjudicates (`attempt: N`); the terminal gate requires the D4 for the
96
+ CURRENT attempt. ATTEMPT from CANDIDATES_READY requires `--subproblem <id>`.
97
+ Terminal gates additionally require `scope.cleanupPassed: true` and the
98
+ current attempt's report.md on disk. Mechanism-level tests and unattended
99
+ batch runs may declare `scope.contractExempt: true` — production modeling
100
+ must never set it.
85
101
 
86
102
  ## L2 display points (MUST show, never ask)
87
103
 
@@ -366,7 +366,7 @@ function validateJournal(events, diagnostics) {
366
366
  })
367
367
  }
368
368
 
369
- export async function initRun(root, { taskId, mode = 'standard', budget, budgets }, runtime = {}) {
369
+ export async function initRun(root, { taskId, mode = 'standard', budget, budgets, contract }, runtime = {}) {
370
370
  assertTaskId(taskId); assertMode(mode)
371
371
  const effectiveBudget = { ...MODE_DEFAULTS[mode], ...(budget ?? budgets ?? {}) }
372
372
  const budgetDiagnostics = []
@@ -379,7 +379,8 @@ export async function initRun(root, { taskId, mode = 'standard', budget, budgets
379
379
  bestCandidateId: null, budget: effectiveBudget, createdAt: timestamp, updatedAt: timestamp,
380
380
  }
381
381
  const ledger = {
382
- schemaVersion: SCHEMA_VERSION, taskId, scope: { independentAuditPassed: false, interactions: [], decisionStack: [], robustnessExempt: false },
382
+ schemaVersion: SCHEMA_VERSION, taskId,
383
+ scope: { independentAuditPassed: false, interactions: [], decisionStack: [], robustnessExempt: false, ...(contract === false ? { contractExempt: true } : {}) },
383
384
  assumptions: [], claims: [], obligations: [], subproblems: [], candidates: [], issues: [],
384
385
  }
385
386
  const event = { schemaVersion: SCHEMA_VERSION, sequence: 0, type: 'RUN_INITIALIZED', taskId, timestamp, snapshot: clone(run) }
@@ -412,9 +413,14 @@ export async function initRun(root, { taskId, mode = 'standard', budget, budgets
412
413
  }
413
414
 
414
415
  /** The v2 interaction contract: decision records, decision-stack backtracking, and the robustness gate. */
415
- function hasDecisionRecord(scope, decisionPoint) {
416
+ function hasDecisionRecord(scope, decisionPoint, { allowAuto = true } = {}) {
416
417
  return Array.isArray(scope?.interactions) && scope.interactions.some(entry =>
417
- entry && (entry.decisionPoint === decisionPoint || entry.auto === true))
418
+ entry && (entry.decisionPoint === decisionPoint || (allowAuto && entry.auto === true)))
419
+ }
420
+ /** D4 verdict records carry the attempt number they adjudicate: {decisionPoint:'D4', attempt: 2, ...} */
421
+ function hasD4ForAttempt(scope, attempt) {
422
+ return Array.isArray(scope?.interactions) && scope.interactions.some(entry =>
423
+ entry && (entry.auto === true || (entry.decisionPoint === 'D4' && entry.attempt === attempt)))
418
424
  }
419
425
  function decisionStackIds(scope) {
420
426
  return Array.isArray(scope?.decisionStack)
@@ -430,6 +436,7 @@ function hasSubproblemD2(scope, subproblem) {
430
436
  function interactionContractViolations(run, ledger, to, reason, subproblem) {
431
437
  if (ledger.schemaVersion !== SCHEMA_VERSION) return []
432
438
  const scope = ledger.scope ?? {}
439
+ if (scope.contractExempt === true) return []
433
440
  const violations = []
434
441
  const transitional = to !== undefined
435
442
  const earlyStop = ['BLOCKED', 'CANCELLED'].includes(run.status)
@@ -439,16 +446,17 @@ function interactionContractViolations(run, ledger, to, reason, subproblem) {
439
446
  }
440
447
  const afterCandidates = ['CANDIDATES_READY', 'ATTEMPT', 'EXECUTE', 'VERIFY', 'REVISE', 'FORK', ...FINAL_STATES]
441
448
  const candidatesGate = transitional ? (to === 'CANDIDATES_READY') : (afterCandidates.includes(run.status) && !earlyStop)
442
- if (candidatesGate && !hasDecisionRecord(scope, 'D-R')) violations.push('D-R literature-research interaction record missing (pre-modeling literature survey is mandatory)')
449
+ if (candidatesGate && !hasDecisionRecord(scope, 'D-R', { allowAuto: false })) violations.push('D-R literature-research interaction record missing (pre-modeling literature survey is mandatory; auto-authorization is not accepted)')
443
450
  const enteringAttemptFromCandidates = transitional ? (to === 'ATTEMPT' && run.status === 'CANDIDATES_READY') : (run.currentAttempt >= 1)
444
451
  if (enteringAttemptFromCandidates) {
452
+ if (transitional && !subproblem) violations.push('ATTEMPT from CANDIDATES_READY requires --subproblem <id>')
445
453
  if (!hasDecisionRecord(scope, 'D3')) violations.push('D3 direction-selection interaction record missing')
446
- if (transitional && !hasSubproblemD2(scope, subproblem)) violations.push(`D2 assumption interaction record missing for subproblem ${subproblem} (run the gate with --subproblem <id>)`)
454
+ if (transitional && subproblem && !hasSubproblemD2(scope, subproblem)) violations.push(`D2 assumption interaction record missing for subproblem ${subproblem} (run the gate with --subproblem <id>)`)
447
455
  }
448
456
  const enteringTerminalFromVerify = transitional
449
457
  ? (FINAL_STATES.includes(to) && run.status === 'VERIFY')
450
458
  : (FINAL_STATES.includes(run.status) && !['BLOCKED', 'CANCELLED'].includes(run.status))
451
- if (enteringTerminalFromVerify && !hasDecisionRecord(scope, 'D4')) violations.push('D4 verdict interaction record missing')
459
+ if (enteringTerminalFromVerify && !hasD4ForAttempt(scope, run.currentAttempt)) violations.push(`D4 verdict interaction record for attempt ${run.currentAttempt} missing`)
452
460
  if (transitional && (to === 'REVISE' || (to === 'RESEARCH' && run.status === 'VERIFY'))) {
453
461
  const ids = decisionStackIds(scope)
454
462
  if (ids.length > 0 && !ids.some(id => typeof reason === 'string' && reason.includes(id))) {
@@ -458,9 +466,12 @@ function interactionContractViolations(run, ledger, to, reason, subproblem) {
458
466
  const terminalAndExempt = transitional
459
467
  ? (FINAL_STATES.includes(to) && !['BLOCKED', 'CANCELLED'].includes(to))
460
468
  : (FINAL_STATES.includes(run.status) && !['BLOCKED', 'CANCELLED'].includes(run.status))
461
- if (terminalAndExempt && scope.robustnessExempt !== true) {
462
- const passed = Array.isArray(ledger.obligations) && ledger.obligations.some(o => o && o.kind === 'robustness' && o.status === 'PASS')
463
- if (!passed) violations.push('terminal state requires a PASSED robustness-kind obligation (or scope.robustnessExempt: true)')
469
+ if (terminalAndExempt) {
470
+ if (scope.robustnessExempt !== true) {
471
+ const passed = Array.isArray(ledger.obligations) && ledger.obligations.some(o => o && o.kind === 'robustness' && o.status === 'PASS')
472
+ if (!passed) violations.push('terminal state requires a PASSED robustness-kind obligation (or scope.robustnessExempt: true)')
473
+ }
474
+ if (scope.cleanupPassed !== true) violations.push('terminal state requires scope.cleanupPassed: true (run the cleanup checklist in run-directory.md)')
464
475
  }
465
476
  return violations
466
477
  }
@@ -550,7 +561,7 @@ async function validateRunUnlocked(root, expectedTaskId) {
550
561
  if (ledger.schemaVersion !== SCHEMA_VERSION) warnings.push(`legacy run (ledger schema v${ledger.schemaVersion}): interaction contract not enforced`)
551
562
  else {
552
563
  for (const violation of interactionContractViolations(run, ledger, undefined, undefined)) {
553
- warnings.push(`interaction contract: ${violation}`)
564
+ diagnostics.push(`interaction contract: ${violation}`)
554
565
  }
555
566
  if (run.currentAttempt >= 1) {
556
567
  const reportPath = join(root, 'attempts', String(run.currentAttempt), 'report.md')
@@ -574,7 +585,7 @@ export async function validateRun(root, expectedTaskId, runtime = {}) {
574
585
  /**
575
586
  * Check the v2 interaction contract for a PROSPECTIVE transition without
576
587
  * mutating anything. The agent workflow must call this before `transition`;
577
- * the CLI `transition` command enforces it internally unless --skip-gate.
588
+ * the CLI `transition` command enforces it internally and has NO escape hatch.
578
589
  */
579
590
  export async function gateTransition(root, { to, reason, subproblem }, runtime = {}) {
580
591
  return withLock(root, MUTATION_LOCK_FILE, 'gate', async () => {
@@ -582,6 +593,23 @@ export async function gateTransition(root, { to, reason, subproblem }, runtime =
582
593
  const run = await readJson(p.run)
583
594
  const ledger = await readJson(p.ledger)
584
595
  const violations = interactionContractViolations(run, ledger, to, reason, subproblem)
596
+ const exempt = ledger.scope?.contractExempt === true
597
+ if (to === 'CANDIDATES_READY' && !exempt) {
598
+ const sourcesPath = join(root, 'research', 'sources.jsonl')
599
+ try {
600
+ if (!(await readFile(sourcesPath, 'utf8')).trim()) violations.push('research/sources.jsonl is empty (pre-modeling literature survey artifacts are mandatory)')
601
+ } catch {
602
+ violations.push('research/sources.jsonl missing (pre-modeling literature survey artifacts are mandatory)')
603
+ }
604
+ }
605
+ if (FINAL_STATES.includes(to) && !['BLOCKED', 'CANCELLED'].includes(to) && !exempt) {
606
+ const reportPath = join(root, 'attempts', String(run.currentAttempt), 'report.md')
607
+ try {
608
+ await readFile(reportPath, 'utf8')
609
+ } catch {
610
+ violations.push(`attempts/${run.currentAttempt}/report.md missing (write the runlog report before a terminal transition)`)
611
+ }
612
+ }
585
613
  return { allowed: violations.length === 0, violations, status: run.status }
586
614
  }, runtime)
587
615
  }
@@ -632,12 +660,10 @@ async function cli(argv) {
632
660
  return result
633
661
  }
634
662
  if (command === 'transition') {
635
- if (options['skip-gate'] !== 'true') {
636
- const gate = await gateTransition(root, { to: options.to, reason: options.reason, subproblem: options.subproblem })
637
- if (!gate.allowed) {
638
- process.exitCode = 1
639
- return { error: `interaction contract violated: ${gate.violations.join('; ')}`, gate }
640
- }
663
+ const gate = await gateTransition(root, { to: options.to, reason: options.reason, subproblem: options.subproblem })
664
+ if (!gate.allowed) {
665
+ process.exitCode = 1
666
+ return { error: `interaction contract violated: ${gate.violations.join('; ')}`, gate }
641
667
  }
642
668
  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 })
643
669
  }