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.
- package/LICENSE +21 -0
- package/README.md +127 -0
- package/cordis.patch.yml +8 -0
- package/package.json +34 -0
- package/skills/math-modeling-agent/SKILL.md +50 -0
- package/skills/math-modeling-agent/examples/minimal-run/README.md +14 -0
- package/skills/math-modeling-agent/examples/resumed-run/README.md +13 -0
- package/skills/math-modeling-agent/references/claims-evidence.md +29 -0
- package/skills/math-modeling-agent/references/data-subproblems.md +17 -0
- package/skills/math-modeling-agent/references/math-grill.md +33 -0
- package/skills/math-modeling-agent/references/modeling-methodology.md +38 -0
- package/skills/math-modeling-agent/references/problem-types.md +20 -0
- package/skills/math-modeling-agent/references/report-contract.md +29 -0
- package/skills/math-modeling-agent/references/research-breakthrough.md +27 -0
- package/skills/math-modeling-agent/references/state-recovery.md +18 -0
- package/skills/math-modeling-agent/references/tool-policy.md +21 -0
- package/skills/math-modeling-agent/references/workflow.md +39 -0
- package/skills/math-modeling-agent/schemas/attempt.schema.json +70 -0
- package/skills/math-modeling-agent/schemas/ledger.schema.json +47 -0
- package/skills/math-modeling-agent/schemas/run.schema.json +102 -0
- package/skills/math-modeling-agent/scripts/capability-probe.mjs +105 -0
- package/skills/math-modeling-agent/scripts/python-environment.mjs +408 -0
- package/skills/math-modeling-agent/scripts/run-state.mjs +547 -0
- package/skills/math-modeling-audit/SKILL.md +41 -0
- package/skills/math-modeling-audit/examples/audit-report.md +13 -0
- package/skills/math-modeling-audit/examples/mcm-final-review.md +86 -0
- package/skills/math-modeling-audit/references/data-citation-audit.md +15 -0
- package/skills/math-modeling-audit/references/evidence-levels.md +10 -0
- package/skills/math-modeling-audit/references/mcm-icm-final-judge.md +331 -0
- package/skills/math-modeling-audit/references/verification-protocol.md +27 -0
- 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.
|