acmer-coach 2.4.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 +34 -3
- package/assets/knowledge/CONTEST_PROFILES.yaml +62 -5
- package/assets/knowledge/skilltree.html +3 -3
- package/assets/rules/coach-rules.md +35 -14
- package/coach/skilltree_template.html +3 -3
- package/dev/catalog-check.mjs +36 -0
- package/dev/coaching-contract-check.mjs +127 -1
- package/dev/curriculum-check.mjs +1 -1
- package/dev/curriculum-evidence-check.mjs +18 -0
- package/dev/grading-check.mjs +161 -0
- package/dev/parity-check.mjs +21 -4
- package/dev/run-all.sh +6 -2
- package/dev/strategy-check.mjs +41 -7
- package/dev/teaching-check.mjs +191 -0
- package/dev/training-check.mjs +183 -0
- package/dev/vp-sync-check.mjs +1 -1
- 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 +37 -0
- package/docs//350/256/276/350/256/241/346/226/271/351/222/210.md +7 -6
- package/index.js +273 -164
- package/lib/curriculum-evidence.js +7 -3
- package/lib/curriculum.js +10 -7
- package/lib/strategy.js +14 -13
- package/lib/teaching.js +78 -0
- package/package.json +3 -3
- package/verify-shape.mjs +35 -5
- package/verify.mjs +82 -63
package/verify.mjs
CHANGED
|
@@ -89,7 +89,7 @@ for (const f of readdirSync(KNOWLEDGE)) {
|
|
|
89
89
|
}
|
|
90
90
|
process.env.COACH_DATA_DIR = tmpEarly
|
|
91
91
|
let stale = []
|
|
92
|
-
for (const f of ['index.js', 'package.json', 'lib/setup.js', 'lib/vp-client.js', 'lib/workspace-migration.js', 'lib/strategy.js', 'lib/curriculum.js', 'lib/curriculum-evidence.js', 'lib/curriculum-planning.js', 'assets/knowledge/NODE_SCOPES.yaml', 'assets/knowledge/CONTEST_PROFILES.yaml', 'assets/rules/coach-rules.md']) {
|
|
92
|
+
for (const f of ['index.js', 'package.json', 'lib/setup.js', 'lib/vp-client.js', 'lib/workspace-migration.js', 'lib/strategy.js', 'lib/teaching.js', 'lib/curriculum.js', 'lib/curriculum-evidence.js', 'lib/curriculum-planning.js', 'assets/knowledge/NODE_SCOPES.yaml', 'assets/knowledge/CONTEST_PROFILES.yaml', 'assets/rules/coach-rules.md']) {
|
|
93
93
|
let same = true
|
|
94
94
|
try {
|
|
95
95
|
same = readFileSync(`${SOURCE_DIR}\\${f}`, 'utf8') === readFileSync(`${INSTALLED_DIR}\\${f}`, 'utf8')
|
|
@@ -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: '' }, '交付物')
|
|
@@ -898,9 +898,9 @@ check('标「在学」建立教学记录(这是"讲过"的机械痕迹)', ma
|
|
|
898
898
|
|
|
899
899
|
// ③ 正面:标过之后同一行要报出「最早的教学痕迹」
|
|
900
900
|
const stYes = await T5('coach_status').execute({}, undefined)
|
|
901
|
-
check('★
|
|
901
|
+
check('★ 标过之后:报出最早接触记录,并说明不证明理解(验收 ③ 正面)',
|
|
902
902
|
/^\d{4}-\d{2}-\d{2}$/.test(stYes.cursorTaught) &&
|
|
903
|
-
T5('coach_status').output.render({}, stYes)[0].text.includes(
|
|
903
|
+
T5('coach_status').output.render({}, stYes)[0].text.includes(`接触记录 ${stYes.cursorTaught}(不证明理解)`),
|
|
904
904
|
`cursorTaught=${JSON.stringify(stYes.cursorTaught)}`)
|
|
905
905
|
// 游标还原 —— 下面几关的夹具是照着"游标本来就是空的"搭的
|
|
906
906
|
if (cursorWas) await T5('coach_set_cursor').execute({ node: cursorWas }, undefined)
|
|
@@ -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
|
],
|
|
@@ -2362,7 +2362,7 @@ function shapeDiff(tool, out) {
|
|
|
2362
2362
|
if (schema.additionalProperties === false && !(k in props)) extra.push(child(k))
|
|
2363
2363
|
}
|
|
2364
2364
|
for (const [k, s] of Object.entries(props)) {
|
|
2365
|
-
if (s?.required && !(k in value)) missing.push(child(k))
|
|
2365
|
+
if ((s?.required === true || (Array.isArray(schema.required) && schema.required.includes(k))) && !(k in value)) missing.push(child(k))
|
|
2366
2366
|
else if (k in value) {
|
|
2367
2367
|
const d = walk(s, value[k], child(k))
|
|
2368
2368
|
extra.push(...d.extra); missing.push(...d.missing)
|
|
@@ -2378,6 +2378,19 @@ function shapeDiff(tool, out) {
|
|
|
2378
2378
|
}
|
|
2379
2379
|
return walk(tool.output.schema, out ?? {}, '')
|
|
2380
2380
|
}
|
|
2381
|
+
const optionalObjectSchema = { type: 'object', required: ['summary'], properties: {
|
|
2382
|
+
summary: { type: 'string' },
|
|
2383
|
+
studentEvidence: { type: 'object', required: ['quote'], properties: { quote: { type: 'string' } } },
|
|
2384
|
+
} }
|
|
2385
|
+
const schemaTool = { output: { schema: optionalObjectSchema } }
|
|
2386
|
+
check('★ 形状尺:可选对象缺省合法,子字段 required 不代表父对象必填',
|
|
2387
|
+
shapeDiff(schemaTool, { summary: '教练判断' }).missing.length === 0)
|
|
2388
|
+
check('★ 形状尺:父对象 required 列表中的字段缺省仍拒绝',
|
|
2389
|
+
shapeDiff(schemaTool, {}).missing.join(',') === 'summary')
|
|
2390
|
+
check('★ 形状尺:提供可选对象后,仍核对子字段 required',
|
|
2391
|
+
shapeDiff(schemaTool, { summary: '教练判断', studentEvidence: {} }).missing.join(',') === 'studentEvidence.quote')
|
|
2392
|
+
check('★ 形状尺:原 DSL 的 required=true 仍核对',
|
|
2393
|
+
shapeDiff({ output: { schema: { type: 'object', properties: { summary: { type: 'string', required: true } } } } }, {}).missing.join(',') === 'summary')
|
|
2381
2394
|
const t1 = await planT.execute({ date: D[3], blocks: [{ from: '10:00', kind: 'review', node: 'A' }] })
|
|
2382
2395
|
check('工具层:排得进去', t1.ok === true && t1.count === 1, t1.reject)
|
|
2383
2396
|
const sd1 = shapeDiff(planT, t1)
|
|
@@ -2651,9 +2664,9 @@ check('★ 并且给出了撤卷的出口(不然模型只能编成绩)',
|
|
|
2651
2664
|
// 原先传空串 = 不归一 → 卷子存 `NC15121`、小鲸报 `15121` 就撞不上,
|
|
2652
2665
|
// 判成「卷子里没有这条结果」。判卷是闭环最后一步,错在这整张卷就没了。
|
|
2653
2666
|
const g30 = await T30.get('coach_grade').execute({ results: [
|
|
2654
|
-
{ problemId: '15121', solved: true, minutes: 15 }, // ← 裸号
|
|
2655
|
-
{ problemId: '1015D', solved: true, minutes: 25 },
|
|
2656
|
-
{ 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 },
|
|
2657
2670
|
]})
|
|
2658
2671
|
check('★ 牛客裸号能对上卷子里的 NC15121', g30.ok === true, JSON.stringify(g30.reject))
|
|
2659
2672
|
check('判完之后卷子清掉了', prog30.load().pending === null,
|
|
@@ -2679,9 +2692,9 @@ p30c.pending = { node: 'A', date: '2026-09-16', at: '2026-09-16 11:00:00',
|
|
|
2679
2692
|
] }
|
|
2680
2693
|
prog30.save(p30c)
|
|
2681
2694
|
const gw = await T30.get('coach_grade').execute({ results: [
|
|
2682
|
-
{ problemId: '1015D', solved: true, minutes: 10 },
|
|
2683
|
-
{ problemId: '1016E', solved: true, minutes: 20 },
|
|
2684
|
-
{ 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 },
|
|
2685
2698
|
]})
|
|
2686
2699
|
check('★ 判卷报出自述题数', gw.selfReported === 1, `${gw.selfReported}`)
|
|
2687
2700
|
check('★ 自述题数落进 checks(事后翻记录分得出这张卷的成色)',
|
|
@@ -3120,9 +3133,9 @@ check('studying 的用 at 算天数(3 天前标的)', rB33?.days === 3, `${r
|
|
|
3120
3133
|
const txt33 = T33.get('coach_schedule').output.render({}, sch33)[0].text
|
|
3121
3134
|
check('★ 渲染里摆得出来,且标明"该不该动是你判"',
|
|
3122
3135
|
txt33.includes('火候') && txt33.includes('该不该动是你判'), (txt33.match(/.*火候.*/) ?? [''])[0])
|
|
3123
|
-
check('★
|
|
3124
|
-
txt33.includes('
|
|
3125
|
-
(txt33.match(
|
|
3136
|
+
check('★ 排课建议依据目标与表现,不强制持续开新课',
|
|
3137
|
+
txt33.includes('根据目标') && txt33.includes('实际表现') && txt33.includes('迁移或 VP') && !txt33.includes('剩下的时间留给推进'),
|
|
3138
|
+
(txt33.match(/.*根据目标.*/) ?? ['(没有)'])[0].slice(0, 70))
|
|
3126
3139
|
|
|
3127
3140
|
// 没有火候(全是未学/没有记录)时不该硬塞一段
|
|
3128
3141
|
const tmp33b = mkdtempSync(path.join(os.tmpdir(), 'coach-ready-b-'))
|
|
@@ -3311,27 +3324,27 @@ check('★ 渲染把两个数都印出来(同一个 40 两个意思,就是
|
|
|
3311
3324
|
check('★ 默认时不啰嗦(boxNote 空)', a35.boxNote === '' && !t35a.includes('止损点'), a35.boxNote)
|
|
3312
3325
|
|
|
3313
3326
|
const a35b = await asg35.execute({ ...base35, minutes: 30 })
|
|
3314
|
-
check('★ 净 30 →
|
|
3315
|
-
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'),
|
|
3316
3329
|
a35b.boxNote)
|
|
3317
|
-
check('★
|
|
3318
|
-
a35b.boxNote.includes('排不出这个形状') && a35b.boxNote.includes('
|
|
3330
|
+
check('★ 自定时长可以如实排程,不再宣称只能 60/25/90',
|
|
3331
|
+
!a35b.boxNote.includes('排不出这个形状') && a35b.boxNote.includes('排程'),
|
|
3319
3332
|
a35b.boxNote)
|
|
3320
3333
|
const t35b = txt35(asg35, a35b)
|
|
3321
|
-
check('★
|
|
3334
|
+
check('★ 自定时长印进人话里', t35b.includes('minutes=65'),
|
|
3322
3335
|
(t35b.match(/.*⚠️.*/) ?? ['(没有)'])[0].trim())
|
|
3323
3336
|
|
|
3324
3337
|
const a35c = await asg35.execute({ ...base35, minutes: 29 })
|
|
3325
|
-
check('★ 净 29
|
|
3326
|
-
a35c.accepted ===
|
|
3338
|
+
check('★ 净 29 可接受,真实总时长 64 分钟',
|
|
3339
|
+
a35c.accepted === true && a35c.totalMinutes === 64,
|
|
3327
3340
|
a35c.reject.split('\n')[0])
|
|
3328
|
-
check('★
|
|
3329
|
-
a35c.minutes === 29 && a35c.tailMinutes === 20 && a35c.boxNote
|
|
3341
|
+
check('★ 自定时长保存净用时与收尾,不丢说明',
|
|
3342
|
+
a35c.minutes === 29 && a35c.tailMinutes === 20 && a35c.boxNote.includes('minutes=64'),
|
|
3330
3343
|
JSON.stringify({ 净: a35c.minutes, 收尾: a35c.tailMinutes, boxNote: a35c.boxNote }))
|
|
3331
3344
|
|
|
3332
3345
|
const a35d = await asg35.execute({ ...base35, minutes: 60 })
|
|
3333
|
-
check('★ 净 60 → 整块 80
|
|
3334
|
-
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'),
|
|
3335
3348
|
a35d.boxNote)
|
|
3336
3349
|
|
|
3337
3350
|
// 账本(#6 的 pendingActions)里存的必须是**拆开的数**,口径才不会下次再漂
|
|
@@ -3495,7 +3508,7 @@ mod38.apply(mockCtx(reg38))
|
|
|
3495
3508
|
const T38 = (n) => reg38.find((t) => t.name === n)
|
|
3496
3509
|
const asg38 = T38('coach_assign')
|
|
3497
3510
|
const prog38 = () => readFileSync(path.join(tmp38, 'PROGRESS.yaml'), 'utf8')
|
|
3498
|
-
// 夹具里的游标是空的 —— 先设上。(`coach_status`
|
|
3511
|
+
// 夹具里的游标是空的 —— 先设上。(`coach_status` 报接触日期读的是
|
|
3499
3512
|
// PROGRESS.yaml 里的游标,不是 assign 那条路径传进来的 cursor 参数。)
|
|
3500
3513
|
await T38('coach_set_cursor').execute({ node: '记忆化搜索' })
|
|
3501
3514
|
|
|
@@ -3504,22 +3517,28 @@ const g38a = await asg38.execute({ cursor: '记忆化搜索', node: '记忆化
|
|
|
3504
3517
|
check('★ 游标节点 + 没讲过 + 不给 teachMinutes → 拒(这就是那个洞)',
|
|
3505
3518
|
g38a.accepted === false && g38a.reject.includes('全新知识点'), g38a.reject.split('\n')[0])
|
|
3506
3519
|
|
|
3507
|
-
// ②
|
|
3520
|
+
// ② 带讲授计划 → 放行,但计划不能伪装成已发生的教学。
|
|
3508
3521
|
const g38b = await asg38.execute({
|
|
3509
3522
|
cursor: '记忆化搜索', node: '记忆化搜索', deliverable: '贴代码', minutes: 40, teachMinutes: 20 })
|
|
3510
3523
|
check('★ 带 teachMinutes → 放行', g38b.accepted === true, g38b.reject)
|
|
3511
|
-
check('★
|
|
3512
|
-
prog38().includes('taught:'),
|
|
3524
|
+
check('★ 讲授计划不写 taught:计划不等于实际接触',
|
|
3525
|
+
!prog38().includes('taught:'), prog38())
|
|
3513
3526
|
const st38 = await T38('coach_status').execute({})
|
|
3514
|
-
check('★
|
|
3515
|
-
st38.cursorTaught
|
|
3516
|
-
|
|
3527
|
+
check('★ 只有计划时 cursorTaught 仍为空,计划时长另存在待办里',
|
|
3528
|
+
st38.cursorTaught === '' && st38.pendingActions.find((a) => a.node === '记忆化搜索')?.teachMinutes === 20,
|
|
3529
|
+
`cursorTaught=${st38.cursorTaught || '(空)'}`)
|
|
3530
|
+
check('★ 讲授计划不动 status(还没发生的课不能变成学习记录)',
|
|
3517
3531
|
st38.studyingCount === 0 && st38.learnedCount === 0 && st38.verifiedCount === 0,
|
|
3518
3532
|
JSON.stringify({ 在学: st38.studyingCount, 学过: st38.learnedCount, 已验证: st38.verifiedCount }))
|
|
3519
3533
|
|
|
3520
|
-
// ③
|
|
3534
|
+
// ③ 计划不能解锁继续练;教练记录实际进入学习后才适用游标例外。
|
|
3521
3535
|
const g38c = await asg38.execute({ cursor: '记忆化搜索', node: '记忆化搜索', deliverable: '贴代码', minutes: 40 })
|
|
3522
|
-
check('★
|
|
3536
|
+
check('★ 只有计划:不给 teachMinutes 仍拒绝',
|
|
3537
|
+
g38c.accepted === false && g38c.reject.includes('全新知识点'), g38c.reject)
|
|
3538
|
+
const marked38 = await T38('coach_mark').execute({ node: '记忆化搜索', status: 'studying' })
|
|
3539
|
+
check('★ 实际开始学习由教练显式记录', marked38.ok === true, marked38.reject)
|
|
3540
|
+
const g38real = await asg38.execute({ cursor: '记忆化搜索', node: '记忆化搜索', deliverable: '贴代码', minutes: 40 })
|
|
3541
|
+
check('★ 实际接触之后:游标节点再布置不用重讲', g38real.accepted === true, g38real.reject)
|
|
3523
3542
|
|
|
3524
3543
|
// ④ 不误伤:非游标的新节点照样要讲
|
|
3525
3544
|
const g38d = await asg38.execute({ cursor: '记忆化搜索', node: '区间 DP', deliverable: '贴代码', minutes: 40 })
|