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,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import { cp, mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
4
|
+
import { dirname, join, basename } from 'node:path'
|
|
5
|
+
|
|
6
|
+
function hashText(text) { return createHash('sha256').update(text, 'utf8').digest('hex') }
|
|
7
|
+
function clone(value) { return JSON.parse(JSON.stringify(value)) }
|
|
8
|
+
function list(value) { return Array.isArray(value) ? value.filter(item => typeof item === 'string' && item.trim()) : [] }
|
|
9
|
+
|
|
10
|
+
async function readRun(root) { return JSON.parse(await readFile(join(root, 'run.json'), 'utf8')) }
|
|
11
|
+
async function readLedger(root) { return JSON.parse(await readFile(join(root, 'ledger.json'), 'utf8')) }
|
|
12
|
+
|
|
13
|
+
export async function applyFindingToLineage(lineageRoot, finding) {
|
|
14
|
+
const ledgerPath = join(lineageRoot, 'ledger.json')
|
|
15
|
+
const ledger = await readLedger(lineageRoot)
|
|
16
|
+
const reason = typeof finding.reason === 'string' && finding.reason.trim() ? finding.reason : 'evaluation finding requires correction'
|
|
17
|
+
const sourceIds = [finding.id].filter(Boolean)
|
|
18
|
+
const claimIds = list(finding.affectedClaims)
|
|
19
|
+
const obligationIds = list(finding.affectedObligations)
|
|
20
|
+
for (const claim of ledger.claims ?? []) {
|
|
21
|
+
if (claimIds.includes(claim.id)) {
|
|
22
|
+
claim.stale = true
|
|
23
|
+
claim.verdict = 'STALE'
|
|
24
|
+
claim.staleReason = reason
|
|
25
|
+
claim.staleSourceIds = sourceIds
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
for (const obligation of ledger.obligations ?? []) {
|
|
29
|
+
if (obligationIds.includes(obligation.id) || claimIds.includes(obligation.claimId)) {
|
|
30
|
+
obligation.status = 'STALE'
|
|
31
|
+
obligation.stale = true
|
|
32
|
+
obligation.staleReason = reason
|
|
33
|
+
obligation.staleSourceIds = sourceIds
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
await writeFile(ledgerPath, JSON.stringify(ledger, null, 2) + '\n', 'utf8')
|
|
37
|
+
return ledger
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function createCorrectionLineage(parentRunRoot, finding) {
|
|
41
|
+
if (!finding || typeof finding.id !== 'string' || !finding.id.trim()) throw new TypeError('finding.id is required')
|
|
42
|
+
const parentRun = await readRun(parentRunRoot)
|
|
43
|
+
const parentLedgerText = await readFile(join(parentRunRoot, 'ledger.json'), 'utf8')
|
|
44
|
+
const parentArtifactHash = hashText(JSON.stringify(parentRun) + '\n' + parentLedgerText)
|
|
45
|
+
const revision = (Number.isInteger(parentRun.revision) ? parentRun.revision : 0) + 1
|
|
46
|
+
const childRunRoot = await (async () => {
|
|
47
|
+
const { mkdtemp } = await import('node:fs/promises')
|
|
48
|
+
return mkdtemp(join(dirname(parentRunRoot), basename(parentRunRoot) + '-correction-'))
|
|
49
|
+
})()
|
|
50
|
+
await cp(parentRunRoot, childRunRoot, { recursive: true, filter: source => !source.endsWith('.run-state.lock') && !source.endsWith('.run-state.reclaim') })
|
|
51
|
+
const childRun = clone(parentRun)
|
|
52
|
+
childRun.schemaVersion = 3
|
|
53
|
+
childRun.lineageId = (parentRun.lineageId ?? parentRun.taskId) + '-correction-' + revision
|
|
54
|
+
childRun.parentRunId = parentRun.lineageId ?? parentRun.taskId
|
|
55
|
+
childRun.revision = revision
|
|
56
|
+
childRun.status = 'CORRECTION_REQUIRED'
|
|
57
|
+
childRun.updatedAt = new Date().toISOString()
|
|
58
|
+
await writeFile(join(childRunRoot, 'run.json'), JSON.stringify(childRun, null, 2) + '\n', 'utf8')
|
|
59
|
+
await applyFindingToLineage(childRunRoot, finding)
|
|
60
|
+
const lineage = { schemaVersion: 1, parentRunId: childRun.parentRunId, parentArtifactHash, findingIds: [finding.id], revision, status: 'CORRECTION_REQUIRED', childRunId: childRun.lineageId }
|
|
61
|
+
await writeFile(join(childRunRoot, 'lineage.json'), JSON.stringify(lineage, null, 2) + '\n', 'utf8')
|
|
62
|
+
return { childRunRoot, parentRunId: childRun.parentRunId, parentArtifactHash, revision, status: childRun.status, lineage }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function applyEvaluationFinding(parentRunRoot, finding) {
|
|
66
|
+
return createCorrectionLineage(parentRunRoot, finding)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function markSuperseded(parentRunRoot, childRunRoot) {
|
|
70
|
+
const childRun = await readRun(childRunRoot)
|
|
71
|
+
const lineagePath = join(childRunRoot, 'lineage.json')
|
|
72
|
+
let lineage
|
|
73
|
+
try { lineage = JSON.parse(await readFile(lineagePath, 'utf8')) } catch (error) { if (error.code !== 'ENOENT') throw error; lineage = { schemaVersion: 1, parentRunId: childRun.parentRunId ?? childRun.taskId, revision: childRun.revision ?? 1 } }
|
|
74
|
+
lineage.supersedes = parentRunRoot
|
|
75
|
+
lineage.status = 'SUPERSEDES_PARENT'
|
|
76
|
+
await writeFile(lineagePath, JSON.stringify(lineage, null, 2) + '\n', 'utf8')
|
|
77
|
+
return lineage
|
|
78
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { lstat, readFile, readdir, realpath } from 'node:fs/promises'
|
|
3
|
+
import { isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
|
|
6
|
+
const MAX_FILES = 10_000
|
|
7
|
+
const MAX_FILE_BYTES = 20 * 1024 * 1024
|
|
8
|
+
const MAX_TOTAL_BYTES = 100 * 1024 * 1024
|
|
9
|
+
const MAX_DEPTH = 32
|
|
10
|
+
|
|
11
|
+
function normalizeRelative(path) {
|
|
12
|
+
return path.split(sep).join('/')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function assertInside(root, path) {
|
|
16
|
+
const relativePath = relative(root, path)
|
|
17
|
+
if (isAbsolute(relativePath) || relativePath === '..' || relativePath.startsWith('..' + sep)) {
|
|
18
|
+
throw new Error('distribution entry escapes package root: ' + path)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function collectFiles(root, entries, { allowMissing = false } = {}) {
|
|
23
|
+
const realRoot = await realpath(root)
|
|
24
|
+
const files = new Map()
|
|
25
|
+
let totalBytes = 0
|
|
26
|
+
|
|
27
|
+
const visit = async (path, depth) => {
|
|
28
|
+
if (depth > MAX_DEPTH) throw new Error('distribution tree exceeds maximum depth: ' + path)
|
|
29
|
+
let information
|
|
30
|
+
try {
|
|
31
|
+
information = await lstat(path)
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (allowMissing && error?.code === 'ENOENT') return
|
|
34
|
+
throw error
|
|
35
|
+
}
|
|
36
|
+
if (information.isSymbolicLink()) throw new Error('symbolic link is not allowed in distribution: ' + path)
|
|
37
|
+
const realPath = await realpath(path)
|
|
38
|
+
assertInside(realRoot, realPath)
|
|
39
|
+
if (information.isDirectory()) {
|
|
40
|
+
for (const entry of await readdir(path)) await visit(join(path, entry), depth + 1)
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
if (!information.isFile()) return
|
|
44
|
+
if (information.size > MAX_FILE_BYTES) throw new Error('distribution file exceeds maximum size: ' + path)
|
|
45
|
+
totalBytes += information.size
|
|
46
|
+
if (totalBytes > MAX_TOTAL_BYTES) throw new Error('distribution tree exceeds maximum size')
|
|
47
|
+
if (files.size >= MAX_FILES) throw new Error('distribution tree exceeds maximum file count')
|
|
48
|
+
const relativePath = normalizeRelative(relative(root, path))
|
|
49
|
+
const digest = createHash('sha256').update(await readFile(path)).digest('hex')
|
|
50
|
+
files.set(relativePath, digest)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
if (typeof entry !== 'string' || !entry.trim()) throw new Error('package files entries must be non-empty strings')
|
|
55
|
+
const path = resolve(root, entry)
|
|
56
|
+
assertInside(root, path)
|
|
57
|
+
await visit(path, 0)
|
|
58
|
+
}
|
|
59
|
+
return files
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Compare the allowlisted package files of a source and distribution root. */
|
|
63
|
+
export async function compareDistribution(sourceRoot, candidateRoot) {
|
|
64
|
+
const source = resolve(sourceRoot)
|
|
65
|
+
const candidate = resolve(candidateRoot)
|
|
66
|
+
const manifest = JSON.parse(await readFile(join(source, 'package.json'), 'utf8'))
|
|
67
|
+
if (!Array.isArray(manifest.files) || manifest.files.length === 0) {
|
|
68
|
+
throw new Error('source package.json must declare a non-empty files allowlist')
|
|
69
|
+
}
|
|
70
|
+
const entries = [...new Set(['package.json', ...manifest.files])]
|
|
71
|
+
const expected = await collectFiles(source, entries)
|
|
72
|
+
const actual = await collectFiles(candidate, entries, { allowMissing: true })
|
|
73
|
+
const missing = [...expected.keys()].filter(path => !actual.has(path)).sort()
|
|
74
|
+
const extra = [...actual.keys()].filter(path => !expected.has(path)).sort()
|
|
75
|
+
const mismatched = [...expected.keys()]
|
|
76
|
+
.filter(path => actual.has(path) && actual.get(path) !== expected.get(path))
|
|
77
|
+
.sort()
|
|
78
|
+
const metadataMismatches = []
|
|
79
|
+
try {
|
|
80
|
+
const candidateManifest = JSON.parse(await readFile(join(candidate, 'package.json'), 'utf8'))
|
|
81
|
+
for (const key of ['name', 'version']) {
|
|
82
|
+
if (candidateManifest[key] !== manifest[key]) {
|
|
83
|
+
metadataMismatches.push(`${key}: expected ${manifest[key]}, received ${candidateManifest[key]}`)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} catch (error) {
|
|
87
|
+
metadataMismatches.push('package.json: ' + (error instanceof Error ? error.message : String(error)))
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
ok: missing.length === 0 && extra.length === 0 && mismatched.length === 0 && metadataMismatches.length === 0,
|
|
91
|
+
sourceRoot: source,
|
|
92
|
+
candidateRoot: candidate,
|
|
93
|
+
expectedFileCount: expected.size,
|
|
94
|
+
candidateFileCount: actual.size,
|
|
95
|
+
missing,
|
|
96
|
+
extra,
|
|
97
|
+
mismatched,
|
|
98
|
+
metadataMismatches,
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parseArguments(argv) {
|
|
103
|
+
if (argv.length !== 2 || argv.some(value => !value || value.startsWith('-'))) {
|
|
104
|
+
throw new Error('usage: node distribution-parity.mjs <source-root> <candidate-root>')
|
|
105
|
+
}
|
|
106
|
+
return { sourceRoot: argv[0], candidateRoot: argv[1] }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
110
|
+
try {
|
|
111
|
+
const { sourceRoot, candidateRoot } = parseArguments(argv)
|
|
112
|
+
const result = await compareDistribution(sourceRoot, candidateRoot)
|
|
113
|
+
console.log(JSON.stringify(result, null, 2))
|
|
114
|
+
return result.ok ? 0 : 1
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error(error instanceof Error ? error.message : String(error))
|
|
117
|
+
return 2
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (process.argv[1] !== undefined && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
|
|
122
|
+
process.exitCode = await main()
|
|
123
|
+
}
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
const EVIDENCE_REQUIRED_FIELDS = [
|
|
2
|
+
'id',
|
|
3
|
+
'kind',
|
|
4
|
+
'coveredClaimIds',
|
|
5
|
+
'coveredObligationIds',
|
|
6
|
+
'artifactPath',
|
|
7
|
+
'inputHashes',
|
|
8
|
+
'outputHashes',
|
|
9
|
+
'command',
|
|
10
|
+
'workdir',
|
|
11
|
+
'environment',
|
|
12
|
+
'exitCode',
|
|
13
|
+
'methodFamily',
|
|
14
|
+
'codeHash',
|
|
15
|
+
'level',
|
|
16
|
+
'limitations',
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
const EVIDENCE_STRING_FIELDS = [
|
|
20
|
+
'id',
|
|
21
|
+
'kind',
|
|
22
|
+
'artifactPath',
|
|
23
|
+
'command',
|
|
24
|
+
'workdir',
|
|
25
|
+
'methodFamily',
|
|
26
|
+
'codeHash',
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
const EVIDENCE_ARRAY_FIELDS = [
|
|
30
|
+
'coveredClaimIds',
|
|
31
|
+
'coveredObligationIds',
|
|
32
|
+
'limitations',
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
const EVIDENCE_NON_EMPTY_ARRAY_FIELDS = new Set([
|
|
36
|
+
'coveredClaimIds',
|
|
37
|
+
'coveredObligationIds',
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
const EVIDENCE_OBJECT_FIELDS = [
|
|
41
|
+
'inputHashes',
|
|
42
|
+
'outputHashes',
|
|
43
|
+
'environment',
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
const EVIDENCE_NON_EMPTY_OBJECT_FIELDS = new Set(EVIDENCE_OBJECT_FIELDS)
|
|
47
|
+
|
|
48
|
+
const EVIDENCE_LEVELS = new Set([
|
|
49
|
+
'DERIVED',
|
|
50
|
+
'EXECUTED',
|
|
51
|
+
'VERIFIED',
|
|
52
|
+
'INDEPENDENTLY_VERIFIED',
|
|
53
|
+
'EXTERNALLY_VALIDATED',
|
|
54
|
+
'LEGACY_UNVERIFIED',
|
|
55
|
+
])
|
|
56
|
+
|
|
57
|
+
const VERIFICATION_REQUIRED_FIELDS = [
|
|
58
|
+
'id',
|
|
59
|
+
'recipeId',
|
|
60
|
+
'evidenceIds',
|
|
61
|
+
'verdict',
|
|
62
|
+
'independenceKey',
|
|
63
|
+
]
|
|
64
|
+
const VERIFICATION_ALLOWED_FIELDS = new Set([...VERIFICATION_REQUIRED_FIELDS, 'derivedFrom'])
|
|
65
|
+
const VERIFICATION_VERDICTS = new Set(['PASS', 'FAIL', 'INCONCLUSIVE', 'NOT_RUN'])
|
|
66
|
+
|
|
67
|
+
function isRecord(value) {
|
|
68
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function hasOwn(value, key) {
|
|
72
|
+
return Object.prototype.hasOwnProperty.call(value, key)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function result(errors) {
|
|
76
|
+
return { valid: errors.length === 0, errors }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function addRequiredErrors(value, fields, errors) {
|
|
80
|
+
for (const field of fields) {
|
|
81
|
+
if (!hasOwn(value, field)) {
|
|
82
|
+
errors.push(field + ' is required')
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function addUnknownFieldErrors(value, allowedFields, errors) {
|
|
88
|
+
for (const field of Object.keys(value)) {
|
|
89
|
+
if (!allowedFields.has(field)) {
|
|
90
|
+
errors.push('unknown top-level field: ' + field)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function addNonEmptyStringErrors(value, fields, errors) {
|
|
96
|
+
for (const field of fields) {
|
|
97
|
+
if (hasOwn(value, field) && (typeof value[field] !== 'string' || value[field].trim() === '')) {
|
|
98
|
+
errors.push(field + ' must be a nonempty string')
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function addStringArrayErrors(value, fields, errors) {
|
|
104
|
+
for (const field of fields) {
|
|
105
|
+
if (!hasOwn(value, field)) continue
|
|
106
|
+
const current = value[field]
|
|
107
|
+
if (!Array.isArray(current)) {
|
|
108
|
+
errors.push(field + ' must be an array of strings')
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
if (EVIDENCE_NON_EMPTY_ARRAY_FIELDS.has(field) && current.length === 0) {
|
|
112
|
+
errors.push(field + ' must not be empty')
|
|
113
|
+
}
|
|
114
|
+
if (current.some(item => typeof item !== 'string' || item.trim() === '')) {
|
|
115
|
+
errors.push(field + ' must contain only nonempty strings')
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function addObjectErrors(value, fields, errors) {
|
|
121
|
+
for (const field of fields) {
|
|
122
|
+
if (hasOwn(value, field) && !isRecord(value[field])) {
|
|
123
|
+
errors.push(field + ' must be an object')
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function addHashMapErrors(value, fields, errors) {
|
|
129
|
+
for (const field of fields) {
|
|
130
|
+
if (!hasOwn(value, field) || !isRecord(value[field])) continue
|
|
131
|
+
if (EVIDENCE_NON_EMPTY_OBJECT_FIELDS.has(field) && Object.keys(value[field]).length === 0) {
|
|
132
|
+
errors.push(field + ' must not be empty')
|
|
133
|
+
}
|
|
134
|
+
for (const [key, hash] of Object.entries(value[field])) {
|
|
135
|
+
if (typeof hash !== 'string' || hash.trim() === '') {
|
|
136
|
+
errors.push(field + '.' + key + ' must be a nonempty string')
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function normalizeMarker(value) {
|
|
143
|
+
return value
|
|
144
|
+
.toLowerCase()
|
|
145
|
+
.replace(/[^a-z0-9]+/g, ' ')
|
|
146
|
+
.trim()
|
|
147
|
+
.replace(/\s+/g, ' ')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isNonIndependentMarker(value) {
|
|
151
|
+
if (typeof value !== 'string' || value.trim() === '') return false
|
|
152
|
+
const marker = normalizeMarker(value)
|
|
153
|
+
return (
|
|
154
|
+
marker.includes('solver summary') ||
|
|
155
|
+
marker.includes('same summary') ||
|
|
156
|
+
marker.includes('deterministic result') ||
|
|
157
|
+
marker.includes('same result') ||
|
|
158
|
+
marker.includes('reprint')
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function stringsIn(value) {
|
|
163
|
+
if (typeof value === 'string') return [value]
|
|
164
|
+
if (Array.isArray(value)) return value.flatMap(stringsIn)
|
|
165
|
+
if (isRecord(value)) return Object.values(value).flatMap(stringsIn)
|
|
166
|
+
return []
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function addIndependenceErrors(value, field, errors) {
|
|
170
|
+
if (!hasOwn(value, field)) return
|
|
171
|
+
if (stringsIn(value[field]).some(isNonIndependentMarker)) {
|
|
172
|
+
errors.push(field + ' must identify an independent check, not a solver summary, deterministic result, or reprint-only check')
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function validateEvidence(value) {
|
|
177
|
+
const errors = []
|
|
178
|
+
if (!isRecord(value)) return result(['evidence must be an object'])
|
|
179
|
+
|
|
180
|
+
addRequiredErrors(value, EVIDENCE_REQUIRED_FIELDS, errors)
|
|
181
|
+
addUnknownFieldErrors(value, new Set(EVIDENCE_REQUIRED_FIELDS), errors)
|
|
182
|
+
addNonEmptyStringErrors(value, EVIDENCE_STRING_FIELDS, errors)
|
|
183
|
+
addStringArrayErrors(value, EVIDENCE_ARRAY_FIELDS, errors)
|
|
184
|
+
addObjectErrors(value, EVIDENCE_OBJECT_FIELDS, errors)
|
|
185
|
+
addHashMapErrors(value, ['inputHashes', 'outputHashes'], errors)
|
|
186
|
+
if (hasOwn(value, 'environment') && isRecord(value.environment) && Object.keys(value.environment).length === 0) {
|
|
187
|
+
errors.push('environment must not be empty')
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (hasOwn(value, 'exitCode') && !Number.isInteger(value.exitCode)) {
|
|
191
|
+
errors.push('exitCode must be an integer')
|
|
192
|
+
}
|
|
193
|
+
if (hasOwn(value, 'level') && !EVIDENCE_LEVELS.has(value.level)) {
|
|
194
|
+
errors.push('level must be one of ' + [...EVIDENCE_LEVELS].join(', '))
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return result(errors)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function validateVerification(value) {
|
|
201
|
+
const errors = []
|
|
202
|
+
if (!isRecord(value)) return result(['verification must be an object'])
|
|
203
|
+
|
|
204
|
+
addRequiredErrors(value, VERIFICATION_REQUIRED_FIELDS, errors)
|
|
205
|
+
addUnknownFieldErrors(value, VERIFICATION_ALLOWED_FIELDS, errors)
|
|
206
|
+
addNonEmptyStringErrors(value, ['id', 'recipeId', 'independenceKey'], errors)
|
|
207
|
+
addStringArrayErrors(value, ['evidenceIds'], errors)
|
|
208
|
+
|
|
209
|
+
if (hasOwn(value, 'evidenceIds') && Array.isArray(value.evidenceIds) && value.evidenceIds.length === 0) {
|
|
210
|
+
errors.push('evidenceIds must not be empty')
|
|
211
|
+
}
|
|
212
|
+
if (hasOwn(value, 'verdict') && !VERIFICATION_VERDICTS.has(value.verdict)) {
|
|
213
|
+
errors.push('verdict must be one of ' + [...VERIFICATION_VERDICTS].join(', '))
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (hasOwn(value, 'derivedFrom')) {
|
|
217
|
+
const derivedFrom = value.derivedFrom
|
|
218
|
+
const validDerivedFrom =
|
|
219
|
+
(typeof derivedFrom === 'string' && derivedFrom.trim() !== '') ||
|
|
220
|
+
(Array.isArray(derivedFrom) && derivedFrom.length > 0 && derivedFrom.every(item => typeof item === 'string' && item.trim() !== ''))
|
|
221
|
+
if (!validDerivedFrom) {
|
|
222
|
+
errors.push('derivedFrom must be a nonempty string or string array')
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
addIndependenceErrors(value, 'independenceKey', errors)
|
|
227
|
+
addIndependenceErrors(value, 'derivedFrom', errors)
|
|
228
|
+
return result(errors)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function graphRecord(value, label) {
|
|
232
|
+
if (!isRecord(value)) throw new TypeError(label + ' must be an object')
|
|
233
|
+
if (typeof value.id !== 'string' || value.id.trim() === '') throw new TypeError(label + '.id must be a nonempty string')
|
|
234
|
+
return value
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function graphCollection(graph, name) {
|
|
238
|
+
if (!isRecord(graph) || !Array.isArray(graph[name])) throw new TypeError('graph.' + name + ' must be an array')
|
|
239
|
+
return graph[name]
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function assertUnique(collection, id, label) {
|
|
243
|
+
if (collection.some(item => item?.id === id)) throw new TypeError('duplicate ' + label + ' id: ' + id)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function assertKnown(collection, id, label) {
|
|
247
|
+
if (!collection.some(item => item?.id === id)) throw new TypeError('unknown ' + label + ' id: ' + id)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function assertStringIds(value, field) {
|
|
251
|
+
if (value === undefined) return []
|
|
252
|
+
if (!Array.isArray(value) || value.length === 0 || value.some(item => typeof item !== 'string' || item.trim() === '')) throw new TypeError(field + ' must be a nonempty string array')
|
|
253
|
+
return value
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function createGraph(taskId) {
|
|
257
|
+
if (typeof taskId !== 'string' || taskId.trim() === '') throw new TypeError('taskId must be a nonempty string')
|
|
258
|
+
return {
|
|
259
|
+
schemaVersion: 3,
|
|
260
|
+
taskId,
|
|
261
|
+
scope: { independentAuditPassed: false, interactions: [], decisionStack: [], robustnessExempt: false },
|
|
262
|
+
requirements: [],
|
|
263
|
+
assumptions: [],
|
|
264
|
+
claims: [],
|
|
265
|
+
obligations: [],
|
|
266
|
+
subproblems: [],
|
|
267
|
+
candidates: [],
|
|
268
|
+
issues: [],
|
|
269
|
+
evidence: [],
|
|
270
|
+
verifications: [],
|
|
271
|
+
failures: [],
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function addRequirement(graph, value) {
|
|
276
|
+
const item = graphRecord(value, 'requirement')
|
|
277
|
+
const requirements = graphCollection(graph, 'requirements')
|
|
278
|
+
assertUnique(requirements, item.id, 'requirement')
|
|
279
|
+
requirements.push(item)
|
|
280
|
+
return item.id
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function addClaim(graph, value) {
|
|
284
|
+
const item = graphRecord(value, 'claim')
|
|
285
|
+
const claims = graphCollection(graph, 'claims')
|
|
286
|
+
assertUnique(claims, item.id, 'claim')
|
|
287
|
+
const requirementIds = value.requirementIds ?? []
|
|
288
|
+
if (!Array.isArray(requirementIds) || requirementIds.some(id => typeof id !== 'string' || id.trim() === '')) throw new TypeError('claim.requirementIds must be a string array')
|
|
289
|
+
for (const id of requirementIds) assertKnown(graphCollection(graph, 'requirements'), id, 'requirement')
|
|
290
|
+
item.requirementIds = requirementIds
|
|
291
|
+
item.obligationIds = value.obligationIds ?? []
|
|
292
|
+
item.evidenceIds = value.evidenceIds ?? []
|
|
293
|
+
claims.push(item)
|
|
294
|
+
return item.id
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function addObligation(graph, value) {
|
|
298
|
+
const item = graphRecord(value, 'obligation')
|
|
299
|
+
const obligations = graphCollection(graph, 'obligations')
|
|
300
|
+
assertUnique(obligations, item.id, 'obligation')
|
|
301
|
+
assertKnown(graphCollection(graph, 'claims'), item.claimId, 'claim')
|
|
302
|
+
item.evidenceIds = value.evidenceIds ?? []
|
|
303
|
+
obligations.push(item)
|
|
304
|
+
return item.id
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function addEvidence(graph, value) {
|
|
308
|
+
const item = graphRecord(value, 'evidence')
|
|
309
|
+
const validation = validateEvidence(item)
|
|
310
|
+
if (!validation.valid) throw new TypeError(validation.errors.join('; '))
|
|
311
|
+
const evidence = graphCollection(graph, 'evidence')
|
|
312
|
+
assertUnique(evidence, item.id, 'evidence')
|
|
313
|
+
for (const id of item.coveredClaimIds) assertKnown(graphCollection(graph, 'claims'), id, 'claim')
|
|
314
|
+
for (const id of item.coveredObligationIds) assertKnown(graphCollection(graph, 'obligations'), id, 'obligation')
|
|
315
|
+
evidence.push(item)
|
|
316
|
+
for (const claim of graphCollection(graph, 'claims')) {
|
|
317
|
+
if (item.coveredClaimIds.includes(claim.id) && !claim.evidenceIds.includes(item.id)) claim.evidenceIds.push(item.id)
|
|
318
|
+
}
|
|
319
|
+
for (const obligation of graphCollection(graph, 'obligations')) {
|
|
320
|
+
if (item.coveredObligationIds.includes(obligation.id) && !obligation.evidenceIds.includes(item.id)) obligation.evidenceIds.push(item.id)
|
|
321
|
+
}
|
|
322
|
+
return item.id
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function markStale(graph, id, { staleReason, staleSourceIds } = {}) {
|
|
326
|
+
if (typeof staleReason !== 'string' || staleReason.trim() === '') throw new TypeError('staleReason is required')
|
|
327
|
+
const sourceIds = assertStringIds(staleSourceIds, 'staleSourceIds')
|
|
328
|
+
const claims = graphCollection(graph, 'claims')
|
|
329
|
+
const obligations = graphCollection(graph, 'obligations')
|
|
330
|
+
const claim = claims.find(item => item?.id === id)
|
|
331
|
+
const obligation = obligations.find(item => item?.id === id)
|
|
332
|
+
if (!claim && !obligation) throw new TypeError('unknown claim or obligation id: ' + id)
|
|
333
|
+
const mark = item => {
|
|
334
|
+
item.stale = true
|
|
335
|
+
item.staleReason = staleReason
|
|
336
|
+
item.staleSourceIds = [...sourceIds]
|
|
337
|
+
}
|
|
338
|
+
if (claim) {
|
|
339
|
+
mark(claim)
|
|
340
|
+
claim.verdict = 'STALE'
|
|
341
|
+
for (const item of obligations.filter(entry => entry?.claimId === id)) { mark(item); item.status = 'STALE' }
|
|
342
|
+
} else {
|
|
343
|
+
mark(obligation)
|
|
344
|
+
obligation.status = 'STALE'
|
|
345
|
+
const owner = claims.find(item => item?.id === obligation.claimId)
|
|
346
|
+
if (owner) { mark(owner); owner.verdict = 'STALE' }
|
|
347
|
+
}
|
|
348
|
+
return id
|
|
349
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { appendFile, mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
const REQUIRED_FIELDS = [
|
|
6
|
+
'failureId', 'stage', 'subproblem', 'attempt', 'rootCause', 'candidateId', 'assumptionIds', 'symptom',
|
|
7
|
+
'minimalReproduction', 'command', 'inputHashes', 'codeHash', 'exitCode', 'affectedClaims', 'affectedObligations',
|
|
8
|
+
'whatWasRuledOut', 'scientificInsight', 'recovery', 'status', 'evidenceIds',
|
|
9
|
+
]
|
|
10
|
+
const ROOT_CAUSES = new Set(['DATA', 'TOOL', 'MODEL', 'ASSUMPTION', 'ANALYSIS', 'VALIDATION', 'EVIDENCE', 'RESEARCH_GAP'])
|
|
11
|
+
const SCIENTIFIC_FAILURES = new Set(['MODEL', 'ASSUMPTION', 'ANALYSIS', 'VALIDATION', 'EVIDENCE'])
|
|
12
|
+
|
|
13
|
+
function nonempty(value) { return typeof value === 'string' && value.trim() !== '' }
|
|
14
|
+
function stringArray(value, allowEmpty = true) { return Array.isArray(value) && (allowEmpty || value.length > 0) && value.every(item => nonempty(item)) }
|
|
15
|
+
|
|
16
|
+
export function validateFailure(value) {
|
|
17
|
+
const errors = []
|
|
18
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return { valid: false, errors: ['failure must be an object'] }
|
|
19
|
+
for (const field of REQUIRED_FIELDS) if (!Object.hasOwn(value, field)) errors.push(field + ' is required')
|
|
20
|
+
for (const field of ['failureId', 'stage', 'subproblem', 'candidateId', 'symptom', 'minimalReproduction', 'command', 'codeHash', 'scientificInsight', 'recovery']) if (Object.hasOwn(value, field) && !nonempty(value[field])) errors.push(field + ' must be nonempty')
|
|
21
|
+
if (Object.hasOwn(value, 'attempt') && (!Number.isInteger(value.attempt) || value.attempt < 1)) errors.push('attempt must be a positive integer')
|
|
22
|
+
if (Object.hasOwn(value, 'rootCause') && !ROOT_CAUSES.has(value.rootCause)) errors.push('rootCause is invalid')
|
|
23
|
+
for (const field of ['assumptionIds', 'affectedClaims', 'affectedObligations', 'evidenceIds']) if (Object.hasOwn(value, field) && !stringArray(value[field])) errors.push(field + ' must be an array of strings')
|
|
24
|
+
if (Object.hasOwn(value, 'whatWasRuledOut') && !stringArray(value.whatWasRuledOut, false) && SCIENTIFIC_FAILURES.has(value.rootCause)) errors.push('whatWasRuledOut must be a nonempty string array')
|
|
25
|
+
if (Object.hasOwn(value, 'inputHashes') && (!value.inputHashes || typeof value.inputHashes !== 'object' || Array.isArray(value.inputHashes) || Object.keys(value.inputHashes).length === 0)) errors.push('inputHashes must be a nonempty object')
|
|
26
|
+
if (Object.hasOwn(value, 'exitCode') && !Number.isInteger(value.exitCode)) errors.push('exitCode must be an integer')
|
|
27
|
+
if (Object.hasOwn(value, 'status') && !['OPEN', 'RECOVERED', 'ABANDONED', 'STALE'].includes(value.status)) errors.push('status is invalid')
|
|
28
|
+
if (SCIENTIFIC_FAILURES.has(value.rootCause)) {
|
|
29
|
+
if (!stringArray(value.whatWasRuledOut, false)) errors.push('scientific failure requires whatWasRuledOut')
|
|
30
|
+
if (!nonempty(value.scientificInsight)) errors.push('scientific failure requires scientificInsight')
|
|
31
|
+
}
|
|
32
|
+
return { valid: errors.length === 0, errors: [...new Set(errors)] }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function appendFailure(runRoot, failure) {
|
|
36
|
+
const result = validateFailure(failure)
|
|
37
|
+
if (!result.valid) throw new TypeError(result.errors.join('; '))
|
|
38
|
+
const path = join(runRoot, 'failed', 'failures.jsonl')
|
|
39
|
+
await mkdir(join(runRoot, 'failed'), { recursive: true })
|
|
40
|
+
await appendFile(path, JSON.stringify(failure) + '\n', 'utf8')
|
|
41
|
+
return { path, failureId: failure.failureId }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function renderFailureInsight(failure) {
|
|
45
|
+
const ruledOut = (failure.whatWasRuledOut ?? []).map(value => '- ' + value).join('\n') || '- 未记录'
|
|
46
|
+
return [
|
|
47
|
+
'## ' + failure.failureId,
|
|
48
|
+
'',
|
|
49
|
+
'- 环节:' + failure.stage + ';子问题:' + failure.subproblem + ';attempt:' + failure.attempt,
|
|
50
|
+
'- 根因:' + failure.rootCause,
|
|
51
|
+
'- 症状:' + failure.symptom,
|
|
52
|
+
'',
|
|
53
|
+
'### 最小复现',
|
|
54
|
+
failure.minimalReproduction,
|
|
55
|
+
'',
|
|
56
|
+
'### 已排除',
|
|
57
|
+
ruledOut,
|
|
58
|
+
'',
|
|
59
|
+
'### 科学启发',
|
|
60
|
+
failure.scientificInsight,
|
|
61
|
+
'',
|
|
62
|
+
'### 修正与恢复',
|
|
63
|
+
failure.recovery,
|
|
64
|
+
'',
|
|
65
|
+
'下游影响:claims=' + (failure.affectedClaims ?? []).join(', ') + '; obligations=' + (failure.affectedObligations ?? []).join(', ') + '.',
|
|
66
|
+
'证据:' + (failure.evidenceIds ?? []).join(', ') + '.',
|
|
67
|
+
].join('\n')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function buildFailureInsights(runRoot) {
|
|
71
|
+
const path = join(runRoot, 'failed', 'failures.jsonl')
|
|
72
|
+
let text = ''
|
|
73
|
+
try { text = await readFile(path, 'utf8') } catch (error) { if (error.code !== 'ENOENT') throw error }
|
|
74
|
+
const failures = text.trim() ? text.trim().split('\n').map(JSON.parse) : []
|
|
75
|
+
const output = failures.length ? '# Failure Insights\n\n' + failures.map(renderFailureInsight).join('\n\n') + '\n' : '# Failure Insights\n\nNo structured failures recorded.\n'
|
|
76
|
+
const outputPath = join(runRoot, 'failure-insights.md')
|
|
77
|
+
await writeFile(outputPath, output, 'utf8')
|
|
78
|
+
return { count: failures.length, path: outputPath }
|
|
79
|
+
}
|