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
|
@@ -1,218 +1,293 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises'
|
|
2
|
-
import { pathToFileURL } from 'node:url'
|
|
3
|
-
|
|
4
|
-
export const RUBRIC = {
|
|
5
|
-
'1.summary': 4,
|
|
6
|
-
'1.understanding': 3,
|
|
7
|
-
'1.assumptions': 3,
|
|
8
|
-
'2.data-source': 3,
|
|
9
|
-
'2.preprocessing': 2,
|
|
10
|
-
'2.bias-leakage': 2,
|
|
11
|
-
'2.parameter-calibration': 3,
|
|
12
|
-
'2.consistency': 2,
|
|
13
|
-
'3.mechanism': 5,
|
|
14
|
-
'3.innovation': 4,
|
|
15
|
-
'3.formulation': 4,
|
|
16
|
-
'3.internal-consistency': 3,
|
|
17
|
-
'3.complexity-explainability': 3,
|
|
18
|
-
'3.multi-problem-unity': 3,
|
|
19
|
-
'4.derivation': 4,
|
|
20
|
-
'4.algorithm-numerics': 4,
|
|
21
|
-
'4.reproducibility': 3,
|
|
22
|
-
'4.complexity-convergence-optimality': 3,
|
|
23
|
-
'4.sanity': 2,
|
|
24
|
-
'5.task-completion': 4,
|
|
25
|
-
'5.numerical-consistency': 4,
|
|
26
|
-
'5.baselines': 3,
|
|
27
|
-
'5.sensitivity': 4,
|
|
28
|
-
'5.robustness-uncertainty': 4,
|
|
29
|
-
'5.independent-validation': 3,
|
|
30
|
-
'5.failure-cases': 2,
|
|
31
|
-
'6.conclusions': 3,
|
|
32
|
-
'6.actionability': 2,
|
|
33
|
-
'6.limitations': 2,
|
|
34
|
-
'6.transferability': 1,
|
|
35
|
-
'7.structure': 2,
|
|
36
|
-
'7.figures': 2,
|
|
37
|
-
'7.language': 1,
|
|
38
|
-
'7.notation': 1,
|
|
39
|
-
'7.citations': 1,
|
|
40
|
-
'7.page-efficiency': 1,
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const CAP_VALUES = {
|
|
44
|
-
MATH_ERROR: 69,
|
|
45
|
-
CORE_UNVALIDATED: 84,
|
|
46
|
-
UNSUPPORTED_SENSITIVE_PARAMETERS: 79,
|
|
47
|
-
NOT_REPRODUCIBLE: 79,
|
|
48
|
-
DATA_LEAKAGE: 69,
|
|
49
|
-
CONCLUSION_CONFLICT: 74,
|
|
50
|
-
NO_QUANTITATIVE_RESULTS: 59,
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const CEILING_VALUES = {
|
|
54
|
-
MISSING_CORE_TASK: 'Meritorious',
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const OUTSTANDING_GATES = [
|
|
58
|
-
'taskComplete',
|
|
59
|
-
'noMajorError',
|
|
60
|
-
'coreValidated',
|
|
61
|
-
'meaningfulSensitivity',
|
|
62
|
-
'parametersSupported',
|
|
63
|
-
'summaryStrong',
|
|
64
|
-
'nonTemplateModeling',
|
|
65
|
-
'closedLoop',
|
|
66
|
-
'reproducible',
|
|
67
|
-
'memorableContribution',
|
|
68
|
-
]
|
|
69
|
-
|
|
70
|
-
const BAND_ORDER = [
|
|
71
|
-
'Unsuccessful Risk',
|
|
72
|
-
'Successful Participant',
|
|
73
|
-
'Honorable Mention',
|
|
74
|
-
'Meritorious',
|
|
75
|
-
'Finalist',
|
|
76
|
-
'Outstanding Candidate',
|
|
77
|
-
]
|
|
78
|
-
|
|
79
|
-
function requireEvidence(value, label) {
|
|
80
|
-
const items = Array.isArray(value) ? value : [value]
|
|
81
|
-
if (!items.length || items.some((item) => typeof item !== 'string' || !item.trim())) {
|
|
82
|
-
throw new TypeError(label + ' requires evidence')
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function scoreBand(score) {
|
|
87
|
-
if (score >= 93) return 'Outstanding Candidate'
|
|
88
|
-
if (score >= 87) return 'Finalist'
|
|
89
|
-
if (score >= 78) return 'Meritorious'
|
|
90
|
-
if (score >= 68) return 'Honorable Mention'
|
|
91
|
-
if (score >= 55) return 'Successful Participant'
|
|
92
|
-
return 'Unsuccessful Risk'
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function lowerBand(left, right) {
|
|
96
|
-
return BAND_ORDER[Math.min(BAND_ORDER.indexOf(left), BAND_ORDER.indexOf(right))]
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function validateScores(scores) {
|
|
100
|
-
const expected = Object.keys(RUBRIC).sort()
|
|
101
|
-
const actual = Object.keys(scores ?? {}).sort()
|
|
102
|
-
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
|
103
|
-
throw new TypeError('scores must contain every rubric subcriterion exactly once')
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
let total = 0
|
|
107
|
-
const categories = {}
|
|
108
|
-
for (const [id, maximum] of Object.entries(RUBRIC)) {
|
|
109
|
-
const entry = scores[id]
|
|
110
|
-
if (!Number.isFinite(entry.score) || entry.score < 0 || entry.score > maximum) {
|
|
111
|
-
throw new TypeError(id + ' score must be between 0 and ' + maximum)
|
|
112
|
-
}
|
|
113
|
-
requireEvidence(entry.evidence, id)
|
|
114
|
-
total += entry.score
|
|
115
|
-
const category = id.split('.')[0]
|
|
116
|
-
categories[category] = (categories[category] ?? 0) + entry.score
|
|
117
|
-
}
|
|
118
|
-
return { total, categories }
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function validateRubricTotal() {
|
|
122
|
-
const maximum = Object.values(RUBRIC).reduce((sum, value) => sum + value, 0)
|
|
123
|
-
if (maximum !== 100) throw new TypeError('rubric maxima must total 100')
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function validateOutstandingGates(gates) {
|
|
127
|
-
for (const gate of OUTSTANDING_GATES) {
|
|
128
|
-
if (typeof gates?.[gate] !== 'boolean') {
|
|
129
|
-
throw new TypeError('missing Outstanding gate: ' + gate)
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
return OUTSTANDING_GATES.every((gate) => gates[gate])
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { pathToFileURL } from 'node:url'
|
|
3
|
+
|
|
4
|
+
export const RUBRIC = {
|
|
5
|
+
'1.summary': 4,
|
|
6
|
+
'1.understanding': 3,
|
|
7
|
+
'1.assumptions': 3,
|
|
8
|
+
'2.data-source': 3,
|
|
9
|
+
'2.preprocessing': 2,
|
|
10
|
+
'2.bias-leakage': 2,
|
|
11
|
+
'2.parameter-calibration': 3,
|
|
12
|
+
'2.consistency': 2,
|
|
13
|
+
'3.mechanism': 5,
|
|
14
|
+
'3.innovation': 4,
|
|
15
|
+
'3.formulation': 4,
|
|
16
|
+
'3.internal-consistency': 3,
|
|
17
|
+
'3.complexity-explainability': 3,
|
|
18
|
+
'3.multi-problem-unity': 3,
|
|
19
|
+
'4.derivation': 4,
|
|
20
|
+
'4.algorithm-numerics': 4,
|
|
21
|
+
'4.reproducibility': 3,
|
|
22
|
+
'4.complexity-convergence-optimality': 3,
|
|
23
|
+
'4.sanity': 2,
|
|
24
|
+
'5.task-completion': 4,
|
|
25
|
+
'5.numerical-consistency': 4,
|
|
26
|
+
'5.baselines': 3,
|
|
27
|
+
'5.sensitivity': 4,
|
|
28
|
+
'5.robustness-uncertainty': 4,
|
|
29
|
+
'5.independent-validation': 3,
|
|
30
|
+
'5.failure-cases': 2,
|
|
31
|
+
'6.conclusions': 3,
|
|
32
|
+
'6.actionability': 2,
|
|
33
|
+
'6.limitations': 2,
|
|
34
|
+
'6.transferability': 1,
|
|
35
|
+
'7.structure': 2,
|
|
36
|
+
'7.figures': 2,
|
|
37
|
+
'7.language': 1,
|
|
38
|
+
'7.notation': 1,
|
|
39
|
+
'7.citations': 1,
|
|
40
|
+
'7.page-efficiency': 1,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const CAP_VALUES = {
|
|
44
|
+
MATH_ERROR: 69,
|
|
45
|
+
CORE_UNVALIDATED: 84,
|
|
46
|
+
UNSUPPORTED_SENSITIVE_PARAMETERS: 79,
|
|
47
|
+
NOT_REPRODUCIBLE: 79,
|
|
48
|
+
DATA_LEAKAGE: 69,
|
|
49
|
+
CONCLUSION_CONFLICT: 74,
|
|
50
|
+
NO_QUANTITATIVE_RESULTS: 59,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const CEILING_VALUES = {
|
|
54
|
+
MISSING_CORE_TASK: 'Meritorious',
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const OUTSTANDING_GATES = [
|
|
58
|
+
'taskComplete',
|
|
59
|
+
'noMajorError',
|
|
60
|
+
'coreValidated',
|
|
61
|
+
'meaningfulSensitivity',
|
|
62
|
+
'parametersSupported',
|
|
63
|
+
'summaryStrong',
|
|
64
|
+
'nonTemplateModeling',
|
|
65
|
+
'closedLoop',
|
|
66
|
+
'reproducible',
|
|
67
|
+
'memorableContribution',
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
const BAND_ORDER = [
|
|
71
|
+
'Unsuccessful Risk',
|
|
72
|
+
'Successful Participant',
|
|
73
|
+
'Honorable Mention',
|
|
74
|
+
'Meritorious',
|
|
75
|
+
'Finalist',
|
|
76
|
+
'Outstanding Candidate',
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
function requireEvidence(value, label) {
|
|
80
|
+
const items = Array.isArray(value) ? value : [value]
|
|
81
|
+
if (!items.length || items.some((item) => typeof item !== 'string' || !item.trim())) {
|
|
82
|
+
throw new TypeError(label + ' requires evidence')
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function scoreBand(score) {
|
|
87
|
+
if (score >= 93) return 'Outstanding Candidate'
|
|
88
|
+
if (score >= 87) return 'Finalist'
|
|
89
|
+
if (score >= 78) return 'Meritorious'
|
|
90
|
+
if (score >= 68) return 'Honorable Mention'
|
|
91
|
+
if (score >= 55) return 'Successful Participant'
|
|
92
|
+
return 'Unsuccessful Risk'
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function lowerBand(left, right) {
|
|
96
|
+
return BAND_ORDER[Math.min(BAND_ORDER.indexOf(left), BAND_ORDER.indexOf(right))]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function validateScores(scores) {
|
|
100
|
+
const expected = Object.keys(RUBRIC).sort()
|
|
101
|
+
const actual = Object.keys(scores ?? {}).sort()
|
|
102
|
+
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
|
103
|
+
throw new TypeError('scores must contain every rubric subcriterion exactly once')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let total = 0
|
|
107
|
+
const categories = {}
|
|
108
|
+
for (const [id, maximum] of Object.entries(RUBRIC)) {
|
|
109
|
+
const entry = scores[id]
|
|
110
|
+
if (!Number.isFinite(entry.score) || entry.score < 0 || entry.score > maximum) {
|
|
111
|
+
throw new TypeError(id + ' score must be between 0 and ' + maximum)
|
|
112
|
+
}
|
|
113
|
+
requireEvidence(entry.evidence, id)
|
|
114
|
+
total += entry.score
|
|
115
|
+
const category = id.split('.')[0]
|
|
116
|
+
categories[category] = (categories[category] ?? 0) + entry.score
|
|
117
|
+
}
|
|
118
|
+
return { total, categories }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function validateRubricTotal() {
|
|
122
|
+
const maximum = Object.values(RUBRIC).reduce((sum, value) => sum + value, 0)
|
|
123
|
+
if (maximum !== 100) throw new TypeError('rubric maxima must total 100')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function validateOutstandingGates(gates) {
|
|
127
|
+
for (const gate of OUTSTANDING_GATES) {
|
|
128
|
+
if (typeof gates?.[gate] !== 'boolean') {
|
|
129
|
+
throw new TypeError('missing Outstanding gate: ' + gate)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return OUTSTANDING_GATES.every((gate) => gates[gate])
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function validateV2EvidenceRecords(records) {
|
|
136
|
+
if (!Array.isArray(records)) throw new TypeError('evidence records are required')
|
|
137
|
+
const ids = new Set()
|
|
138
|
+
for (const record of records) {
|
|
139
|
+
if (!record || typeof record !== 'object' || Array.isArray(record)) throw new TypeError('evidence record must be an object')
|
|
140
|
+
if (typeof record.id !== 'string' || !record.id.trim() || ids.has(record.id)) throw new TypeError('evidence record ids must be unique nonempty strings')
|
|
141
|
+
if (typeof record.artifactPath !== 'string' || !record.artifactPath.trim()) throw new TypeError('evidence record artifactPath is required')
|
|
142
|
+
if (typeof record.artifactHash !== 'string' || !/^[a-f0-9]{64}$/i.test(record.artifactHash)) throw new TypeError('evidence record artifactHash must be a SHA-256 hex string')
|
|
143
|
+
if (record.stale === true || ['STALE', 'WAIVED', 'REFUTED'].includes(record.claimVerdict)) throw new TypeError('stale or unsupported evidence cannot support a score')
|
|
144
|
+
ids.add(record.id)
|
|
145
|
+
}
|
|
146
|
+
return new Map(records.map(record => [record.id, record]))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function requireEvidenceRefs(refs, label, records) {
|
|
150
|
+
if (!Array.isArray(refs) || refs.length === 0 || refs.some(ref => typeof ref !== 'string' || !ref.trim())) throw new TypeError(label + ' requires evidenceRefs')
|
|
151
|
+
for (const ref of refs) {
|
|
152
|
+
const record = records.get(ref)
|
|
153
|
+
if (!record) throw new TypeError(label + ' references missing evidence artifact: ' + ref)
|
|
154
|
+
if (record.claimVerdict === 'INCONCLUSIVE' || record.claimVerdict === 'CONDITIONAL') throw new TypeError(label + ' cannot use conditional or inconclusive evidence as PASS')
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function validateV2Scores(scores, records) {
|
|
159
|
+
const expected = Object.keys(RUBRIC).sort()
|
|
160
|
+
const actual = Object.keys(scores ?? {}).sort()
|
|
161
|
+
if (JSON.stringify(actual) !== JSON.stringify(expected)) throw new TypeError('scores must contain every rubric subcriterion exactly once')
|
|
162
|
+
let total = 0
|
|
163
|
+
const categories = {}
|
|
164
|
+
for (const [id, maximum] of Object.entries(RUBRIC)) {
|
|
165
|
+
const entry = scores[id]
|
|
166
|
+
if (!Number.isFinite(entry?.score) || entry.score < 0 || entry.score > maximum) throw new TypeError(id + ' score must be between 0 and ' + maximum)
|
|
167
|
+
requireEvidenceRefs(entry.evidenceRefs, id, records)
|
|
168
|
+
if (!['SUPPORTED', 'CONDITIONAL', 'INCONCLUSIVE', 'REFUTED'].includes(entry.verdict)) throw new TypeError(id + ' verdict is invalid')
|
|
169
|
+
if (typeof entry.comment !== 'string' || !entry.comment.trim()) throw new TypeError(id + ' comment is required')
|
|
170
|
+
total += entry.score
|
|
171
|
+
const category = id.split('.')[0]
|
|
172
|
+
categories[category] = (categories[category] ?? 0) + entry.score
|
|
173
|
+
}
|
|
174
|
+
return { total, categories }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function scoreReviewV2(input) {
|
|
178
|
+
if (input.rubricId !== 'mcm-icm-final-judge') throw new TypeError('unsupported rubricId')
|
|
179
|
+
if (typeof input.rubricVersion !== 'string' || !input.rubricVersion.trim()) throw new TypeError('rubricVersion is required')
|
|
180
|
+
if (typeof input.rubricHash !== 'string' || !/^[a-f0-9]{64}$/i.test(input.rubricHash)) throw new TypeError('rubricHash must be a SHA-256 hex string')
|
|
181
|
+
validateRubricTotal()
|
|
182
|
+
const records = validateV2EvidenceRecords(input.evidence)
|
|
183
|
+
if (input.disqualificationRisk) {
|
|
184
|
+
requireEvidenceRefs(input.disqualificationRisk.evidenceRefs, 'disqualification risk', records)
|
|
185
|
+
return { schemaVersion: 2, rubricId: input.rubricId, rubricVersion: input.rubricVersion, rubricHash: input.rubricHash, disqualification: true, disqualificationRisk: input.disqualificationRisk, categoryScores: null, rawScore: null, caps: [], strictestCap: null, cappedScore: null, awardCeiling: null, outstandingGatePassed: null, finalBand: 'Disqualification Risk' }
|
|
186
|
+
}
|
|
187
|
+
const { total: rawScore, categories } = validateV2Scores(input.scores, records)
|
|
188
|
+
const outstandingGatePassed = validateOutstandingGates(input.outstandingGates)
|
|
189
|
+
const caps = (input.caps ?? []).map(entry => {
|
|
190
|
+
if (!(entry.code in CAP_VALUES)) throw new TypeError('unknown cap code: ' + entry.code)
|
|
191
|
+
requireEvidenceRefs(entry.evidenceRefs, 'cap ' + entry.code, records)
|
|
192
|
+
return { ...entry, maximum: CAP_VALUES[entry.code] }
|
|
193
|
+
})
|
|
194
|
+
const strictestCap = caps.length ? Math.min(...caps.map(entry => entry.maximum)) : 100
|
|
195
|
+
const cappedScore = Math.min(rawScore, strictestCap)
|
|
196
|
+
const ceilings = (input.awardCeilings ?? []).map(entry => {
|
|
197
|
+
if (!(entry.code in CEILING_VALUES)) throw new TypeError('unknown award ceiling code: ' + entry.code)
|
|
198
|
+
requireEvidenceRefs(entry.evidenceRefs, 'award ceiling ' + entry.code, records)
|
|
199
|
+
return { ...entry, maximumBand: CEILING_VALUES[entry.code] }
|
|
200
|
+
})
|
|
201
|
+
let awardCeiling = ceilings.length ? ceilings.map(entry => entry.maximumBand).reduce((current, value) => lowerBand(current, value), 'Outstanding Candidate') : 'Outstanding Candidate'
|
|
202
|
+
if (!outstandingGatePassed) awardCeiling = lowerBand(awardCeiling, 'Finalist')
|
|
203
|
+
const rawBand = scoreBand(rawScore)
|
|
204
|
+
const cappedBand = scoreBand(cappedScore)
|
|
205
|
+
return { schemaVersion: 2, rubricId: input.rubricId, rubricVersion: input.rubricVersion, rubricHash: input.rubricHash, disqualification: false, disqualificationRisk: null, categoryScores: categories, rawScore, caps, strictestCap, cappedScore, rawBand, cappedBand, awardCeiling, outstandingGatePassed, finalBand: lowerBand(cappedBand, awardCeiling) }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function scoreReview(input) {
|
|
209
|
+
if (input?.schemaVersion === 2 && input.rubricId !== undefined) return scoreReviewV2(input)
|
|
210
|
+
|
|
211
|
+
if (input?.schemaVersion !== 1) throw new TypeError('unsupported schemaVersion')
|
|
212
|
+
validateRubricTotal()
|
|
213
|
+
|
|
214
|
+
if (input.disqualificationRisk) {
|
|
215
|
+
requireEvidence(input.disqualificationRisk.evidence, 'disqualification risk')
|
|
216
|
+
return {
|
|
217
|
+
schemaVersion: 1,
|
|
218
|
+
disqualification: true,
|
|
219
|
+
disqualificationRisk: input.disqualificationRisk,
|
|
220
|
+
categoryScores: null,
|
|
221
|
+
rawScore: null,
|
|
222
|
+
caps: [],
|
|
223
|
+
strictestCap: null,
|
|
224
|
+
cappedScore: null,
|
|
225
|
+
awardCeiling: null,
|
|
226
|
+
outstandingGatePassed: null,
|
|
227
|
+
finalBand: 'Disqualification Risk',
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const { total: rawScore, categories } = validateScores(input.scores)
|
|
232
|
+
const outstandingGatePassed = validateOutstandingGates(input.outstandingGates)
|
|
233
|
+
|
|
234
|
+
const caps = (input.caps ?? []).map((entry) => {
|
|
235
|
+
if (!(entry.code in CAP_VALUES)) throw new TypeError('unknown cap code: ' + entry.code)
|
|
236
|
+
requireEvidence(entry.evidence, 'cap ' + entry.code)
|
|
237
|
+
return { ...entry, maximum: CAP_VALUES[entry.code] }
|
|
238
|
+
})
|
|
239
|
+
const strictestCap = caps.length
|
|
240
|
+
? Math.min(...caps.map((entry) => entry.maximum))
|
|
241
|
+
: 100
|
|
242
|
+
const cappedScore = Math.min(rawScore, strictestCap)
|
|
243
|
+
|
|
244
|
+
const ceilings = (input.awardCeilings ?? []).map((entry) => {
|
|
245
|
+
if (!(entry.code in CEILING_VALUES)) {
|
|
246
|
+
throw new TypeError('unknown award ceiling code: ' + entry.code)
|
|
247
|
+
}
|
|
248
|
+
requireEvidence(entry.evidence, 'award ceiling ' + entry.code)
|
|
249
|
+
return { ...entry, maximumBand: CEILING_VALUES[entry.code] }
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
let awardCeiling = ceilings.length
|
|
253
|
+
? ceilings
|
|
254
|
+
.map((entry) => entry.maximumBand)
|
|
255
|
+
.reduce((current, value) => lowerBand(current, value), 'Outstanding Candidate')
|
|
256
|
+
: 'Outstanding Candidate'
|
|
257
|
+
if (!outstandingGatePassed) awardCeiling = lowerBand(awardCeiling, 'Finalist')
|
|
258
|
+
|
|
259
|
+
const rawBand = scoreBand(rawScore)
|
|
260
|
+
const cappedBand = scoreBand(cappedScore)
|
|
261
|
+
const finalBand = lowerBand(cappedBand, awardCeiling)
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
schemaVersion: 1,
|
|
265
|
+
disqualification: false,
|
|
266
|
+
disqualificationRisk: null,
|
|
267
|
+
categoryScores: categories,
|
|
268
|
+
rawScore,
|
|
269
|
+
caps,
|
|
270
|
+
strictestCap,
|
|
271
|
+
cappedScore,
|
|
272
|
+
rawBand,
|
|
273
|
+
cappedBand,
|
|
274
|
+
awardCeiling,
|
|
275
|
+
outstandingGatePassed,
|
|
276
|
+
finalBand,
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function main() {
|
|
281
|
+
const path = process.argv[2]
|
|
282
|
+
if (!path) throw new Error('usage: mcm-score.mjs <review-score.json>')
|
|
283
|
+
const input = JSON.parse(await readFile(path, 'utf8'))
|
|
284
|
+
process.stdout.write(JSON.stringify(scoreReview(input), null, 2) + '\n')
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const invokedPath = process.argv[1]
|
|
288
|
+
if (invokedPath && import.meta.url === pathToFileURL(invokedPath).href) {
|
|
289
|
+
main().catch((error) => {
|
|
290
|
+
process.stderr.write(error.message + '\n')
|
|
291
|
+
process.exitCode = 1
|
|
292
|
+
})
|
|
293
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
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 { evidenceRecords, rubricHash, scoreInput } from './project-initial-review.mjs'
|
|
7
|
+
import { scoreReview } from './mcm-score.mjs'
|
|
8
|
+
|
|
9
|
+
function hashText(text) { return createHash('sha256').update(text, 'utf8').digest('hex') }
|
|
10
|
+
|
|
11
|
+
function finalSections(text) {
|
|
12
|
+
return [...text.matchAll(/^###\s+([一二三四五六七八九十百]+、[^\n]+)$/gm)].map(match => match[1])
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function reviewPaper(paperPath, runRoot) {
|
|
16
|
+
const paperText = await readFile(paperPath, 'utf8')
|
|
17
|
+
const ledgerText = await readFile(join(runRoot, 'ledger.json'), 'utf8')
|
|
18
|
+
const ledger = JSON.parse(ledgerText)
|
|
19
|
+
const manuscriptSections = finalSections(paperText)
|
|
20
|
+
const hash = await rubricHash()
|
|
21
|
+
const score = scoreReview(scoreInput(ledger, hash))
|
|
22
|
+
const findings = manuscriptSections.length === 14 ? [] : [{ code: 'PAPER_SECTION_MISSING', severity: 'P1', message: 'the final manuscript must contain all fourteen panel sections' }]
|
|
23
|
+
const result = {
|
|
24
|
+
mode: 'PAPER_FINAL_REVIEW',
|
|
25
|
+
paperPath,
|
|
26
|
+
paperHash: hashText(paperText),
|
|
27
|
+
evidenceGraphHash: hashText(ledgerText),
|
|
28
|
+
rubricHash: hash,
|
|
29
|
+
sectionCount: manuscriptSections.length,
|
|
30
|
+
score,
|
|
31
|
+
categoryScores: score.categoryScores,
|
|
32
|
+
findings,
|
|
33
|
+
evidenceCount: evidenceRecords(ledger).length,
|
|
34
|
+
}
|
|
35
|
+
const outputRoot = join(runRoot, 'review')
|
|
36
|
+
await mkdir(outputRoot, { recursive: true })
|
|
37
|
+
await writeFile(join(outputRoot, 'final-panel.md'), '# Paper Final Review\n\n' + JSON.stringify(result, null, 2) + '\n', 'utf8')
|
|
38
|
+
await writeFile(join(outputRoot, 'final-score.json'), JSON.stringify(score, null, 2) + '\n', 'utf8')
|
|
39
|
+
return result
|
|
40
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
import { RUBRIC, scoreReview } from './mcm-score.mjs'
|
|
7
|
+
|
|
8
|
+
const OUTSTANDING_KEYS = ['taskComplete', 'noMajorError', 'coreValidated', 'meaningfulSensitivity', 'parametersSupported', 'summaryStrong', 'nonTemplateModeling', 'closedLoop', 'reproducible', 'memorableContribution']
|
|
9
|
+
|
|
10
|
+
function hashText(text) { return createHash('sha256').update(text, 'utf8').digest('hex') }
|
|
11
|
+
export async function rubricHash() { return hashText(await readFile(new URL('../references/mcm-icm-final-judge.md', import.meta.url), 'utf8')) }
|
|
12
|
+
async function hasDirectory(path) { try { return (await stat(path)).isDirectory() } catch (error) { if (error.code === 'ENOENT') return false; throw error } }
|
|
13
|
+
export function evidenceRecords(graph) {
|
|
14
|
+
return (graph.evidence ?? []).map(record => {
|
|
15
|
+
const rawHash = Object.values(record.outputHashes ?? {})[0] ?? ''
|
|
16
|
+
const artifactHash = rawHash.replace(/^sha256:/i, '')
|
|
17
|
+
return { id: record.id, artifactPath: record.artifactPath, artifactHash, claimVerdict: record.level === 'LEGACY_UNVERIFIED' ? 'INCONCLUSIVE' : 'SUPPORTED', stale: false }
|
|
18
|
+
}).filter(record => /^[a-f0-9]{64}$/i.test(record.artifactHash))
|
|
19
|
+
}
|
|
20
|
+
export function scoreInput(graph, hash) {
|
|
21
|
+
const evidence = evidenceRecords(graph)
|
|
22
|
+
const evidenceId = evidence[0]?.id
|
|
23
|
+
const scores = Object.fromEntries(Object.entries(RUBRIC).map(([id]) => [id, { score: 0, evidenceRefs: evidenceId ? [evidenceId] : [], verdict: evidenceId ? 'SUPPORTED' : 'INCONCLUSIVE', comment: evidenceId ? 'Project initial review baseline.' : 'No evidence artifact recorded.' }]))
|
|
24
|
+
return { schemaVersion: 2, rubricId: 'mcm-icm-final-judge', rubricVersion: '2026-09-02', rubricHash: hash, evidence, scores, caps: [], awardCeilings: [], outstandingGates: Object.fromEntries(OUTSTANDING_KEYS.map(key => [key, evidence.length > 0])) }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function reviewProject(runRoot) {
|
|
28
|
+
const ledger = JSON.parse(await readFile(join(runRoot, 'ledger.json'), 'utf8'))
|
|
29
|
+
const hash = await rubricHash()
|
|
30
|
+
const score = evidenceRecords(ledger).length ? scoreReview(scoreInput(ledger, hash)) : null
|
|
31
|
+
const paperComplete = await hasDirectory(join(runRoot, 'paper-evidence'))
|
|
32
|
+
const findings = []
|
|
33
|
+
if (!paperComplete) findings.push({ code: 'PAPER_EVIDENCE_INCOMPLETE', severity: 'P1', message: 'paper-evidence package is missing; model claims are not rewritten' })
|
|
34
|
+
if (!ledger.evidence?.length) findings.push({ code: 'EVIDENCE_MISSING', severity: 'P0', message: 'no evidence artifacts are linked in the graph' })
|
|
35
|
+
const outputRoot = join(runRoot, 'review')
|
|
36
|
+
await mkdir(outputRoot, { recursive: true })
|
|
37
|
+
const result = { mode: 'PROJECT_INITIAL_REVIEW', paperStatus: paperComplete ? 'READY' : 'INCOMPLETE', score, findings, evidenceGraphHash: hashText(JSON.stringify(ledger)), claimCount: (ledger.claims ?? []).length }
|
|
38
|
+
await writeFile(join(outputRoot, 'project-initial.md'), '# Project Initial Review\n\n' + JSON.stringify(result, null, 2) + '\n', 'utf8')
|
|
39
|
+
await writeFile(join(outputRoot, 'score.json'), JSON.stringify(score, null, 2) + '\n', 'utf8')
|
|
40
|
+
return result
|
|
41
|
+
}
|