dsh-math-modeling-agent 0.1.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.
Files changed (31) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +127 -0
  3. package/cordis.patch.yml +8 -0
  4. package/package.json +34 -0
  5. package/skills/math-modeling-agent/SKILL.md +50 -0
  6. package/skills/math-modeling-agent/examples/minimal-run/README.md +14 -0
  7. package/skills/math-modeling-agent/examples/resumed-run/README.md +13 -0
  8. package/skills/math-modeling-agent/references/claims-evidence.md +29 -0
  9. package/skills/math-modeling-agent/references/data-subproblems.md +17 -0
  10. package/skills/math-modeling-agent/references/math-grill.md +33 -0
  11. package/skills/math-modeling-agent/references/modeling-methodology.md +38 -0
  12. package/skills/math-modeling-agent/references/problem-types.md +20 -0
  13. package/skills/math-modeling-agent/references/report-contract.md +29 -0
  14. package/skills/math-modeling-agent/references/research-breakthrough.md +27 -0
  15. package/skills/math-modeling-agent/references/state-recovery.md +18 -0
  16. package/skills/math-modeling-agent/references/tool-policy.md +21 -0
  17. package/skills/math-modeling-agent/references/workflow.md +39 -0
  18. package/skills/math-modeling-agent/schemas/attempt.schema.json +70 -0
  19. package/skills/math-modeling-agent/schemas/ledger.schema.json +47 -0
  20. package/skills/math-modeling-agent/schemas/run.schema.json +102 -0
  21. package/skills/math-modeling-agent/scripts/capability-probe.mjs +105 -0
  22. package/skills/math-modeling-agent/scripts/python-environment.mjs +408 -0
  23. package/skills/math-modeling-agent/scripts/run-state.mjs +547 -0
  24. package/skills/math-modeling-audit/SKILL.md +41 -0
  25. package/skills/math-modeling-audit/examples/audit-report.md +13 -0
  26. package/skills/math-modeling-audit/examples/mcm-final-review.md +86 -0
  27. package/skills/math-modeling-audit/references/data-citation-audit.md +15 -0
  28. package/skills/math-modeling-audit/references/evidence-levels.md +10 -0
  29. package/skills/math-modeling-audit/references/mcm-icm-final-judge.md +331 -0
  30. package/skills/math-modeling-audit/references/verification-protocol.md +27 -0
  31. package/skills/math-modeling-audit/scripts/mcm-score.mjs +218 -0
