dsh-math-modeling-agent 0.3.1 → 0.5.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/README.md +247 -172
- package/package.json +34 -34
- package/skills/math-modeling-agent/SKILL.md +70 -56
- package/skills/math-modeling-agent/references/claims-evidence.md +53 -41
- package/skills/math-modeling-agent/references/interaction-protocol.md +165 -163
- package/skills/math-modeling-agent/references/original-project-parity.md +19 -0
- package/skills/math-modeling-agent/references/report-contract.md +130 -122
- package/skills/math-modeling-agent/references/run-directory.md +65 -62
- package/skills/math-modeling-agent/references/subagent-dispatch.md +3 -2
- package/skills/math-modeling-agent/references/tool-policy.md +11 -0
- package/skills/math-modeling-agent/references/verification-recipes.md +43 -0
- package/skills/math-modeling-agent/references/workflow.md +10 -9
- package/skills/math-modeling-agent/schemas/attempt.schema.json +89 -70
- package/skills/math-modeling-agent/schemas/evidence.schema.json +110 -0
- package/skills/math-modeling-agent/schemas/failure.schema.json +30 -0
- package/skills/math-modeling-agent/schemas/ledger.schema.json +88 -68
- package/skills/math-modeling-agent/schemas/run.schema.json +126 -102
- package/skills/math-modeling-agent/schemas/verification.schema.json +60 -0
- package/skills/math-modeling-agent/scripts/computation/README.md +20 -0
- package/skills/math-modeling-agent/scripts/computation/backend-inventory.schema.json +78 -0
- package/skills/math-modeling-agent/scripts/computation/backend_inventory.ps1 +351 -0
- package/skills/math-modeling-agent/scripts/computation/backend_inventory.py +322 -0
- package/skills/math-modeling-agent/scripts/computation/computation_record.py +361 -0
- package/skills/math-modeling-agent/scripts/computation/probe_backends.ps1 +396 -0
- package/skills/math-modeling-agent/scripts/computation/probe_backends.py +230 -0
- package/skills/math-modeling-agent/scripts/correction-lineage.mjs +78 -0
- package/skills/math-modeling-agent/scripts/distribution-parity.mjs +123 -0
- package/skills/math-modeling-agent/scripts/evidence-store.mjs +349 -0
- package/skills/math-modeling-agent/scripts/failure-insights.mjs +79 -0
- package/skills/math-modeling-agent/scripts/input-snapshot.mjs +98 -0
- package/skills/math-modeling-agent/scripts/ledger-mutation.mjs +82 -0
- package/skills/math-modeling-agent/scripts/migration-v3.mjs +31 -0
- package/skills/math-modeling-agent/scripts/output-integrity.mjs +82 -0
- package/skills/math-modeling-agent/scripts/paper-evidence.mjs +45 -0
- package/skills/math-modeling-agent/scripts/report-contract.mjs +112 -0
- package/skills/math-modeling-agent/scripts/run-state.mjs +829 -707
- package/skills/math-modeling-agent/scripts/verification-recipes.mjs +52 -0
- package/skills/math-modeling-agent/scripts/verification-runner.mjs +8 -0
- package/skills/math-modeling-audit/SKILL.md +41 -41
- package/skills/math-modeling-audit/references/mcm-icm-final-judge.md +335 -331
- package/skills/math-modeling-audit/scripts/mcm-score.mjs +293 -218
- package/skills/math-modeling-audit/scripts/paper-final-review.mjs +40 -0
- package/skills/math-modeling-audit/scripts/project-initial-review.mjs +41 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import { chmod, copyFile, lstat, mkdir, readFile, stat, writeFile } from 'node:fs/promises'
|
|
4
|
+
import { dirname, isAbsolute, relative, resolve, sep } from 'node:path'
|
|
5
|
+
|
|
6
|
+
const MANIFEST_PATH = ['inputs', 'manifest.json']
|
|
7
|
+
|
|
8
|
+
function hashBytes(bytes) {
|
|
9
|
+
return createHash('sha256').update(bytes).digest('hex')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isContained(root, target) {
|
|
13
|
+
const value = relative(root, target)
|
|
14
|
+
return value === '' || (!value.startsWith('..' + sep) && value !== '..' && !isAbsolute(value))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function assertRelativePath(value) {
|
|
18
|
+
if (typeof value !== 'string' || value.trim() === '' || isAbsolute(value)) throw new TypeError('input path must be a nonempty relative path')
|
|
19
|
+
const normalized = value.replaceAll('\\', '/')
|
|
20
|
+
if (normalized.split('/').some(part => part === '..')) throw new TypeError('input path must remain contained')
|
|
21
|
+
return normalized
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function aggregateHash(files) {
|
|
25
|
+
const material = files
|
|
26
|
+
.slice()
|
|
27
|
+
.sort((left, right) => left.relativePath.localeCompare(right.relativePath))
|
|
28
|
+
.map(file => file.relativePath + '\0' + file.snapshotHash)
|
|
29
|
+
.join('\n')
|
|
30
|
+
return hashBytes(Buffer.from(material, 'utf8'))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function readManifest(runRoot) {
|
|
34
|
+
const path = resolve(runRoot, ...MANIFEST_PATH)
|
|
35
|
+
return { path, manifest: JSON.parse(await readFile(path, 'utf8')) }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function snapshotInputs(sourceRoot, runRoot, relativeFiles) {
|
|
39
|
+
if (!Array.isArray(relativeFiles) || relativeFiles.length === 0) throw new TypeError('relativeFiles must be a nonempty array')
|
|
40
|
+
const sourceBase = resolve(sourceRoot)
|
|
41
|
+
const runBase = resolve(runRoot)
|
|
42
|
+
const files = []
|
|
43
|
+
for (const input of relativeFiles) {
|
|
44
|
+
const relativePath = assertRelativePath(input)
|
|
45
|
+
const sourcePath = resolve(sourceBase, relativePath)
|
|
46
|
+
const snapshotPath = resolve(runBase, 'inputs', 'raw', relativePath)
|
|
47
|
+
if (!isContained(sourceBase, sourcePath) || !isContained(resolve(runBase, 'inputs', 'raw'), snapshotPath)) throw new TypeError('input path must remain contained')
|
|
48
|
+
const info = await lstat(sourcePath)
|
|
49
|
+
if (!info.isFile() || info.isSymbolicLink()) throw new TypeError('input must be a regular file: ' + relativePath)
|
|
50
|
+
const bytes = await readFile(sourcePath)
|
|
51
|
+
await mkdir(dirname(snapshotPath), { recursive: true })
|
|
52
|
+
await copyFile(sourcePath, snapshotPath)
|
|
53
|
+
await chmod(snapshotPath, 0o444)
|
|
54
|
+
const sourceHash = hashBytes(bytes)
|
|
55
|
+
const snapshotHash = hashBytes(await readFile(snapshotPath))
|
|
56
|
+
files.push({ relativePath, sourcePath, snapshotPath: relative(runBase, snapshotPath).replaceAll('\\', '/'), sourceHash, snapshotHash, bytes: bytes.length, readOnly: true })
|
|
57
|
+
}
|
|
58
|
+
const manifest = { schemaVersion: 1, sourceRoot: sourceBase, createdAt: new Date().toISOString(), files, inputSnapshotHash: aggregateHash(files) }
|
|
59
|
+
const manifestPath = resolve(runBase, ...MANIFEST_PATH)
|
|
60
|
+
await mkdir(dirname(manifestPath), { recursive: true })
|
|
61
|
+
await writeFile(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf8')
|
|
62
|
+
return manifest
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function verifySnapshot(runRoot) {
|
|
66
|
+
const { manifest } = await readManifest(runRoot)
|
|
67
|
+
const runBase = resolve(runRoot)
|
|
68
|
+
let snapshotChanged = false
|
|
69
|
+
let sourceChanged = false
|
|
70
|
+
let sourceUnknown = false
|
|
71
|
+
const files = []
|
|
72
|
+
for (const file of manifest.files) {
|
|
73
|
+
const snapshotPath = resolve(runBase, file.snapshotPath)
|
|
74
|
+
let snapshotHash = null
|
|
75
|
+
try { snapshotHash = hashBytes(await readFile(snapshotPath)) } catch (error) { if (error.code !== 'ENOENT') throw error }
|
|
76
|
+
const snapshotMismatch = snapshotHash !== file.snapshotHash
|
|
77
|
+
snapshotChanged ||= snapshotMismatch
|
|
78
|
+
let currentSourceHash = null
|
|
79
|
+
try { currentSourceHash = hashBytes(await readFile(file.sourcePath)) } catch (error) { if (error.code === 'ENOENT') sourceUnknown = true; else throw error }
|
|
80
|
+
const sourceMismatch = currentSourceHash !== null && currentSourceHash !== file.sourceHash
|
|
81
|
+
sourceChanged ||= sourceMismatch
|
|
82
|
+
files.push({ ...file, currentSnapshotHash: snapshotHash, currentSourceHash, snapshotMismatch, sourceMismatch })
|
|
83
|
+
}
|
|
84
|
+
const aggregate = aggregateHash(files.map(file => ({ relativePath: file.relativePath, snapshotHash: file.currentSnapshotHash ?? '' })))
|
|
85
|
+
const aggregateMismatch = aggregate !== manifest.inputSnapshotHash
|
|
86
|
+
return { valid: !snapshotChanged && !aggregateMismatch, snapshotChanged, sourceChanged, sourceUnknown, aggregateMismatch, files }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function assertSnapshotReadOnly(runRoot) {
|
|
90
|
+
const result = await verifySnapshot(runRoot)
|
|
91
|
+
const writable = []
|
|
92
|
+
for (const file of result.files) {
|
|
93
|
+
const path = resolve(runRoot, file.snapshotPath)
|
|
94
|
+
const info = await stat(path)
|
|
95
|
+
if (!file.readOnly || (info.mode & 0o200) !== 0) writable.push(file.relativePath)
|
|
96
|
+
}
|
|
97
|
+
return { valid: result.valid && writable.length === 0, writable, snapshot: result }
|
|
98
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto'
|
|
3
|
+
import { appendFile, mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises'
|
|
4
|
+
import { dirname, join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
addClaim,
|
|
8
|
+
addEvidence,
|
|
9
|
+
addObligation,
|
|
10
|
+
addRequirement,
|
|
11
|
+
markStale,
|
|
12
|
+
} from './evidence-store.mjs'
|
|
13
|
+
|
|
14
|
+
function hashText(text) {
|
|
15
|
+
return createHash('sha256').update(text, 'utf8').digest('hex')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function clone(value) {
|
|
19
|
+
return JSON.parse(JSON.stringify(value))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function atomicWrite(path, text) {
|
|
23
|
+
await mkdir(dirname(path), { recursive: true })
|
|
24
|
+
const temporary = path + '.tmp-' + process.pid + '-' + randomUUID()
|
|
25
|
+
await writeFile(temporary, text, 'utf8')
|
|
26
|
+
try {
|
|
27
|
+
await rename(temporary, path)
|
|
28
|
+
} finally {
|
|
29
|
+
await unlink(temporary).catch(error => {
|
|
30
|
+
if (error.code !== 'ENOENT') throw error
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function mutationTarget(mutation) {
|
|
36
|
+
return typeof mutation.id === 'string' && mutation.id.trim() !== '' ? mutation.id : null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function applyMutation(ledger, mutation) {
|
|
40
|
+
if (!mutation || typeof mutation !== 'object' || Array.isArray(mutation)) throw new TypeError('mutation must be an object')
|
|
41
|
+
if (mutation.type === 'RUN_STATUS' || mutation.path === 'run.json') throw new TypeError('ledger mutation cannot write run.json')
|
|
42
|
+
switch (mutation.type) {
|
|
43
|
+
case 'ADD_REQUIREMENT': addRequirement(ledger, mutation.value); return mutation.value.id
|
|
44
|
+
case 'ADD_CLAIM': addClaim(ledger, mutation.value); return mutation.value.id
|
|
45
|
+
case 'ADD_OBLIGATION': addObligation(ledger, mutation.value); return mutation.value.id
|
|
46
|
+
case 'ADD_EVIDENCE': addEvidence(ledger, mutation.value); return mutation.value.id
|
|
47
|
+
case 'CLAIM_STALE':
|
|
48
|
+
markStale(ledger, mutation.id, { staleReason: mutation.staleReason, staleSourceIds: mutation.staleSourceIds })
|
|
49
|
+
return mutation.id
|
|
50
|
+
case 'OBLIGATION_STALE':
|
|
51
|
+
markStale(ledger, mutation.id, { staleReason: mutation.staleReason, staleSourceIds: mutation.staleSourceIds })
|
|
52
|
+
return mutation.id
|
|
53
|
+
default: throw new TypeError('unsupported ledger mutation type: ' + String(mutation.type))
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function mutateLedger(root, mutation) {
|
|
58
|
+
if (typeof root !== 'string' || root.trim() === '') throw new TypeError('root must be a nonempty string')
|
|
59
|
+
const ledgerPath = join(root, 'ledger.json')
|
|
60
|
+
const journalPath = join(root, 'ledger-mutations.jsonl')
|
|
61
|
+
const beforeText = await readFile(ledgerPath, 'utf8')
|
|
62
|
+
const ledger = JSON.parse(beforeText)
|
|
63
|
+
const next = clone(ledger)
|
|
64
|
+
const targetId = applyMutation(next, mutation)
|
|
65
|
+
const afterText = JSON.stringify(next, null, 2) + '\n'
|
|
66
|
+
const journal = {
|
|
67
|
+
mutationId: randomUUID(),
|
|
68
|
+
type: mutation.type,
|
|
69
|
+
targetId: targetId ?? mutationTarget(mutation),
|
|
70
|
+
beforeHash: hashText(beforeText),
|
|
71
|
+
afterHash: hashText(afterText),
|
|
72
|
+
timestamp: new Date().toISOString(),
|
|
73
|
+
}
|
|
74
|
+
await atomicWrite(ledgerPath, afterText)
|
|
75
|
+
try {
|
|
76
|
+
await appendFile(journalPath, JSON.stringify(journal) + '\n', 'utf8')
|
|
77
|
+
} catch (error) {
|
|
78
|
+
await atomicWrite(ledgerPath, beforeText)
|
|
79
|
+
throw error
|
|
80
|
+
}
|
|
81
|
+
return { ledger: next, journal }
|
|
82
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { cp, mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
function clone(value) { return JSON.parse(JSON.stringify(value)) }
|
|
6
|
+
function legacyEvidence(value) {
|
|
7
|
+
if (typeof value === 'string') return { id: value, level: 'LEGACY_UNVERIFIED', source: value, limitations: ['Migrated from legacy string evidence; artifact not independently verified.'] }
|
|
8
|
+
return { ...clone(value), level: 'LEGACY_UNVERIFIED', limitations: [...(value.limitations ?? []), 'Migrated from a v2 run; verification must be rerun.'] }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function migrateV2(sourceRoot, destinationRoot) {
|
|
12
|
+
await mkdir(destinationRoot, { recursive: true })
|
|
13
|
+
await cp(sourceRoot, destinationRoot, { recursive: true })
|
|
14
|
+
const run = JSON.parse(await readFile(join(sourceRoot, 'run.json'), 'utf8'))
|
|
15
|
+
const oldLedger = JSON.parse(await readFile(join(sourceRoot, 'ledger.json'), 'utf8'))
|
|
16
|
+
const ledger = {
|
|
17
|
+
...oldLedger,
|
|
18
|
+
schemaVersion: 3,
|
|
19
|
+
requirements: oldLedger.requirements ?? [],
|
|
20
|
+
evidence: (oldLedger.evidence ?? []).map(legacyEvidence),
|
|
21
|
+
verifications: oldLedger.verifications ?? [],
|
|
22
|
+
failures: oldLedger.failures ?? [],
|
|
23
|
+
claims: (oldLedger.claims ?? []).map(claim => ({ ...claim, verdict: 'INCONCLUSIVE', stale: claim.stale ?? false, staleReason: claim.staleReason ?? null, staleSourceIds: claim.staleSourceIds ?? [] })),
|
|
24
|
+
}
|
|
25
|
+
const migratedRun = { ...run, schemaVersion: 3, lineageId: run.lineageId ?? run.taskId, parentRunId: run.parentRunId ?? null, revision: run.revision ?? 0, inputSnapshotHash: run.inputSnapshotHash ?? null, evidenceGraphHash: run.evidenceGraphHash ?? null }
|
|
26
|
+
await writeFile(join(destinationRoot, 'run.json'), JSON.stringify(migratedRun, null, 2) + '\n', 'utf8')
|
|
27
|
+
await writeFile(join(destinationRoot, 'ledger.json'), JSON.stringify(ledger, null, 2) + '\n', 'utf8')
|
|
28
|
+
const migration = { schemaVersion: 1, sourceSchemaVersion: run.schemaVersion, destinationSchemaVersion: 3, mintedEvidence: 0, downgradedEvidence: ledger.evidence.length, claimsDowngraded: ledger.claims.length, sourceRoot, destinationRoot }
|
|
29
|
+
await writeFile(join(destinationRoot, 'migration-v3.json'), JSON.stringify(migration, null, 2) + '\n', 'utf8')
|
|
30
|
+
return { run: migratedRun, ledger, migration }
|
|
31
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
function decimalPlaces(numberFormat) {
|
|
4
|
+
if (typeof numberFormat !== 'string') return null
|
|
5
|
+
const match = numberFormat.match(/\.([0#]+)/)
|
|
6
|
+
return match ? match[1].length : 0
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function quantize(value, decimals, roundingMode = 'decimal-half-up') {
|
|
10
|
+
if (!Number.isFinite(value) || !Number.isInteger(decimals) || decimals < 0 || roundingMode !== 'decimal-half-up') throw new TypeError('quantize requires a finite value, nonnegative integer decimals, and decimal-half-up')
|
|
11
|
+
const scale = 10 ** decimals
|
|
12
|
+
const scaled = value * scale
|
|
13
|
+
const adjustment = Number.EPSILON * Math.max(1, Math.abs(scaled))
|
|
14
|
+
return (scaled < 0 ? -Math.round(-scaled + adjustment) : Math.round(scaled + adjustment)) / scale
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function inspectPrecision({ value, numberFormat, decimals = decimalPlaces(numberFormat) }) {
|
|
18
|
+
if (!Number.isFinite(value) || !Number.isInteger(decimals) || decimals < 0) throw new TypeError('inspectPrecision requires a finite value and nonnegative integer decimals')
|
|
19
|
+
const quantizedValue = quantize(value, decimals)
|
|
20
|
+
return {
|
|
21
|
+
displayPrecision: decimalPlaces(numberFormat),
|
|
22
|
+
storedValue: value,
|
|
23
|
+
quantizedValue,
|
|
24
|
+
storedPrecision: Object.is(value, quantizedValue) ? 'at-most-requested' : 'more-than-requested',
|
|
25
|
+
quantized: Object.is(value, quantizedValue),
|
|
26
|
+
decimals,
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function pointMap(coordinates) {
|
|
31
|
+
if (!Array.isArray(coordinates)) throw new TypeError('coordinates must be an array')
|
|
32
|
+
const map = new Map()
|
|
33
|
+
for (const point of coordinates) {
|
|
34
|
+
if (!point || typeof point.id !== 'string' || map.has(point.id)) throw new TypeError('coordinates require unique string ids')
|
|
35
|
+
if (![point.x, point.y, point.z].every(Number.isFinite)) throw new TypeError('coordinates require finite x/y/z')
|
|
36
|
+
map.set(point.id, point)
|
|
37
|
+
}
|
|
38
|
+
return map
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function distance(left, right) {
|
|
42
|
+
return Math.hypot(left.x - right.x, left.y - right.y, left.z - right.z)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function checkQuantizedGeometry({ coordinates, deltas, decimals, constraints }) {
|
|
46
|
+
const original = pointMap(coordinates)
|
|
47
|
+
if (!Array.isArray(constraints)) throw new TypeError('constraints must be an array')
|
|
48
|
+
const quantized = [...original.values()].map(point => ({ ...point, x: quantize(point.x, decimals), y: quantize(point.y, decimals), z: quantize(point.z, decimals) }))
|
|
49
|
+
const quantizedDeltas = Array.isArray(deltas) ? deltas.map(value => quantize(value, decimals)) : null
|
|
50
|
+
const actual = pointMap(quantized)
|
|
51
|
+
const results = constraints.map(constraint => {
|
|
52
|
+
const left = actual.get(constraint.source)
|
|
53
|
+
const right = actual.get(constraint.target)
|
|
54
|
+
if (!left || !right || !Number.isFinite(constraint.originalLength) || constraint.originalLength <= 0 || !Number.isFinite(constraint.maxRelativeChange) || constraint.maxRelativeChange < 0) throw new TypeError('constraint references valid points and positive numeric limits')
|
|
55
|
+
const relativeChange = Math.abs(distance(left, right) - constraint.originalLength) / constraint.originalLength
|
|
56
|
+
return { ...constraint, relativeChange, strictPass: relativeChange <= constraint.maxRelativeChange, tolerancePass: relativeChange <= constraint.maxRelativeChange + 1e-12 }
|
|
57
|
+
})
|
|
58
|
+
const strictViolations = results.filter(result => !result.strictPass).length
|
|
59
|
+
const toleranceViolations = results.filter(result => !result.tolerancePass).length
|
|
60
|
+
return {
|
|
61
|
+
decimals,
|
|
62
|
+
quantizedCoordinates: quantized,
|
|
63
|
+
quantizedDeltas,
|
|
64
|
+
constraints: results,
|
|
65
|
+
strictViolations,
|
|
66
|
+
toleranceViolations,
|
|
67
|
+
worstRelativeChange: results.length ? Math.max(...results.map(result => result.relativeChange)) : 0,
|
|
68
|
+
verdict: strictViolations > 0 ? 'FAIL' : 'PASS',
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function equalWithin(left, right, tolerance) {
|
|
73
|
+
if (typeof left === 'number' && typeof right === 'number') return Number.isFinite(left) && Number.isFinite(right) && Math.abs(left - right) <= tolerance
|
|
74
|
+
return JSON.stringify(left) === JSON.stringify(right)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function compareArtifacts(left, right, { fields, tolerance = 0 } = {}) {
|
|
78
|
+
if (!left || typeof left !== 'object' || !right || typeof right !== 'object' || !Number.isFinite(tolerance) || tolerance < 0) throw new TypeError('compareArtifacts requires objects and nonnegative tolerance')
|
|
79
|
+
const selected = Array.isArray(fields) ? fields : [...new Set([...Object.keys(left), ...Object.keys(right)])]
|
|
80
|
+
const differences = selected.filter(field => !equalWithin(left[field], right[field], tolerance)).map(field => ({ field, left: left[field], right: right[field] }))
|
|
81
|
+
return { valid: differences.length === 0, differences }
|
|
82
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
import { claimEvidenceMatrix } from './report-contract.mjs'
|
|
7
|
+
|
|
8
|
+
const PAPER_FILES = [
|
|
9
|
+
'problem-understanding.md', 'problem-coverage.md', 'data-and-dataset.md', 'assumptions-and-scope.md',
|
|
10
|
+
'symbols.jsonl', 'method-derivation.md', 'parameter-provenance.md', 'validation-summary.md',
|
|
11
|
+
'robustness-and-uncertainty.md', 'failure-insights.md', 'model-evaluation-and-improvement.md',
|
|
12
|
+
'limitations-and-threats.md', 'references.md', 'reproducibility.md', 'claim-evidence-matrix.json',
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
function hashText(text) { return createHash('sha256').update(text, 'utf8').digest('hex') }
|
|
16
|
+
function evidenceRefs(graph) { return (graph?.evidence ?? []).map(item => item.id).filter(Boolean).join(', ') || 'none recorded' }
|
|
17
|
+
function claims(graph) { return (graph?.claims ?? []).map(item => item.statement + ' [evidence: ' + (item.evidenceIds ?? []).join(', ') + ']').join('\n') || 'No claims recorded.' }
|
|
18
|
+
function markdown(title, graph, body) { return '# ' + title + '\n\n' + body + '\n\nEvidence references: ' + evidenceRefs(graph) + '.\n' }
|
|
19
|
+
|
|
20
|
+
export async function buildPaperEvidence(runRoot) {
|
|
21
|
+
const ledgerPath = join(runRoot, 'ledger.json')
|
|
22
|
+
const ledgerText = await readFile(ledgerPath, 'utf8')
|
|
23
|
+
const graph = JSON.parse(ledgerText)
|
|
24
|
+
const outputRoot = join(runRoot, 'paper-evidence')
|
|
25
|
+
await mkdir(outputRoot, { recursive: true })
|
|
26
|
+
const content = {
|
|
27
|
+
'problem-understanding.md': markdown('Problem understanding', graph, 'This package records the frozen problem background, dataset scope, requested outputs and question dependency chain. Each claim below remains tied to its evidence IDs.\n\n' + claims(graph)),
|
|
28
|
+
'problem-coverage.md': markdown('Problem coverage', graph, 'Each registered requirement is mapped to a claim, obligation, evidence record and report location. Missing or stale mappings remain visible rather than being silently filled.'),
|
|
29
|
+
'data-and-dataset.md': markdown('Data and dataset', graph, 'The data section records source files, units, identifiers, coordinate conventions, observed ranges and the input snapshot hash.'),
|
|
30
|
+
'assumptions-and-scope.md': markdown('Assumptions and scope', graph, 'Global and subproblem assumptions are listed with their basis, risk, sensitivity plan and affected claims.'),
|
|
31
|
+
'symbols.jsonl': (graph.symbols ?? []).map(symbol => JSON.stringify(symbol)).join('\n') + ((graph.symbols ?? []).length ? '\n' : ''),
|
|
32
|
+
'method-derivation.md': markdown('Method and derivation', graph, 'The derivation section records equations, variable meanings, units, numerical method choices and the exact artifact that implements each step.'),
|
|
33
|
+
'parameter-provenance.md': markdown('Parameter provenance', graph, 'Every parameter is linked to problem text, data, theory, literature or an explicitly challengeable modeling simplification.'),
|
|
34
|
+
'validation-summary.md': markdown('Validation summary', graph, 'Verification verdicts are shown separately from obligation and claim status. A PASS verdict is not silently promoted to a supported claim.'),
|
|
35
|
+
'robustness-and-uncertainty.md': markdown('Robustness and uncertainty', graph, 'Perturbation targets, magnitudes, sampling rules, conclusion-flip criteria and untested scope are recorded here.'),
|
|
36
|
+
'failure-insights.md': markdown('Failure insights', graph, 'Failed directions preserve symptoms, root causes, exclusions, scientific insight, downstream impact and recovery conditions.'),
|
|
37
|
+
'model-evaluation-and-improvement.md': markdown('Model evaluation and improvement', graph, 'Candidate comparisons state the criterion, evidence scope, limitations and the next controlled improvement.'),
|
|
38
|
+
'limitations-and-threats.md': markdown('Limitations and threats', graph, 'Unproven global optimality, missing physical parameters, sampling restrictions and external-validity threats remain explicit.'),
|
|
39
|
+
'references.md': markdown('References', graph, 'References are mapped to the exact method, assumption or interpretation they support. Unknown metadata is not invented.'),
|
|
40
|
+
'reproducibility.md': markdown('Reproducibility', graph, 'The run records frozen inputs, commands, environment, code hashes, artifact hashes and clean-rerun conditions.'),
|
|
41
|
+
'claim-evidence-matrix.json': JSON.stringify(claimEvidenceMatrix(graph), null, 2) + '\n',
|
|
42
|
+
}
|
|
43
|
+
for (const file of PAPER_FILES) await writeFile(join(outputRoot, file), content[file], 'utf8')
|
|
44
|
+
return { files: [...PAPER_FILES], evidenceGraphHash: hashText(ledgerText), outputRoot }
|
|
45
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
export const SCIENTIFIC_SECTIONS = Object.freeze([
|
|
4
|
+
'问题重述',
|
|
5
|
+
'问题分析',
|
|
6
|
+
'模型假设',
|
|
7
|
+
'符号说明',
|
|
8
|
+
'模型建立与求解',
|
|
9
|
+
'验证',
|
|
10
|
+
'鲁棒性分析',
|
|
11
|
+
'模型的评价与改进',
|
|
12
|
+
'参考文献',
|
|
13
|
+
])
|
|
14
|
+
|
|
15
|
+
export const ADMINISTRATIVE_FIELDS = Object.freeze([
|
|
16
|
+
'objective', 'candidate', 'assumptionDelta', 'claimsChanged', 'artifacts', 'evidenceIds',
|
|
17
|
+
'obligations', 'issuesOpened', 'issuesClosed', 'attemptDelta', 'progress', 'budget', 'currentStatus', 'nextAction',
|
|
18
|
+
])
|
|
19
|
+
|
|
20
|
+
const SECTION_ALIASES = new Map([['模型评价与改进', '模型的评价与改进']])
|
|
21
|
+
|
|
22
|
+
function normalizeHeading(value) {
|
|
23
|
+
return SECTION_ALIASES.get(String(value).trim()) ?? String(value).trim()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function textOf(input) {
|
|
27
|
+
if (typeof input === 'string') return input
|
|
28
|
+
if (input && typeof input.text === 'string') return input.text
|
|
29
|
+
return ''
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function sectionBlocks(text) {
|
|
33
|
+
const matches = [...text.matchAll(/^##\s+(.+)$/gm)]
|
|
34
|
+
return matches.map((match, index) => ({ name: normalizeHeading(match[1]), body: text.slice(match.index + match[0].length, matches[index + 1]?.index ?? text.length).trim() }))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function quality(body) {
|
|
38
|
+
const value = body.toLowerCase()
|
|
39
|
+
return {
|
|
40
|
+
why: /why|为什么|原因|用于|问题是/.test(value),
|
|
41
|
+
how: /how|如何|按|使用|计算|重建|推导|方法/.test(value),
|
|
42
|
+
what: /what|结果|输出|得到|数值|结论/.test(value),
|
|
43
|
+
check: /check|检查|验证|比较|证据|审计|不一致/.test(value),
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function validateSymbols(symbols) {
|
|
48
|
+
const errors = []
|
|
49
|
+
if (!Array.isArray(symbols)) return { valid: false, errors: ['symbols must be an array'] }
|
|
50
|
+
const ids = new Set()
|
|
51
|
+
for (const [index, symbol] of symbols.entries()) {
|
|
52
|
+
if (!symbol || typeof symbol !== 'object' || Array.isArray(symbol)) { errors.push('symbols[' + index + '] must be an object'); continue }
|
|
53
|
+
const id = symbol.id ?? symbol.name
|
|
54
|
+
if (typeof id !== 'string' || id.trim() === '') errors.push('symbols[' + index + '] requires id or name')
|
|
55
|
+
else if (ids.has(id)) errors.push('duplicate symbol: ' + id)
|
|
56
|
+
else ids.add(id)
|
|
57
|
+
for (const field of ['meaning', 'unit']) if (typeof symbol[field] !== 'string' || symbol[field].trim() === '') errors.push('symbols[' + index + '].' + field + ' is required')
|
|
58
|
+
}
|
|
59
|
+
return { valid: errors.length === 0, errors }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function validateReportText({ text, symbols = [], evidence = [] } = {}) {
|
|
63
|
+
const errors = []
|
|
64
|
+
if (typeof text !== 'string' || text.trim() === '') errors.push('report text is required')
|
|
65
|
+
const symbolResult = validateSymbols(symbols)
|
|
66
|
+
errors.push(...symbolResult.errors)
|
|
67
|
+
const evidenceCount = Array.isArray(evidence) ? evidence.length : 0
|
|
68
|
+
if (/\bglobal\s+optimum\b|\bglobally\s+optimal\b/i.test(text ?? '') && evidenceCount === 0) errors.push('global optimum claim requires evidence')
|
|
69
|
+
if (/\bverified\b/i.test(text ?? '') && evidenceCount === 0) errors.push('verified claim requires evidence')
|
|
70
|
+
return { valid: errors.length === 0, errors }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function validateCoverage(entries) {
|
|
74
|
+
const blockers = []
|
|
75
|
+
const conditionals = []
|
|
76
|
+
if (!Array.isArray(entries)) return { canLeavePhase: false, blockers: ['coverage entries are missing'], conditionals: [] }
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
if (!entry || typeof entry.requirementId !== 'string' || entry.requirementId.trim() === '') { blockers.push('unknown requirement'); continue }
|
|
79
|
+
if (entry.status === 'missing' || entry.status === 'stale') blockers.push(entry.requirementId)
|
|
80
|
+
else if (entry.status === 'partial') conditionals.push(entry.requirementId)
|
|
81
|
+
}
|
|
82
|
+
return { canLeavePhase: blockers.length === 0, blockers, conditionals }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function validateAttemptReport(input) {
|
|
86
|
+
const text = textOf(input)
|
|
87
|
+
const blocks = sectionBlocks(text)
|
|
88
|
+
const names = new Set(blocks.map(block => block.name))
|
|
89
|
+
const errors = []
|
|
90
|
+
for (const section of SCIENTIFIC_SECTIONS) if (!names.has(section)) errors.push('missing scientific section: ' + section)
|
|
91
|
+
const administrative = input && typeof input === 'object' && !Array.isArray(input) ? input.administrative : null
|
|
92
|
+
for (const field of ADMINISTRATIVE_FIELDS) if (!administrative || !Object.hasOwn(administrative, field)) errors.push('missing administrative field: ' + field)
|
|
93
|
+
const qualityBySection = Object.fromEntries(blocks.map(block => [block.name, quality(block.body)]))
|
|
94
|
+
const missingQuality = Object.entries(qualityBySection).flatMap(([section, checks]) => Object.entries(checks).filter(([, present]) => !present).map(([kind]) => ({ section, kind })))
|
|
95
|
+
const textLint = validateReportText({ text, symbols: input?.symbols ?? [], evidence: input?.evidence ?? [] })
|
|
96
|
+
errors.push(...textLint.errors)
|
|
97
|
+
return { valid: errors.length === 0, errors, sections: blocks.map(block => block.name), quality: qualityBySection, missingQuality, canLeavePhase: errors.length === 0 }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function claimEvidenceMatrix(graph) {
|
|
101
|
+
return {
|
|
102
|
+
schemaVersion: 1,
|
|
103
|
+
claims: (graph?.claims ?? []).map(claim => ({
|
|
104
|
+
claimId: claim.id,
|
|
105
|
+
statement: claim.statement,
|
|
106
|
+
verdict: claim.verdict ?? 'INCONCLUSIVE',
|
|
107
|
+
evidenceIds: [...(claim.evidenceIds ?? [])],
|
|
108
|
+
obligationIds: [...(claim.obligationIds ?? [])],
|
|
109
|
+
limitations: [...(claim.limitations ?? [])],
|
|
110
|
+
})),
|
|
111
|
+
}
|
|
112
|
+
}
|