acmer-coach 2.5.0 → 2.6.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 +8 -0
- package/assets/knowledge/CONTEST_PROFILES.yaml +62 -5
- package/assets/knowledge/skilltree.html +3 -3
- package/assets/rules/coach-rules.md +8 -0
- package/coach/skilltree_template.html +3 -3
- package/dev/catalog-check.mjs +36 -0
- package/dev/curriculum-evidence-check.mjs +18 -0
- package/dev/grading-check.mjs +161 -0
- package/dev/parity-check.mjs +5 -0
- package/dev/run-all.sh +2 -0
- package/dev/training-check.mjs +183 -0
- package/dev.sh +2 -2
- package/docs//346/257/224/350/265/233/344/270/216/346/225/231/345/255/246/344/276/235/346/215/256.md +6 -0
- package/docs//350/256/276/350/256/241/346/226/271/351/222/210.md +1 -1
- package/index.js +214 -134
- package/lib/curriculum-evidence.js +7 -3
- package/lib/curriculum.js +10 -7
- package/lib/strategy.js +10 -3
- package/package.json +2 -2
- package/verify.mjs +47 -47
|
@@ -85,6 +85,7 @@ export function collectEvidence(progress, schedule = { days: {} }) {
|
|
|
85
85
|
if (!seenIds.has(id)) {
|
|
86
86
|
seenIds.add(id)
|
|
87
87
|
const facts = {}
|
|
88
|
+
if (text(record.competency)) facts.competency = text(record.competency)
|
|
88
89
|
if (typeof record.independent === 'boolean') facts.independent = record.independent
|
|
89
90
|
if (record.occurredAt !== undefined) facts.occurredAt = record.occurredAt
|
|
90
91
|
const minutes = source === 'schedule' ? record.actualMin : record.minutes
|
|
@@ -141,13 +142,16 @@ export function collectEvidence(progress, schedule = { days: {} }) {
|
|
|
141
142
|
if (!day || !object(entry) || !Array.isArray(entry.actual)) continue
|
|
142
143
|
for (const record of entry.actual) {
|
|
143
144
|
const node = text(record?.node)
|
|
144
|
-
|
|
145
|
+
const competency = text(record?.competency)
|
|
146
|
+
if ((!node && !competency) || !practiceValid(record, record?.actualMin)) continue
|
|
145
147
|
if (progressPractice.has(practiceKey(day, node, record, record.actualMin))) continue
|
|
146
148
|
const signal = record.solved && record.independent ? 'baseline' : 'gap'
|
|
147
|
-
const label =
|
|
149
|
+
const label = competency && !node
|
|
150
|
+
? (record.solved ? (record.independent ? '独立达到本次交付物要求' : '借助帮助达到本次交付物要求') : '尚未达到本次交付物要求')
|
|
151
|
+
: record.solved ? (record.independent ? '独立做出' : '依赖提示或题解做出') : '尚未做出'
|
|
148
152
|
const summary = `${text(record.problemId) ? `${text(record.problemId)}:` : ''}${label},实际用时 ${record.actualMin} 分钟`
|
|
149
153
|
const locator = `${day}:${encodeURIComponent(text(record.problemId))}`
|
|
150
|
-
const eventId = `practice:${encodeURIComponent(node)}:${day}:${encodeURIComponent(text(record.problemId))}`
|
|
154
|
+
const eventId = `practice:${encodeURIComponent(node || `competency:${competency}`)}:${day}:${encodeURIComponent(text(record.problemId))}`
|
|
151
155
|
add('schedule', node, day, locator, eventId, signal, summary, record)
|
|
152
156
|
}
|
|
153
157
|
}
|
package/lib/curriculum.js
CHANGED
|
@@ -17,10 +17,10 @@ function validDate(value) {
|
|
|
17
17
|
const d = new Date(`${value}T12:00:00Z`)
|
|
18
18
|
return Number.isFinite(d.getTime()) && d.toISOString().slice(0, 10) === value
|
|
19
19
|
}
|
|
20
|
-
export function createCurriculumStore({ dataDir, loadMap, loadProgress, loadSchedule = () => ({ days: {} }), loadStrategyEvidence = () => [], now = () => new Date() }) {
|
|
20
|
+
export function createCurriculumStore({ dataDir, loadMap, loadProgress, loadSchedule = () => ({ days: {} }), loadCompetencies = () => [], loadStrategyEvidence = () => [], now = () => new Date() }) {
|
|
21
21
|
const path = join(dataDir, 'CURRICULUM.yaml')
|
|
22
22
|
const evidence = () => [
|
|
23
|
-
...collectEvidence(loadProgress(), loadSchedule()).filter((entry) => loadMap().byId.has(entry.node)),
|
|
23
|
+
...collectEvidence(loadProgress(), loadSchedule()).filter((entry) => loadMap().byId.has(entry.node) || loadCompetencies().some(item => item.id === entry.competency)),
|
|
24
24
|
...loadStrategyEvidence().filter((entry) => entry && typeof entry.id === 'string' && typeof entry.competency === 'string'),
|
|
25
25
|
]
|
|
26
26
|
function enter(doc, catalog) {
|
|
@@ -227,7 +227,9 @@ export function createCurriculumStore({ dataDir, loadMap, loadProgress, loadSche
|
|
|
227
227
|
return { ...snapshot(null), ok: false, reject: err.message }
|
|
228
228
|
}
|
|
229
229
|
}
|
|
230
|
-
function assignment({ node, revision, purpose = 'progress', reason = '' }) {
|
|
230
|
+
function assignment({ node = '', competency = '', revision, purpose = 'progress', reason = '' }) {
|
|
231
|
+
if (Boolean(node) === Boolean(competency)) return { ok: false, reject: 'node 与 competency 必须且只能指定一个。', binding: null }
|
|
232
|
+
if (competency && !loadCompetencies().some(item => item.id === competency)) return { ok: false, reject: 'competency 必须来自已有比赛能力目录。', binding: null }
|
|
231
233
|
const doc = load()
|
|
232
234
|
if (!doc) return { ok: true, reject: '', binding: null }
|
|
233
235
|
if (doc.status !== 'active') return { ok: false, reject: '课程已经完成,请先明确下一条路线。', binding: null }
|
|
@@ -237,14 +239,15 @@ export function createCurriculumStore({ dataDir, loadMap, loadProgress, loadSche
|
|
|
237
239
|
if (!['progress', 'remediation', 'review'].includes(purpose)) return { ok: false, reject: 'purpose 只能是 progress/remediation/review。', binding: null }
|
|
238
240
|
if (purpose === 'progress') {
|
|
239
241
|
if (state.focusExpired) return { ok: false, reject: '本周主线已到期,先复盘并 revise;不会自动换专题。', binding: null }
|
|
240
|
-
if (!state.focusNodes.includes(node)) return { ok: false, reject: `「${node}」不在当前阶段主线;调整走 revise,补漏/复习要声明 purpose 和 routeReason。`, binding: null }
|
|
242
|
+
if (competency ? !state.phaseCompetencies.includes(competency) : !state.focusNodes.includes(node)) return { ok: false, reject: `「${node || competency}」不在当前阶段主线;调整走 revise,补漏/复习要声明 purpose 和 routeReason。`, binding: null }
|
|
241
243
|
} else if (clean(reason).length < 10) return { ok: false, reject: '补漏/复习需至少 10 字的 routeReason,说明与当前阶段的关系和回归条件。', binding: null }
|
|
242
|
-
if (purpose === 'review' && !['learned', 'verified'].includes(loadProgress().nodes?.[node]?.status)) return { ok: false, reject: '复习节点应有学过或已验证的记录;首次学习不能伪装成复习。', binding: null }
|
|
243
|
-
if (doc.phases[doc.current].kind === 'training') {
|
|
244
|
+
if (purpose === 'review' && node && !['learned', 'verified'].includes(loadProgress().nodes?.[node]?.status)) return { ok: false, reject: '复习节点应有学过或已验证的记录;首次学习不能伪装成复习。', binding: null }
|
|
245
|
+
if (node && doc.phases[doc.current].kind === 'training') {
|
|
244
246
|
const missing = prerequisitesOf([node], loadMap().byId).filter((dependency) => !knownNodes(loadProgress()).has(dependency))
|
|
245
247
|
if (missing.length) return { ok: false, reject: `当前训练还缺显式前置记录:${missing.join('、')};游标不会自动证明前置已掌握。`, binding: null }
|
|
246
248
|
}
|
|
247
|
-
const
|
|
249
|
+
const actionKey = node || `competency:${competency}`
|
|
250
|
+
const other = state.pendingNodes.filter((n) => n !== actionKey)
|
|
248
251
|
if (other.length) return { ok: false, reject: `当前单元还有未完成动作:${other.join('、')}。先完成或撤销,避免又换一节。`, binding: null }
|
|
249
252
|
return { ok: true, reject: '', binding: { revision: doc.revision, phaseId: state.phaseId,
|
|
250
253
|
phaseTitle: state.phaseTitle, purpose, reason: clean(reason) } }
|
package/lib/strategy.js
CHANGED
|
@@ -199,9 +199,16 @@ export function createStrategyStore({ dataDir, vpDir = dataDir, competenciesPath
|
|
|
199
199
|
const saveTarget = (doc) => writeYaml(targetPath, '# TARGET.yaml — 比赛目标与能力优先级\n', doc)
|
|
200
200
|
const saveVp = (doc) => writeYaml(vpPath, '# VP_EVENTS.yaml — VP 记录与赛后复盘\n', doc)
|
|
201
201
|
const loadCompetencies = () => readYaml(competenciesPath, () => ({ version: 1, competencies: [] })).competencies ?? []
|
|
202
|
-
const loadProfiles = () => readYaml(contestProfilesPath, () => ({ profiles: [] })).profiles ?? []
|
|
202
|
+
const loadProfiles = () => (readYaml(contestProfilesPath, () => ({ profiles: [] })).profiles ?? []).map(profile => {
|
|
203
|
+
const samples = asArray(profile.samples)
|
|
204
|
+
return { ...profile, samples, coverage: {
|
|
205
|
+
sampledProblems: samples.length, reviewedSolutions: samples.filter(sample => sample.solutionReviewed === true).length,
|
|
206
|
+
years: [...new Set(samples.map(sample => sample.year))].sort(), isSyllabus: false,
|
|
207
|
+
} }
|
|
208
|
+
})
|
|
203
209
|
const profileFor = id => loadProfiles().find(item => item.id === id) ?? { id: '', title: '', scope: '', status: 'unselected',
|
|
204
|
-
sources: [], competencyIds: [], selectionGuidance: [],
|
|
210
|
+
sources: [], competencyIds: [], selectionGuidance: [], samples: [], coverage: { sampledProblems: 0, reviewedSolutions: 0, years: [], isSyllabus: false },
|
|
211
|
+
limitations: ['未选择比赛样本配置;不能把通用能力卡当作本场考纲。'] }
|
|
205
212
|
|
|
206
213
|
function target(action = 'read', args = {}) {
|
|
207
214
|
let old
|
|
@@ -395,7 +402,7 @@ export function createStrategyStore({ dataDir, vpDir = dataDir, competenciesPath
|
|
|
395
402
|
return [
|
|
396
403
|
`- 比赛目标:${t.contest},目标日期 ${t.date},结果目标 ${t.result},团队模式 ${t.teamMode}`,
|
|
397
404
|
`- 距目标日期:${f.daysRemaining}天;剩余时间影响训练取舍,不能只给所有能力乘同一紧急系数。`,
|
|
398
|
-
`- 比赛样本配置:${f.profile.title || '尚未选择'}(${f.profile.status}
|
|
405
|
+
`- 比赛样本配置:${f.profile.title || '尚未选择'}(${f.profile.status});覆盖 ${f.profile.coverage.sampledProblems} 题,已核对题解 ${f.profile.coverage.reviewedSolutions} 题;不是完整考纲。${f.profile.limitations.join(';')}`,
|
|
399
406
|
`- 当前目标权重:${t.priorities.map((p) => `${p.id}(${p.weight})`).join('、')}`,
|
|
400
407
|
f.focus?.length ? `- 本轮优先能力:${f.focus.map((x) => `${x.title}(${x.whyNow})`).join(';')}` : '- 当前没有可计算的能力重点。',
|
|
401
408
|
f.postponed?.length ? `- 暂缓能力:${f.postponed.join('、')}` : '',
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "acmer-coach",
|
|
3
3
|
"description": "小鲸教练插件:保存训练位置和真实证据,支持教练判断、持续教学与比赛备赛。读 MAP/DEPENDS/PROGRESS,提供训练、判因和检测工具。",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.6.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/Miantiao1231/acmer-coach.git"
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"@deepseek-ai/dsh-tools": "0.2.0-rc.2"
|
|
24
24
|
},
|
|
25
25
|
"scripts": {
|
|
26
|
-
"test": "node --test dev/curriculum-check.mjs dev/curriculum-evidence-check.mjs dev/strategy-check.mjs dev/catalog-check.mjs dev/coaching-contract-check.mjs dev/teaching-check.mjs dev/workspace-migration-check.mjs dev/vp-client-check.mjs dev/vp-sync-check.mjs"
|
|
26
|
+
"test": "node --test dev/curriculum-check.mjs dev/curriculum-evidence-check.mjs dev/strategy-check.mjs dev/catalog-check.mjs dev/coaching-contract-check.mjs dev/teaching-check.mjs dev/grading-check.mjs dev/training-check.mjs dev/workspace-migration-check.mjs dev/vp-client-check.mjs dev/vp-sync-check.mjs"
|
|
27
27
|
},
|
|
28
28
|
"dsh": {
|
|
29
29
|
"engines": {
|
package/verify.mjs
CHANGED
|
@@ -364,8 +364,8 @@ await denyCase('挑了个不是下一层的节点 → 拒绝',
|
|
|
364
364
|
// 而训练员已经把位运算标成已验证 —— 它不再是个"缺前置"的场景了。
|
|
365
365
|
// **别在会变的真实进度上写死假设。** 等价用例(更严)在第 24 节的合成夹具里。
|
|
366
366
|
// ④ 时间盒太短
|
|
367
|
-
await
|
|
368
|
-
|
|
367
|
+
const shortDiagnostic = await assign.execute({ cursor: '动态规划基础', node: '区间 DP', kind: 'diagnostic', deliverable: '解释模型选择', minutes: 15 })
|
|
368
|
+
check('15 分钟诊断可直接布置,不伪造讲课或要求先学完', shortDiagnostic.accepted && shortDiagnostic.totalMinutes === 15, shortDiagnostic.reject)
|
|
369
369
|
// ⑤ 没交付物
|
|
370
370
|
await denyCase('没写交付物 → 拒绝',
|
|
371
371
|
{ cursor: '动态规划基础', node: '区间 DP', deliverable: '' }, '交付物')
|
|
@@ -987,9 +987,9 @@ const progPath5 = () => readFileSync(prog5, 'utf8')
|
|
|
987
987
|
// 卷子还在(第 20 关开的):P1 易 / P4 中 / P6 难,限时 90,通过线 4
|
|
988
988
|
const g1 = await grade.execute({
|
|
989
989
|
results: [
|
|
990
|
-
{ problemId: 'P1', solved: true, minutes: 15 },
|
|
991
|
-
{ problemId: 'P4', solved: true, minutes: 25 },
|
|
992
|
-
{ problemId: 'P6', solved: true, minutes: 30 },
|
|
990
|
+
{ problemId: 'P1', solved: true, minutes: 15, independent: true },
|
|
991
|
+
{ problemId: 'P4', solved: true, minutes: 25, independent: true },
|
|
992
|
+
{ problemId: 'P6', solved: true, minutes: 30, independent: true },
|
|
993
993
|
],
|
|
994
994
|
}, undefined)
|
|
995
995
|
check('判卷成功', g1.ok === true, g1.reject)
|
|
@@ -997,7 +997,7 @@ check('三题全下 → 6 分', g1.score === 6, String(g1.score))
|
|
|
997
997
|
check('用时 70 ≤ 90 → 过', g1.outcome === 'passed', g1.outcome)
|
|
998
998
|
const after1 = progPath5()
|
|
999
999
|
check('★ 写过检测记录(checks)', after1.includes('checks'), after1.includes('checks') ? '' : '没写')
|
|
1000
|
-
check('★
|
|
1000
|
+
check('★ 明确独立且过卷才新增 verified',
|
|
1001
1001
|
after1.includes('status: verified'), after1.includes('status') ? '' : '没写 status')
|
|
1002
1002
|
// 注意别拿**文件头注释**当数据测:头里就写着 "pending 是正押着的那张检测卷",
|
|
1003
1003
|
// 用 includes('pending') 永远为真、永远测不出东西。要盯的是 YAML 顶层键。
|
|
@@ -1012,9 +1012,9 @@ const denyG = async (label, args, want) => {
|
|
|
1012
1012
|
check(label, ok, ok ? '' : `ok=${r.ok} reject="${r.reject}"`)
|
|
1013
1013
|
}
|
|
1014
1014
|
const r3 = [
|
|
1015
|
-
{ problemId: 'P1', solved: true, minutes: 10 },
|
|
1016
|
-
{ problemId: 'P4', solved: true, minutes: 10 },
|
|
1017
|
-
{ problemId: 'P6', solved: true, minutes: 10 },
|
|
1015
|
+
{ problemId: 'P1', solved: true, minutes: 10, independent: true },
|
|
1016
|
+
{ problemId: 'P4', solved: true, minutes: 10, independent: true },
|
|
1017
|
+
{ problemId: 'P6', solved: true, minutes: 10, independent: true },
|
|
1018
1018
|
]
|
|
1019
1019
|
await denyG('手里没押卷就判卷 → 拒绝', { results: r3 }, '没押着')
|
|
1020
1020
|
|
|
@@ -1032,13 +1032,13 @@ await test.execute({ node: '线段树', problems: [
|
|
|
1032
1032
|
] }, undefined)
|
|
1033
1033
|
await denyG('结果少给一道 → 拒绝', { results: r3.slice(0, 2) }, '一道都不能少')
|
|
1034
1034
|
await denyG('报了个不在卷子里的题号 → 拒绝',
|
|
1035
|
-
{ results: [...r3.slice(0, 2), { problemId: 'P99999', solved: true, minutes: 5 }] }, '卷子里没有')
|
|
1035
|
+
{ results: [...r3.slice(0, 2), { problemId: 'P99999', solved: true, minutes: 5, independent: true }] }, '卷子里没有')
|
|
1036
1036
|
|
|
1037
1037
|
const g2 = await grade.execute({
|
|
1038
1038
|
results: [
|
|
1039
|
-
{ problemId: 'P2', solved: true, minutes: 20 },
|
|
1040
|
-
{ problemId: 'P5', solved: true, minutes: 30 },
|
|
1041
|
-
{ problemId: 'P7', solved: true, minutes: 50 }, // 合计 100 > 90
|
|
1039
|
+
{ problemId: 'P2', solved: true, minutes: 20, independent: true },
|
|
1040
|
+
{ problemId: 'P5', solved: true, minutes: 30, independent: true },
|
|
1041
|
+
{ problemId: 'P7', solved: true, minutes: 50, independent: true }, // 合计 100 > 90
|
|
1042
1042
|
],
|
|
1043
1043
|
}, undefined)
|
|
1044
1044
|
check('★ 分数够但超时 → 判「不熟」,不算过', g2.outcome === 'overTime', g2.outcome)
|
|
@@ -1053,7 +1053,7 @@ await test.execute({ node: '线段树', problems: [
|
|
|
1053
1053
|
] }, undefined)
|
|
1054
1054
|
const g3 = await grade.execute({
|
|
1055
1055
|
results: [
|
|
1056
|
-
{ problemId: 'P1', solved: true, minutes: 18 },
|
|
1056
|
+
{ problemId: 'P1', solved: true, minutes: 18, independent: true },
|
|
1057
1057
|
{ problemId: 'P4', solved: false, minutes: 30 },
|
|
1058
1058
|
{ problemId: 'P6', solved: false, minutes: 20 },
|
|
1059
1059
|
],
|
|
@@ -1108,9 +1108,9 @@ check('★ 卷面写明「网上找的 · 核实:…」(他要能复核)',
|
|
|
1108
1108
|
|
|
1109
1109
|
const gWebR = await grade.execute({
|
|
1110
1110
|
results: [
|
|
1111
|
-
{ problemId: 'ABC300-F', solved: true, minutes: 15 },
|
|
1112
|
-
{ problemId: 'P1', solved: true, minutes: 20 },
|
|
1113
|
-
{ problemId: 'P4', solved: true, minutes: 30 },
|
|
1111
|
+
{ problemId: 'ABC300-F', solved: true, minutes: 15, independent: true },
|
|
1112
|
+
{ problemId: 'P1', solved: true, minutes: 20, independent: true },
|
|
1113
|
+
{ problemId: 'P4', solved: true, minutes: 30, independent: true },
|
|
1114
1114
|
],
|
|
1115
1115
|
}, undefined)
|
|
1116
1116
|
check('★ 全下 → 过', gWebR.ok === true && gWebR.outcome === 'passed', `${gWebR.outcome} ${gWebR.reject}`)
|
|
@@ -1168,9 +1168,9 @@ check('★ 新核实的 verifiedReused=false(这次真核了,不是沿用)
|
|
|
1168
1168
|
// 判掉这张卷,才能开第二张(一次只开一张)
|
|
1169
1169
|
const gFirst = await grade.execute({
|
|
1170
1170
|
results: [
|
|
1171
|
-
{ problemId: 'P1', solved: true, minutes: 15 },
|
|
1172
|
-
{ problemId: 'P4', solved: true, minutes: 25 },
|
|
1173
|
-
{ problemId: 'P6', solved: true, minutes: 35 },
|
|
1171
|
+
{ problemId: 'P1', solved: true, minutes: 15, independent: true },
|
|
1172
|
+
{ problemId: 'P4', solved: true, minutes: 25, independent: true },
|
|
1173
|
+
{ problemId: 'P6', solved: true, minutes: 35, independent: true },
|
|
1174
1174
|
],
|
|
1175
1175
|
}, undefined)
|
|
1176
1176
|
check('夹具:第一张卷判掉了', gFirst.ok === true, gFirst.reject)
|
|
@@ -1187,9 +1187,9 @@ check('★ 卷面上写明是沿用台账记录',
|
|
|
1187
1187
|
(test.output.render({}, reuse)[0].text.match(/.*沿用台账.*/) ?? ['(没有)'])[0].slice(0, 60))
|
|
1188
1188
|
const gSecond = await grade.execute({
|
|
1189
1189
|
results: [
|
|
1190
|
-
{ problemId: 'P1', solved: true, minutes: 15 },
|
|
1191
|
-
{ problemId: 'P4', solved: true, minutes: 25 },
|
|
1192
|
-
{ problemId: 'P6', solved: true, minutes: 35 },
|
|
1190
|
+
{ problemId: 'P1', solved: true, minutes: 15, independent: true },
|
|
1191
|
+
{ problemId: 'P4', solved: true, minutes: 25, independent: true },
|
|
1192
|
+
{ problemId: 'P6', solved: true, minutes: 35, independent: true },
|
|
1193
1193
|
],
|
|
1194
1194
|
}, undefined)
|
|
1195
1195
|
check('夹具:第二张卷也判掉了(不留 pending 给后面几节)', gSecond.ok === true, gSecond.reject)
|
|
@@ -1296,9 +1296,9 @@ check('★ 并且逐道标出是沿用的(三道都要,不能只认一道)
|
|
|
1296
1296
|
// "上一张没判"被拒,看着像通过,其实拒的理由跟本关要验的完全无关。
|
|
1297
1297
|
const gClos = await grade.execute({
|
|
1298
1298
|
results: [
|
|
1299
|
-
{ problemId: 'P1', solved: true, minutes: 15 },
|
|
1300
|
-
{ problemId: 'P4', solved: true, minutes: 25 },
|
|
1301
|
-
{ problemId: 'P6', solved: true, minutes: 35 },
|
|
1299
|
+
{ problemId: 'P1', solved: true, minutes: 15, independent: true },
|
|
1300
|
+
{ problemId: 'P4', solved: true, minutes: 25, independent: true },
|
|
1301
|
+
{ problemId: 'P6', solved: true, minutes: 35, independent: true },
|
|
1302
1302
|
],
|
|
1303
1303
|
}, undefined)
|
|
1304
1304
|
check('夹具:闭环那张卷判掉了(不留 pending)', gClos.ok === true, gClos.reject)
|
|
@@ -1371,9 +1371,9 @@ const open22 = await test.execute({ node: '线段树', problems: [
|
|
|
1371
1371
|
check('夹具:22b 那张卷开出来了', open22.ok === true, open22.reject)
|
|
1372
1372
|
const g22 = await grade.execute({
|
|
1373
1373
|
results: [
|
|
1374
|
-
{ problemId: 'P1', solved: true, minutes: 12 },
|
|
1375
|
-
{ problemId: 'P4', solved: true, minutes: 22 },
|
|
1376
|
-
{ problemId: 'P6', solved: true, minutes: 35 },
|
|
1374
|
+
{ problemId: 'P1', solved: true, minutes: 12, independent: true },
|
|
1375
|
+
{ problemId: 'P4', solved: true, minutes: 22, independent: true },
|
|
1376
|
+
{ problemId: 'P6', solved: true, minutes: 35, independent: true },
|
|
1377
1377
|
],
|
|
1378
1378
|
}, undefined)
|
|
1379
1379
|
check('全下 → 过', g22.outcome === 'passed', g22.outcome)
|
|
@@ -1392,7 +1392,7 @@ await test.execute({ node: '线段树', problems: [
|
|
|
1392
1392
|
] }, undefined)
|
|
1393
1393
|
const g22b = await grade.execute({
|
|
1394
1394
|
results: [
|
|
1395
|
-
{ problemId: 'P2', solved: true, minutes: 10 },
|
|
1395
|
+
{ problemId: 'P2', solved: true, minutes: 10, independent: true },
|
|
1396
1396
|
{ problemId: 'P5', solved: false, minutes: 30 },
|
|
1397
1397
|
{ problemId: 'P7', solved: false, minutes: 30 },
|
|
1398
1398
|
],
|
|
@@ -2664,9 +2664,9 @@ check('★ 并且给出了撤卷的出口(不然模型只能编成绩)',
|
|
|
2664
2664
|
// 原先传空串 = 不归一 → 卷子存 `NC15121`、小鲸报 `15121` 就撞不上,
|
|
2665
2665
|
// 判成「卷子里没有这条结果」。判卷是闭环最后一步,错在这整张卷就没了。
|
|
2666
2666
|
const g30 = await T30.get('coach_grade').execute({ results: [
|
|
2667
|
-
{ problemId: '15121', solved: true, minutes: 15 }, // ← 裸号
|
|
2668
|
-
{ problemId: '1015D', solved: true, minutes: 25 },
|
|
2669
|
-
{ problemId: '1016E', solved: true, minutes: 35 },
|
|
2667
|
+
{ problemId: '15121', solved: true, minutes: 15, independent: true }, // ← 裸号
|
|
2668
|
+
{ problemId: '1015D', solved: true, minutes: 25, independent: true },
|
|
2669
|
+
{ problemId: '1016E', solved: true, minutes: 35, independent: true },
|
|
2670
2670
|
]})
|
|
2671
2671
|
check('★ 牛客裸号能对上卷子里的 NC15121', g30.ok === true, JSON.stringify(g30.reject))
|
|
2672
2672
|
check('判完之后卷子清掉了', prog30.load().pending === null,
|
|
@@ -2692,9 +2692,9 @@ p30c.pending = { node: 'A', date: '2026-09-16', at: '2026-09-16 11:00:00',
|
|
|
2692
2692
|
] }
|
|
2693
2693
|
prog30.save(p30c)
|
|
2694
2694
|
const gw = await T30.get('coach_grade').execute({ results: [
|
|
2695
|
-
{ problemId: '1015D', solved: true, minutes: 10 },
|
|
2696
|
-
{ problemId: '1016E', solved: true, minutes: 20 },
|
|
2697
|
-
{ problemId: 'abc999_c', solved: true, minutes: 30 },
|
|
2695
|
+
{ problemId: '1015D', solved: true, minutes: 10, independent: true },
|
|
2696
|
+
{ problemId: '1016E', solved: true, minutes: 20, independent: true },
|
|
2697
|
+
{ problemId: 'abc999_c', solved: true, minutes: 30, independent: true },
|
|
2698
2698
|
]})
|
|
2699
2699
|
check('★ 判卷报出自述题数', gw.selfReported === 1, `${gw.selfReported}`)
|
|
2700
2700
|
check('★ 自述题数落进 checks(事后翻记录分得出这张卷的成色)',
|
|
@@ -3324,27 +3324,27 @@ check('★ 渲染把两个数都印出来(同一个 40 两个意思,就是
|
|
|
3324
3324
|
check('★ 默认时不啰嗦(boxNote 空)', a35.boxNote === '' && !t35a.includes('止损点'), a35.boxNote)
|
|
3325
3325
|
|
|
3326
3326
|
const a35b = await asg35.execute({ ...base35, minutes: 30 })
|
|
3327
|
-
check('★ 净 30 →
|
|
3328
|
-
a35b.accepted === true && a35b.totalMinutes === 65 && a35b.boxNote.includes('
|
|
3327
|
+
check('★ 净 30 → 练习与收尾 50,加讲解共 65,报告可直接排的时长',
|
|
3328
|
+
a35b.accepted === true && a35b.totalMinutes === 65 && a35b.boxNote.includes('minutes=65'),
|
|
3329
3329
|
a35b.boxNote)
|
|
3330
|
-
check('★
|
|
3331
|
-
a35b.boxNote.includes('排不出这个形状') && a35b.boxNote.includes('
|
|
3330
|
+
check('★ 自定时长可以如实排程,不再宣称只能 60/25/90',
|
|
3331
|
+
!a35b.boxNote.includes('排不出这个形状') && a35b.boxNote.includes('排程'),
|
|
3332
3332
|
a35b.boxNote)
|
|
3333
3333
|
const t35b = txt35(asg35, a35b)
|
|
3334
|
-
check('★
|
|
3334
|
+
check('★ 自定时长印进人话里', t35b.includes('minutes=65'),
|
|
3335
3335
|
(t35b.match(/.*⚠️.*/) ?? ['(没有)'])[0].trim())
|
|
3336
3336
|
|
|
3337
3337
|
const a35c = await asg35.execute({ ...base35, minutes: 29 })
|
|
3338
|
-
check('★ 净 29
|
|
3339
|
-
a35c.accepted ===
|
|
3338
|
+
check('★ 净 29 可接受,真实总时长 64 分钟',
|
|
3339
|
+
a35c.accepted === true && a35c.totalMinutes === 64,
|
|
3340
3340
|
a35c.reject.split('\n')[0])
|
|
3341
|
-
check('★
|
|
3342
|
-
a35c.minutes === 29 && a35c.tailMinutes === 20 && a35c.boxNote
|
|
3341
|
+
check('★ 自定时长保存净用时与收尾,不丢说明',
|
|
3342
|
+
a35c.minutes === 29 && a35c.tailMinutes === 20 && a35c.boxNote.includes('minutes=64'),
|
|
3343
3343
|
JSON.stringify({ 净: a35c.minutes, 收尾: a35c.tailMinutes, boxNote: a35c.boxNote }))
|
|
3344
3344
|
|
|
3345
3345
|
const a35d = await asg35.execute({ ...base35, minutes: 60 })
|
|
3346
|
-
check('★ 净 60 → 整块 80
|
|
3347
|
-
a35d.accepted === true && a35d.totalMinutes === 95 && a35d.boxNote.includes('
|
|
3346
|
+
check('★ 净 60 → 整块 80,加讲解共 95,可以直接排程',
|
|
3347
|
+
a35d.accepted === true && a35d.totalMinutes === 95 && a35d.boxNote.includes('minutes=95'),
|
|
3348
3348
|
a35d.boxNote)
|
|
3349
3349
|
|
|
3350
3350
|
// 账本(#6 的 pendingActions)里存的必须是**拆开的数**,口径才不会下次再漂
|