@@ -0,0 +1,547 @@
1
+ #!/usr/bin/env node
2
+ import { access, appendFile, mkdir, open, readFile, rename, rmdir, stat, unlink, writeFile } from 'node:fs/promises'
3
+ import { dirname, join, resolve } from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { isDeepStrictEqual } from 'node:util'
6
+
7
+ export const SCHEMA_VERSION = 1
8
+ export const MODE_DEFAULTS = Object.freeze({
9
+ fast: Object.freeze({ attempts: 2, researchQueries: 0, computeSeconds: 60 }),
10
+ standard: Object.freeze({ attempts: 12, researchQueries: 12, computeSeconds: 1800 }),
11
+ 'high-assurance': Object.freeze({ attempts: 24, researchQueries: 30, computeSeconds: 7200 }),
12
+ })
13
+
14
+ const NON_FINAL_STATES = ['TRIAGE', 'SCOPE_FROZEN', 'INPUT_PROFILED', 'CLAIMS_REGISTERED', 'CANDIDATES_READY', 'ATTEMPT', 'EXECUTE', 'VERIFY', 'REVISE', 'RESEARCH', 'FORK']
15
+ const FINAL_STATES = ['SOLVED', 'PARTIAL', 'CONDITIONAL', 'INCONCLUSIVE', 'REFUTED', 'INFEASIBLE', 'UNIDENTIFIABLE', 'BLOCKED', 'CANCELLED']
16
+ const STATUSES = [...NON_FINAL_STATES, ...FINAL_STATES]
17
+ const STOP_TRANSITIONS = ['BLOCKED', 'CANCELLED']
18
+ const TRANSITIONS = Object.freeze({
19
+ TRIAGE: ['SCOPE_FROZEN', ...STOP_TRANSITIONS],
20
+ SCOPE_FROZEN: ['INPUT_PROFILED', ...STOP_TRANSITIONS],
21
+ INPUT_PROFILED: ['CLAIMS_REGISTERED', ...STOP_TRANSITIONS],
22
+ CLAIMS_REGISTERED: ['CANDIDATES_READY', ...STOP_TRANSITIONS],
23
+ CANDIDATES_READY: ['ATTEMPT', ...STOP_TRANSITIONS],
24
+ ATTEMPT: ['EXECUTE', ...STOP_TRANSITIONS],
25
+ EXECUTE: ['VERIFY', ...STOP_TRANSITIONS],
26
+ VERIFY: ['REVISE', 'RESEARCH', 'FORK', ...FINAL_STATES],
27
+ REVISE: ['ATTEMPT', ...STOP_TRANSITIONS],
28
+ RESEARCH: ['CANDIDATES_READY', ...STOP_TRANSITIONS],
29
+ FORK: ['ATTEMPT', ...STOP_TRANSITIONS],
30
+ })
31
+ const TASK_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
32
+ const RUN_KEYS = ['schemaVersion', 'taskId', 'mode', 'status', 'eventSequence', 'currentAttempt', 'bestCandidateId', 'budget', 'createdAt', 'updatedAt']
33
+ const IMMUTABLE_RUN_KEYS = ['schemaVersion', 'taskId', 'mode', 'budget', 'createdAt']
34
+ const LEDGER_KEYS = ['schemaVersion', 'taskId', 'scope', 'assumptions', 'claims', 'obligations', 'subproblems', 'candidates', 'issues']
35
+ const RUN_FILE = 'run.json'
36
+ const LEDGER_FILE = 'ledger.json'
37
+ const EVENTS_FILE = 'events.jsonl'
38
+ const MUTATION_LOCK_FILE = '.run-state.lock'
39
+ const RECLAIM_GUARD_FILE = '.run-state.reclaim'
40
+
41
+ function clone(value) { return JSON.parse(JSON.stringify(value)) }
42
+ function now() { return new Date().toISOString() }
43
+ function isObject(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) }
44
+ function exactKeys(value, keys) { return isObject(value) && Object.keys(value).length === keys.length && keys.every(key => Object.hasOwn(value, key)) }
45
+ function nonnegativeInteger(value) { return Number.isInteger(value) && value >= 0 }
46
+ function validateBudgetShape(budget, diagnostics, prefix = 'budget') {
47
+ addDiagnostic(diagnostics, isObject(budget), `${prefix} must be an object`)
48
+ if (!isObject(budget)) return
49
+ addDiagnostic(diagnostics, exactKeys(budget, ['attempts', 'researchQueries', 'computeSeconds']), `${prefix} has invalid fields`)
50
+ addDiagnostic(diagnostics, Number.isFinite(budget.attempts) && Number.isInteger(budget.attempts) && budget.attempts >= 1, `${prefix}.attempts must be a finite integer >= 1`)
51
+ addDiagnostic(diagnostics, Number.isFinite(budget.researchQueries) && nonnegativeInteger(budget.researchQueries), `${prefix}.researchQueries must be a finite nonnegative integer`)
52
+ addDiagnostic(diagnostics, Number.isFinite(budget.computeSeconds) && nonnegativeInteger(budget.computeSeconds), `${prefix}.computeSeconds must be a finite nonnegative integer`)
53
+ }
54
+ async function readJson(path) { return JSON.parse(await readFile(path, 'utf8')) }
55
+ const RENAME_DEFAULTS = Object.freeze({ attempts: 5, retryDelayMs: 5 })
56
+ const WINDOWS_SHARING_ERRORS = new Set(['EPERM', 'EBUSY', 'EACCES'])
57
+ async function renameWithRetry(source, target, runtime = {}) {
58
+ const options = { ...RENAME_DEFAULTS, ...(runtime.rename ?? {}) }
59
+ const renameFile = runtime.fs?.rename ?? rename
60
+ for (let attempt = 1; ; attempt += 1) {
61
+ try { return await renameFile(source, target) }
62
+ catch (error) {
63
+ if (!WINDOWS_SHARING_ERRORS.has(error.code) || attempt >= options.attempts) {
64
+ if (WINDOWS_SHARING_ERRORS.has(error.code)) error.message = `atomic rename failed after ${attempt} attempts: ${error.message}`
65
+ throw error
66
+ }
67
+ await sleep(options.retryDelayMs * attempt)
68
+ }
69
+ }
70
+ }
71
+ async function unlinkWithRetry(path, unlinkFile = unlink, options = RENAME_DEFAULTS) {
72
+ for (let attempt = 1; ; attempt += 1) {
73
+ try { await unlinkFile(path); return true }
74
+ catch (error) {
75
+ if (error.code === 'ENOENT') return false
76
+ if (!WINDOWS_SHARING_ERRORS.has(error.code) || attempt >= options.attempts) throw error
77
+ await sleep(options.retryDelayMs * attempt)
78
+ }
79
+ }
80
+ }
81
+ async function atomicWrite(path, value, runtime = {}) {
82
+ await mkdir(dirname(path), { recursive: true })
83
+ const temporary = `${path}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`
84
+ await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, 'utf8')
85
+ try { await renameWithRetry(temporary, path, runtime) }
86
+ finally { await unlink(temporary).catch(error => { if (error.code !== 'ENOENT') throw error }) }
87
+ }
88
+ function paths(root) { return { run: join(root, RUN_FILE), ledger: join(root, LEDGER_FILE), events: join(root, EVENTS_FILE) } }
89
+ const LOCK_DEFAULTS = Object.freeze({ timeoutMs: 2000, retryDelayMs: 10, staleMs: 30_000 })
90
+ function sleep(milliseconds) { return new Promise(resolve => setTimeout(resolve, milliseconds)) }
91
+ function processIsAlive(pid) {
92
+ try { process.kill(pid, 0); return true }
93
+ catch (error) { return error.code === 'EPERM' }
94
+ }
95
+ function lockBusyError(operation) {
96
+ const error = new Error(`LOCK_BUSY: timed out waiting for run-state lock during ${operation}`)
97
+ error.code = 'LOCK_BUSY'
98
+ return error
99
+ }
100
+ async function readLockMetadata(lockPath) {
101
+ try { return JSON.parse(await readFile(lockPath, 'utf8')) } catch { return null }
102
+ }
103
+ async function validCompleteRunExists(root) {
104
+ try {
105
+ const p = paths(root)
106
+ validateAuthoritativeState(await readJson(p.run), await readJson(p.ledger), await readEvents(p.events))
107
+ return true
108
+ } catch { return false }
109
+ }
110
+ async function cleanupDeadInitAttempt(root, metadata) {
111
+ if (metadata?.operation !== 'init' || await validCompleteRunExists(root)) return
112
+ const owned = Array.isArray(metadata.ownedArtifacts) ? metadata.ownedArtifacts : []
113
+ for (const file of owned) {
114
+ if ([RUN_FILE, LEDGER_FILE, EVENTS_FILE].includes(file)) await unlink(join(root, file)).catch(error => { if (error.code !== 'ENOENT') throw error })
115
+ }
116
+ if (typeof metadata.stagingDir === 'string' && metadata.stagingDir.startsWith('.run-state-init-')) {
117
+ const stagingRoot = join(root, metadata.stagingDir)
118
+ for (const file of [RUN_FILE, LEDGER_FILE, EVENTS_FILE]) await unlink(join(stagingRoot, file)).catch(error => { if (error.code !== 'ENOENT') throw error })
119
+ await rmdir(stagingRoot).catch(error => { if (!['ENOENT', 'ENOTEMPTY'].includes(error.code)) throw error })
120
+ }
121
+ }
122
+ async function reclaimGuardIsBusy(path, runtime = {}) {
123
+ const accessFile = runtime.fs?.access ?? access
124
+ try { await accessFile(path); return true }
125
+ catch (error) {
126
+ if (error.code === 'ENOENT') return false
127
+ if (WINDOWS_SHARING_ERRORS.has(error.code)) return true
128
+ throw error
129
+ }
130
+ }
131
+ function sameLockGeneration(observedMetadata, observedStat, currentMetadata, currentStat) {
132
+ if (observedMetadata === null || currentMetadata === null) {
133
+ return observedMetadata === null && currentMetadata === null
134
+ && observedStat.dev === currentStat.dev
135
+ && observedStat.ino === currentStat.ino
136
+ && observedStat.size === currentStat.size
137
+ && observedStat.mtimeMs === currentStat.mtimeMs
138
+ }
139
+ return typeof observedMetadata.ownerId === 'string'
140
+ && currentMetadata.ownerId === observedMetadata.ownerId
141
+ && observedStat.dev === currentStat.dev
142
+ && observedStat.ino === currentStat.ino
143
+ && observedStat.size === currentStat.size
144
+ && observedStat.mtimeMs === currentStat.mtimeMs
145
+ }
146
+ function demonstrablyDeadAndStale(metadata, lockStat, lockOptions) {
147
+ const acquiredAt = Date.parse(metadata?.acquiredAt)
148
+ const staleSince = Number.isFinite(acquiredAt) ? acquiredAt : lockStat.mtimeMs
149
+ return typeof metadata?.ownerId === 'string'
150
+ && Number.isInteger(metadata.pid)
151
+ && Date.now() - staleSince >= lockOptions.staleMs
152
+ && !lockOptions.isProcessAlive(metadata.pid)
153
+ }
154
+
155
+ function isReclaimableLock(metadata, lockStat, lockOptions) {
156
+ if (metadata === null) return Date.now() - lockStat.mtimeMs >= lockOptions.staleMs
157
+ return demonstrablyDeadAndStale(metadata, lockStat, lockOptions)
158
+ }
159
+
160
+ async function reclaimStaleGuard(guardPath, lockOptions, runtime = {}) {
161
+ const statFile = runtime.fs?.stat ?? stat
162
+ const unlinkFile = runtime.fs?.unlink ?? unlink
163
+ try {
164
+ const guardStat = await statFile(guardPath)
165
+ if (Date.now() - guardStat.mtimeMs < lockOptions.staleMs) return false
166
+ const guardMetadata = await readLockMetadata(guardPath)
167
+ if (guardMetadata !== null && typeof guardMetadata.ownerId !== 'string') return false
168
+ await unlinkWithRetry(guardPath, unlinkFile)
169
+ return true
170
+ } catch (error) {
171
+ if (error.code === 'ENOENT') return true
172
+ throw error
173
+ }
174
+ }
175
+ async function reclaimDeadStaleLock(root, lockPath, guardPath, lockOptions) {
176
+ let observedMetadata, observedStat
177
+ try { [observedMetadata, observedStat] = await Promise.all([readLockMetadata(lockPath), stat(lockPath)]) }
178
+ catch (error) { if (error.code === 'ENOENT') return true; throw error }
179
+ if (!isReclaimableLock(observedMetadata, observedStat, lockOptions)) return false
180
+ const guardOwnerId = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`
181
+ let guard
182
+ try {
183
+ guard = await open(guardPath, 'wx')
184
+ await guard.writeFile(JSON.stringify({ ownerId: guardOwnerId }), 'utf8')
185
+ } catch (error) {
186
+ if (guard) {
187
+ await guard.close()
188
+ const currentGuard = await readLockMetadata(guardPath)
189
+ if (currentGuard?.ownerId === guardOwnerId) await unlinkWithRetry(guardPath)
190
+ }
191
+ if (['EEXIST', 'EPERM', 'EBUSY', 'EACCES'].includes(error.code)) return false
192
+ throw error
193
+ }
194
+ try {
195
+ let currentMetadata, currentStat
196
+ try { [currentMetadata, currentStat] = await Promise.all([readLockMetadata(lockPath), stat(lockPath)]) }
197
+ catch (error) { if (error.code === 'ENOENT') return true; throw error }
198
+ if (!sameLockGeneration(observedMetadata, observedStat, currentMetadata, currentStat)) return false
199
+ if (!isReclaimableLock(currentMetadata, currentStat, lockOptions)) return false
200
+ await cleanupDeadInitAttempt(root, currentMetadata)
201
+ const finalMetadata = await readLockMetadata(lockPath)
202
+ if (finalMetadata?.ownerId !== observedMetadata?.ownerId) return false
203
+ await unlinkWithRetry(lockPath)
204
+ return true
205
+ } finally {
206
+ await guard.close()
207
+ const currentGuard = await readLockMetadata(guardPath)
208
+ if (currentGuard?.ownerId === guardOwnerId) await unlinkWithRetry(guardPath)
209
+ }
210
+ }
211
+ async function withLock(root, file, operationName, operation, runtime = {}) {
212
+ await mkdir(root, { recursive: true })
213
+ const lockPath = join(root, file)
214
+ const guardPath = join(root, RECLAIM_GUARD_FILE)
215
+ const lockOptions = { ...LOCK_DEFAULTS, ...(runtime.lock ?? {}), isProcessAlive: runtime.lock?.isProcessAlive ?? processIsAlive }
216
+ const deadline = Date.now() + lockOptions.timeoutMs
217
+ const ownerId = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`
218
+ let lock
219
+ let metadata = { pid: process.pid, acquiredAt: now(), operation: operationName, ownerId }
220
+ while (!lock) {
221
+ if (await reclaimGuardIsBusy(guardPath, runtime)) {
222
+ if (await reclaimStaleGuard(guardPath, lockOptions, runtime)) {
223
+ if (Date.now() >= deadline) throw lockBusyError(operationName)
224
+ await sleep(lockOptions.retryDelayMs)
225
+ continue
226
+ }
227
+ if (Date.now() >= deadline) throw lockBusyError(operationName)
228
+ await sleep(lockOptions.retryDelayMs)
229
+ continue
230
+ }
231
+ try {
232
+ const candidate = await open(lockPath, 'wx')
233
+ if (await reclaimGuardIsBusy(guardPath, runtime)) {
234
+ await candidate.close()
235
+ await unlinkWithRetry(lockPath)
236
+ if (await reclaimStaleGuard(guardPath, lockOptions, runtime)) {
237
+ if (Date.now() >= deadline) throw lockBusyError(operationName)
238
+ await sleep(lockOptions.retryDelayMs)
239
+ continue
240
+ }
241
+ if (Date.now() >= deadline) throw lockBusyError(operationName)
242
+ await sleep(lockOptions.retryDelayMs)
243
+ continue
244
+ }
245
+ lock = candidate
246
+ await lock.writeFile(JSON.stringify(metadata), 'utf8')
247
+ } catch (error) {
248
+ if (!['EEXIST', 'EPERM', 'EBUSY', 'EACCES'].includes(error.code)) throw error
249
+ if (error.code === 'EEXIST' && await reclaimDeadStaleLock(root, lockPath, guardPath, lockOptions)) continue
250
+ if (Date.now() >= deadline) throw lockBusyError(operationName)
251
+ await sleep(lockOptions.retryDelayMs)
252
+ }
253
+ }
254
+ const updateMetadata = async patch => {
255
+ metadata = { ...metadata, ...patch }
256
+ await writeFile(lockPath, JSON.stringify(metadata), 'utf8')
257
+ }
258
+ try {
259
+ await runtime.hooks?.afterLockAcquired?.({ operation: operationName, ownerId })
260
+ return await operation({ ownerId, updateMetadata })
261
+ } finally {
262
+ await lock.close()
263
+ const current = await readLockMetadata(lockPath)
264
+ if (current?.ownerId === ownerId) await unlinkWithRetry(lockPath)
265
+ }
266
+ }
267
+ async function assertRunFilesDoNotExist(p) {
268
+ for (const path of [p.run, p.ledger, p.events]) {
269
+ try { await access(path); throw new Error('contracted run file already exists') }
270
+ catch (error) { if (error.code !== 'ENOENT') throw error }
271
+ }
272
+ }
273
+ function assertTaskId(taskId) { if (!TASK_ID.test(taskId ?? '')) throw new Error('taskId must be kebab-case') }
274
+ function assertMode(mode) { if (!MODE_DEFAULTS[mode]) throw new Error(`unsupported mode: ${mode}`) }
275
+ function linkedIds(input) { return [...(input.evidenceIds ?? []), ...(input.issueIds ?? [])] }
276
+ function isAllowedTransition(from, to) { return (TRANSITIONS[from] ?? []).includes(to) }
277
+ function illegalTransitionMessage(from, to) { return `illegal transition ${from} -> ${to}` }
278
+ function nextAttempt(currentAttempt, to) { return currentAttempt + (to === 'ATTEMPT' ? 1 : 0) }
279
+ function addDiagnostic(diagnostics, condition, message) { if (!condition) diagnostics.push(message) }
280
+ function deeplyEqual(left, right) { return isDeepStrictEqual(left, right) }
281
+
282
+ function validateRunShape(run, diagnostics, prefix = 'run') {
283
+ addDiagnostic(diagnostics, exactKeys(run, RUN_KEYS), `${prefix} must contain exactly the contracted fields`)
284
+ if (!isObject(run)) return
285
+ addDiagnostic(diagnostics, run.schemaVersion === SCHEMA_VERSION, `${prefix}.schemaVersion must equal ${SCHEMA_VERSION}`)
286
+ addDiagnostic(diagnostics, typeof run.taskId === 'string' && TASK_ID.test(run.taskId), `${prefix}.taskId must be kebab-case`)
287
+ addDiagnostic(diagnostics, Object.hasOwn(MODE_DEFAULTS, run.mode), `${prefix}.mode is invalid`)
288
+ addDiagnostic(diagnostics, STATUSES.includes(run.status), `${prefix}.status is invalid`)
289
+ addDiagnostic(diagnostics, nonnegativeInteger(run.eventSequence), `${prefix}.eventSequence must be a nonnegative integer`)
290
+ addDiagnostic(diagnostics, nonnegativeInteger(run.currentAttempt), `${prefix}.currentAttempt must be a nonnegative integer`)
291
+ addDiagnostic(diagnostics, run.bestCandidateId === null || typeof run.bestCandidateId === 'string', `${prefix}.bestCandidateId must be a string or null`)
292
+ validateBudgetShape(run.budget, diagnostics, `${prefix}.budget`)
293
+ addDiagnostic(diagnostics, typeof run.createdAt === 'string' && run.createdAt.length > 0, `${prefix}.createdAt must be a string`)
294
+ addDiagnostic(diagnostics, typeof run.updatedAt === 'string' && run.updatedAt.length > 0, `${prefix}.updatedAt must be a string`)
295
+ }
296
+
297
+ function validateLedgerShape(ledger, diagnostics) {
298
+ addDiagnostic(diagnostics, exactKeys(ledger, LEDGER_KEYS), 'ledger must contain exactly the contracted fields')
299
+ if (!isObject(ledger)) return
300
+ addDiagnostic(diagnostics, ledger.schemaVersion === SCHEMA_VERSION, `ledger.schemaVersion must equal ${SCHEMA_VERSION}`)
301
+ addDiagnostic(diagnostics, typeof ledger.taskId === 'string' && TASK_ID.test(ledger.taskId), 'ledger.taskId must be kebab-case')
302
+ addDiagnostic(diagnostics, isObject(ledger.scope), 'ledger.scope must be an object')
303
+ for (const key of ['assumptions', 'claims', 'obligations', 'subproblems', 'candidates', 'issues']) addDiagnostic(diagnostics, Array.isArray(ledger[key]), `ledger.${key} must be an array`)
304
+ }
305
+
306
+ function validateEventShape(event, diagnostics, index) {
307
+ const prefix = `event[${index}]`
308
+ addDiagnostic(diagnostics, isObject(event), `${prefix} must be an object`)
309
+ if (!isObject(event)) return
310
+ const expectedType = index === 0 ? 'RUN_INITIALIZED' : 'STATUS_TRANSITION'
311
+ const expectedKeys = expectedType === 'RUN_INITIALIZED'
312
+ ? ['schemaVersion', 'sequence', 'type', 'taskId', 'timestamp', 'snapshot']
313
+ : ['schemaVersion', 'sequence', 'type', 'taskId', 'from', 'to', 'reason', 'evidenceIds', 'issueIds', 'timestamp', 'snapshot']
314
+ 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}`)
316
+ addDiagnostic(diagnostics, event.sequence === index, `${prefix}.sequence must equal ${index}`)
317
+ addDiagnostic(diagnostics, typeof event.taskId === 'string' && TASK_ID.test(event.taskId), `${prefix}.taskId must be kebab-case`)
318
+ addDiagnostic(diagnostics, event.type === expectedType, `${prefix}.type must be ${expectedType}`)
319
+ addDiagnostic(diagnostics, typeof event.timestamp === 'string' && event.timestamp.length > 0, `${prefix}.timestamp must be a string`)
320
+ addDiagnostic(diagnostics, isObject(event.snapshot), `${prefix}.snapshot must be an object`)
321
+ if (isObject(event.snapshot)) {
322
+ validateRunShape(event.snapshot, diagnostics, `${prefix}.snapshot`)
323
+ addDiagnostic(diagnostics, event.snapshot.taskId === event.taskId, `${prefix} snapshot task mismatch`)
324
+ addDiagnostic(diagnostics, event.snapshot.eventSequence === event.sequence, `${prefix} snapshot sequence mismatch`)
325
+ if (expectedType === 'RUN_INITIALIZED') {
326
+ addDiagnostic(diagnostics, event.snapshot.status === 'TRIAGE', `${prefix}.snapshot.status must be TRIAGE`)
327
+ addDiagnostic(diagnostics, event.snapshot.eventSequence === 0, `${prefix}.snapshot.eventSequence must equal 0`)
328
+ addDiagnostic(diagnostics, event.snapshot.currentAttempt === 0, `${prefix}.snapshot.currentAttempt must equal 0`)
329
+ addDiagnostic(diagnostics, event.snapshot.bestCandidateId === null, `${prefix}.snapshot.bestCandidateId must be null`)
330
+ }
331
+ }
332
+ if (expectedType === 'STATUS_TRANSITION') {
333
+ addDiagnostic(diagnostics, STATUSES.includes(event.from) && STATUSES.includes(event.to), `${prefix} transition states are invalid`)
334
+ addDiagnostic(diagnostics, typeof event.reason === 'string' && event.reason.trim().length > 0, `${prefix}.reason must be a nonempty string`)
335
+ const evidenceIdsValid = Array.isArray(event.evidenceIds) && event.evidenceIds.every(id => typeof id === 'string')
336
+ const issueIdsValid = Array.isArray(event.issueIds) && event.issueIds.every(id => typeof id === 'string')
337
+ addDiagnostic(diagnostics, evidenceIdsValid, `${prefix}.evidenceIds must be a string array`)
338
+ addDiagnostic(diagnostics, issueIdsValid, `${prefix}.issueIds must be a string array`)
339
+ addDiagnostic(diagnostics, evidenceIdsValid && issueIdsValid && linkedIds(event).length > 0, `${prefix} requires at least one evidence or issue ID`)
340
+ }
341
+ }
342
+
343
+ function validateJournal(events, diagnostics) {
344
+ if (events.length === 0) { diagnostics.push('event journal is empty'); return }
345
+ const taskId = events[0]?.taskId
346
+ const initializationSnapshot = events[0]?.snapshot
347
+ events.forEach((event, index) => {
348
+ validateEventShape(event, diagnostics, index)
349
+ addDiagnostic(diagnostics, event?.sequence === index, 'non-monotonic events')
350
+ addDiagnostic(diagnostics, event?.taskId === taskId, `event[${index}] task mismatch`)
351
+ if (index > 0) {
352
+ for (const key of IMMUTABLE_RUN_KEYS) {
353
+ addDiagnostic(diagnostics, deeplyEqual(event?.snapshot?.[key], initializationSnapshot?.[key]), `event[${index}] snapshot immutable field ${key} mismatch`)
354
+ }
355
+ const previousSnapshot = events[index - 1]?.snapshot
356
+ const previousStatus = previousSnapshot?.status
357
+ addDiagnostic(diagnostics, event?.from === previousStatus, `event[${index}] transition from does not match previous snapshot`)
358
+ addDiagnostic(diagnostics, event?.to === event?.snapshot?.status, `event[${index}] transition to does not match snapshot`)
359
+ addDiagnostic(diagnostics, isAllowedTransition(event?.from, event?.to), illegalTransitionMessage(event?.from, event?.to))
360
+ addDiagnostic(diagnostics, !FINAL_STATES.includes(previousStatus), `terminal state ${previousStatus} cannot have later events`)
361
+ const expectedAttempt = nextAttempt(previousSnapshot?.currentAttempt, event?.to)
362
+ addDiagnostic(diagnostics, event?.snapshot?.currentAttempt === expectedAttempt, `event[${index}] currentAttempt delta is invalid`)
363
+ }
364
+ })
365
+ }
366
+
367
+ export async function initRun(root, { taskId, mode = 'standard', budget, budgets }, runtime = {}) {
368
+ assertTaskId(taskId); assertMode(mode)
369
+ const effectiveBudget = { ...MODE_DEFAULTS[mode], ...(budget ?? budgets ?? {}) }
370
+ const budgetDiagnostics = []
371
+ validateBudgetShape(effectiveBudget, budgetDiagnostics)
372
+ if (budgetDiagnostics.length > 0) throw new Error(budgetDiagnostics.join('; '))
373
+ return withLock(root, MUTATION_LOCK_FILE, 'init', async ({ ownerId, updateMetadata }) => {
374
+ const timestamp = now()
375
+ const run = {
376
+ schemaVersion: SCHEMA_VERSION, taskId, mode, status: 'TRIAGE', eventSequence: 0, currentAttempt: 0,
377
+ bestCandidateId: null, budget: effectiveBudget, createdAt: timestamp, updatedAt: timestamp,
378
+ }
379
+ const ledger = {
380
+ schemaVersion: SCHEMA_VERSION, taskId, scope: { independentAuditPassed: false }, assumptions: [], claims: [],
381
+ obligations: [], subproblems: [], candidates: [], issues: [],
382
+ }
383
+ const event = { schemaVersion: SCHEMA_VERSION, sequence: 0, type: 'RUN_INITIALIZED', taskId, timestamp, snapshot: clone(run) }
384
+ const p = paths(root)
385
+ await assertRunFilesDoNotExist(p)
386
+ const stagingName = `.run-state-init-${ownerId}`
387
+ const stagingRoot = join(root, stagingName)
388
+ const staged = paths(stagingRoot)
389
+ const published = []
390
+ await mkdir(stagingRoot)
391
+ await updateMetadata({ stagingDir: stagingName, ownedArtifacts: [LEDGER_FILE, EVENTS_FILE, RUN_FILE] })
392
+ try {
393
+ await writeFile(staged.ledger, `${JSON.stringify(ledger, null, 2)}\n`, { encoding: 'utf8', flag: 'wx' })
394
+ await writeFile(staged.events, `${JSON.stringify(event)}\n`, { encoding: 'utf8', flag: 'wx' })
395
+ await writeFile(staged.run, `${JSON.stringify(run, null, 2)}\n`, { encoding: 'utf8', flag: 'wx' })
396
+ for (const [source, target] of [[staged.ledger, p.ledger], [staged.events, p.events], [staged.run, p.run]]) {
397
+ await renameWithRetry(source, target, runtime)
398
+ published.push(target)
399
+ await runtime.hooks?.afterPublish?.({ file: target, count: published.length })
400
+ }
401
+ return clone(run)
402
+ } catch (error) {
403
+ for (const target of published.reverse()) await unlink(target).catch(cleanupError => { if (cleanupError.code !== 'ENOENT') throw cleanupError })
404
+ throw error
405
+ } finally {
406
+ for (const path of [staged.run, staged.events, staged.ledger]) await unlink(path).catch(error => { if (error.code !== 'ENOENT') throw error })
407
+ await rmdir(stagingRoot).catch(error => { if (!['ENOENT', 'ENOTEMPTY'].includes(error.code)) throw error })
408
+ }
409
+ }, runtime)
410
+ }
411
+
412
+ function enforceSolvedGate(run, ledger) {
413
+ if (ledger.claims.length < 1) throw new Error('SOLVED requires at least one claim')
414
+ if (ledger.obligations.some(item => item.required !== false && item.status !== 'PASS')) throw new Error('required obligations remain open')
415
+ if (ledger.issues.some(item => item.severity === 'critical' && item.status !== 'CLOSED')) throw new Error('critical issues remain open')
416
+ if (run.mode === 'high-assurance' && ledger.scope.independentAuditPassed !== true) throw new Error('independent audit must pass')
417
+ }
418
+
419
+ function validateTransitionInput(input) {
420
+ if (typeof input?.reason !== 'string' || input.reason.trim().length === 0) throw new Error('transition reason must be a nonempty string')
421
+ for (const key of ['evidenceIds', 'issueIds']) {
422
+ if (input[key] !== undefined && (!Array.isArray(input[key]) || input[key].some(id => typeof id !== 'string'))) throw new Error(`${key} must be an array of strings`)
423
+ }
424
+ if (linkedIds(input).length === 0) throw new Error('transition requires at least one evidence or issue ID')
425
+ if (input.patch !== undefined && (!isObject(input.patch) || Object.keys(input.patch).some(key => key !== 'bestCandidateId'))) throw new Error('patch may only contain bestCandidateId')
426
+ if (input.patch && (!Object.hasOwn(input.patch, 'bestCandidateId') || (input.patch.bestCandidateId !== null && typeof input.patch.bestCandidateId !== 'string'))) throw new Error('bestCandidateId must be a string or null')
427
+ }
428
+
429
+ function validateAuthoritativeState(run, ledger, events) {
430
+ const diagnostics = []
431
+ validateRunShape(run, diagnostics)
432
+ validateLedgerShape(ledger, diagnostics)
433
+ validateJournal(events, diagnostics)
434
+ if (run.taskId !== ledger.taskId || events.some(event => event.taskId !== run.taskId)) diagnostics.push('task mismatch')
435
+ const last = events.at(-1)
436
+ if (!last || run.eventSequence !== last.sequence) diagnostics.push('state/journal sequence mismatch')
437
+ if (!last || !deeplyEqual(run, last.snapshot)) diagnostics.push('state/journal snapshot mismatch')
438
+ if (diagnostics.length > 0) throw new Error(`invalid run state: ${diagnostics.join('; ')}`)
439
+ }
440
+
441
+ export async function transitionRun(root, input, runtime = {}) {
442
+ validateTransitionInput(input)
443
+ return withLock(root, MUTATION_LOCK_FILE, 'transition', async () => {
444
+ const p = paths(root)
445
+ const run = await readJson(p.run)
446
+ const ledger = await readJson(p.ledger)
447
+ const events = await readEvents(p.events)
448
+ validateAuthoritativeState(run, ledger, events)
449
+ if (!isAllowedTransition(run.status, input.to)) throw new Error(illegalTransitionMessage(run.status, input.to))
450
+ if (input.to === 'SOLVED') enforceSolvedGate(run, ledger)
451
+ const timestamp = now()
452
+ const bestCandidateId = input.patch && Object.hasOwn(input.patch, 'bestCandidateId') ? input.patch.bestCandidateId : run.bestCandidateId
453
+ const next = { ...run, status: input.to, currentAttempt: nextAttempt(run.currentAttempt, input.to), eventSequence: run.eventSequence + 1, bestCandidateId, updatedAt: timestamp }
454
+ const event = {
455
+ schemaVersion: SCHEMA_VERSION, sequence: next.eventSequence, type: 'STATUS_TRANSITION', taskId: run.taskId,
456
+ from: run.status, to: input.to, reason: input.reason, evidenceIds: input.evidenceIds ?? [], issueIds: input.issueIds ?? [], timestamp, snapshot: clone(next),
457
+ }
458
+ const prospectiveEvents = [...events, event]
459
+ validateAuthoritativeState(next, ledger, prospectiveEvents)
460
+ await appendFile(p.events, `${JSON.stringify(event)}\n`, 'utf8')
461
+ await atomicWrite(p.run, next, runtime)
462
+ return clone(next)
463
+ }, runtime)
464
+ }
465
+
466
+ async function readEvents(path) {
467
+ const text = await readFile(path, 'utf8')
468
+ if (!text.trim()) return []
469
+ return text.trimEnd().split(/\r?\n/).map((line, index) => {
470
+ try { return JSON.parse(line) } catch { throw new Error(`invalid event JSON at line ${index + 1}`) }
471
+ })
472
+ }
473
+
474
+ async function validateRunUnlocked(root, expectedTaskId) {
475
+ const diagnostics = []
476
+ let run, ledger, events
477
+ const p = paths(root)
478
+ try { run = await readJson(p.run) } catch (error) { diagnostics.push(`run unreadable: ${error.message}`) }
479
+ try { ledger = await readJson(p.ledger) } catch (error) { diagnostics.push(`ledger unreadable: ${error.message}`) }
480
+ try { events = await readEvents(p.events) } catch (error) { diagnostics.push(error.message) }
481
+ if (!run || !ledger || !events) return { valid: false, diagnostics }
482
+ validateRunShape(run, diagnostics)
483
+ validateLedgerShape(ledger, diagnostics)
484
+ validateJournal(events, diagnostics)
485
+ if (run.taskId !== ledger.taskId || events.some(event => event.taskId !== run.taskId) || (expectedTaskId && expectedTaskId !== run.taskId)) diagnostics.push('task mismatch')
486
+ const last = events.at(-1)
487
+ if (last && run.eventSequence !== last.sequence) diagnostics.push('state/journal sequence mismatch')
488
+ 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) }
490
+ }
491
+
492
+ export async function validateRun(root, expectedTaskId, runtime = {}) {
493
+ return withLock(root, MUTATION_LOCK_FILE, 'validate', () => validateRunUnlocked(root, expectedTaskId), runtime)
494
+ }
495
+
496
+ export async function recoverRun(root, runtime = {}) {
497
+ return withLock(root, MUTATION_LOCK_FILE, 'recover', async () => {
498
+ const p = paths(root)
499
+ const events = await readEvents(p.events)
500
+ if (events.length === 0) throw new Error('event journal is empty')
501
+ const last = events.at(-1)
502
+ const diagnostics = []
503
+ validateJournal(events, diagnostics)
504
+ if (diagnostics.length > 0) throw new Error(`malformed journal: ${diagnostics.join('; ')}`)
505
+ const solvedEvent = events.find(event => event.to === 'SOLVED')
506
+ if (solvedEvent) {
507
+ const ledger = await readJson(p.ledger)
508
+ enforceSolvedGate(solvedEvent.snapshot, ledger)
509
+ }
510
+ await runtime.hooks?.afterJournalValidated?.()
511
+ const snapshot = clone(last.snapshot)
512
+ await atomicWrite(p.run, snapshot, runtime)
513
+ return snapshot
514
+ }, runtime)
515
+ }
516
+
517
+ async function statusRunUnlocked(root) { return readJson(paths(root).run) }
518
+ export async function statusRun(root, runtime = {}) {
519
+ return withLock(root, MUTATION_LOCK_FILE, 'status', () => statusRunUnlocked(root), runtime)
520
+ }
521
+
522
+ function parseOptions(args) {
523
+ const options = { _: [] }
524
+ for (let index = 0; index < args.length; index += 1) {
525
+ const token = args[index]
526
+ if (!token.startsWith('--')) options._.push(token)
527
+ else { const key = token.slice(2); const value = args[index + 1]; options[key] = value; index += 1 }
528
+ }
529
+ return options
530
+ }
531
+ async function cli(argv) {
532
+ const [command, rootArg = '.', ...rest] = argv
533
+ const root = resolve(rootArg)
534
+ const options = parseOptions(rest)
535
+ 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 })
537
+ if (command === 'validate') {
538
+ const result = await validateRun(root, options['task-id'])
539
+ if (!result.valid) process.exitCode = 1
540
+ return result
541
+ }
542
+ if (command === 'recover') return recoverRun(root)
543
+ if (command === 'status') return statusRun(root)
544
+ throw new Error('usage: run-state.mjs <init|transition|validate|recover|status> <run-directory> [options]')
545
+ }
546
+ const invoked = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)
547
+ 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 })
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: math-modeling-audit
3
+ description: This skill should be used when the user asks to "独立核验数学模型", "检查推导", "寻找反例", "审计建模报告", "按 MCM/ICM 终审框架打分", "verify this model", or needs an artifact-only adversarial audit of mathematical claims, data, code, results, citations, or a competition paper.
4
+ whenToUse: Use for existing artifacts and papers; do not take over open-ended model construction or silently rewrite the audited work.
5
+ user-invocable: true
6
+ ---
7
+
8
+ # Math Modeling Audit
9
+
10
+ ## Goal
11
+
12
+ Independently decide which claims in an existing artifact pass, fail, or remain inconclusive, and why.
13
+
14
+ ## Interface
15
+
16
+ Accept a paper, model, derivation, code/result artifact, or MathModelingAgent run. Optionally accept the original problem, competition year/code, target claims, and assurance level.
17
+
18
+ Return per-claim verdicts, evidence levels, counterexamples, reproducibility findings, residual risk, and—only for MCM/ICM judging intent—the fixed final-panel report.
19
+
20
+ ## Workflow
21
+
22
+ 1. Freeze the artifact set and record review coverage.
23
+ 2. Reconstruct claims, assumptions, obligations, and evidence without trusting the author’s summary.
24
+ 3. Apply `references/verification-protocol.md` and `references/evidence-levels.md`.
25
+ 4. Audit data, parameters, leakage, citations, and reproducibility with `references/data-citation-audit.md`.
26
+ 5. Recompute applicable formulas and results through independent tools.
27
+ 6. Seek boundary cases, counterexamples, alternative explanations, and simpler models.
28
+ 7. Output PASS, FAIL, or INCONCLUSIVE per claim; do not silently repair the source.
29
+ 8. For MCM/ICM scoring intent only, load `references/mcm-icm-final-judge.md`, validate arithmetic with `scripts/mcm-score.mjs`, and preserve its fourteen-section output.
30
+
31
+ ## Invariants
32
+
33
+ - Paper prose and solver success are not verification artifacts.
34
+ - Missing evidence is reported, never supplied on the author’s behalf.
35
+ - Lean verifies the formal statement, not its natural-language fidelity or data pipeline.
36
+ - A score never feeds back into the solver’s SOLVED gate.
37
+ - Disqualification risk stops ordinary award scoring.
38
+
39
+ ## Boundaries
40
+
41
+ Do not edit the audited artifact. Do not reward complexity or presentation without evidence. Do not load the large MCM/ICM rubric for generic audits.
@@ -0,0 +1,13 @@
1
+ # Audit Example
2
+
3
+ ## Claim C-004
4
+
5
+ Claim: “The returned point is the global optimum.”
6
+
7
+ Evidence supplied: one successful SLSQP run.
8
+
9
+ Verdict: INCONCLUSIVE.
10
+
11
+ Reason: solver success supports a stationary/best-found point only. The artifact contains no convexity proof, certified bound, multi-start/global search, or optimality gap.
12
+
13
+ Required upgrade: weaken the claim to “best found local solution” or add an appropriate global-optimality certificate.