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 CHANGED
@@ -121,6 +121,14 @@ learned/verified 记录,或路线中在此前安排的必验训练,不能从
121
121
 
122
122
  详见 [长期课程机制](docs/长期课程机制.md)。
123
123
 
124
+ ### 2.6:独立验收、灵活训练与逐题依据
125
+
126
+ 章节分数及限时通过仍记为 `passed`,但只有本次做出的题都明确报告 `independent: true`,才新增 `verified` 和验证段位。独立性未知或有帮助时,保留实际成绩,不能自动认证;旧证书保留,不补造独立事实。独立性来自明确记录,不是工具能从 AC 猜出的结论。延迟保持与迁移仍由既有课程 `exitChecks` 单独验收。
127
+
128
+ 训练沿用 `coach_assign` / `coach_log` / `coach_plan`:短诊断和整场 VP 可以直接绑定比赛能力,时长由教练明确给出;旧知识点练习和默认时长继续支持。计划只记录安排,不证明训练完成。
129
+
130
+ `coach_target profiles` 和 `coach_focus` 返回逐题 `samples` 与 `coverage`。中国亚洲区域赛主配置包含南京 2022 全卷十三题和 2023 六题,每题列模型、能力和命题组解题页;NWERC 十三题仅作扩展参考,WF 的三题仍是题面诊断。西安目录已经核对,题解未获取,算法样本数如实为零。覆盖数字不产生专题频率、奖牌线或预测;小鲸结合个人证据决定重点。
131
+
124
132
  ### 2.5:接着教,判断仍归小鲸
125
133
 
126
134
  `coach_note` 只做一件事:把一个知识点的教学判断和接续点留在现有
@@ -2,17 +2,69 @@
2
2
  version: 1
3
3
  profiles:
4
4
  - id: regional-diagnostic
5
- title: 区域赛通用能力诊断(有限样本)
5
+ title: 中国亚洲区域赛能力诊断(南京 2022/2023 样本)
6
+ scope: 以中国亚洲区域赛原题和命题组题解建立模型诊断,结合本人和团队的 VP 事实取舍。
7
+ status: provisional
8
+ sources:
9
+ - {title: SUA 南京 2022 命题组中文题解(全卷十三题), url: 'https://sua.ac/wiki/2022-icpc-nanjing/tutorial-sketch-zh.pdf'}
10
+ - {title: SUA 南京 2023 命题组题解(QOJ 镜像,采样六题), url: 'https://universal-cup-website.qoj.ac/files/ucup/tutorials/tutorials-2-11.pdf'}
11
+ - {title: QOJ 西安历年比赛目录(对应赛区的原题入口), url: 'https://qoj.ac/category/87'}
12
+ competencyIds: [contest-reading, contest-decision, implementation-stability, dp-modeling, graph-modeling, data-structures, math-combinatorics, code-review, team-communication]
13
+ samples:
14
+ - {year: 2022, contest: Nanjing, problem: I, model: 从旋转回文条件推出字符频次计数, competencyIds: [contest-reading, implementation-stability], solutionReviewed: true, sourceUrl: 'https://sua.ac/wiki/2022-icpc-nanjing/tutorial-sketch-zh.pdf#page=3'}
15
+ - {year: 2022, contest: Nanjing, problem: G, model: 维护总和及数量的可反悔贪心, competencyIds: [math-combinatorics, implementation-stability], solutionReviewed: true, sourceUrl: 'https://sua.ac/wiki/2022-icpc-nanjing/tutorial-sketch-zh.pdf#page=5'}
16
+ - {year: 2022, contest: Nanjing, problem: D, model: 二分阈值与有效操作区间前缀统计, competencyIds: [contest-reading, data-structures], solutionReviewed: true, sourceUrl: 'https://sua.ac/wiki/2022-icpc-nanjing/tutorial-sketch-zh.pdf#page=7'}
17
+ - {year: 2022, contest: Nanjing, problem: B, model: 单调队列优化 DP 与前后缀复用, competencyIds: [dp-modeling, data-structures], solutionReviewed: true, sourceUrl: 'https://sua.ac/wiki/2022-icpc-nanjing/tutorial-sketch-zh.pdf#page=10'}
18
+ - {year: 2022, contest: Nanjing, problem: A, model: 整体移动转边界与二维前缀和, competencyIds: [contest-reading, data-structures], solutionReviewed: true, sourceUrl: 'https://sua.ac/wiki/2022-icpc-nanjing/tutorial-sketch-zh.pdf#page=15'}
19
+ - {year: 2022, contest: Nanjing, problem: M, model: 局部极小与水平边界的分类计数, competencyIds: [math-combinatorics, implementation-stability], solutionReviewed: true, sourceUrl: 'https://sua.ac/wiki/2022-icpc-nanjing/tutorial-sketch-zh.pdf#page=19'}
20
+ - {year: 2022, contest: Nanjing, problem: J, model: 等式变二分图边与连通块配对, competencyIds: [graph-modeling], solutionReviewed: true, sourceUrl: 'https://sua.ac/wiki/2022-icpc-nanjing/tutorial-sketch-zh.pdf#page=22'}
21
+ - {year: 2022, contest: Nanjing, problem: E, model: 树形 DP 与虚树深度统计, competencyIds: [dp-modeling, graph-modeling], solutionReviewed: true, sourceUrl: 'https://sua.ac/wiki/2022-icpc-nanjing/tutorial-sketch-zh.pdf#page=25'}
22
+ - {year: 2022, contest: Nanjing, problem: C, model: 逆序操作与通配位置的贪心构造, competencyIds: [contest-reading, implementation-stability], solutionReviewed: true, sourceUrl: 'https://sua.ac/wiki/2022-icpc-nanjing/tutorial-sketch-zh.pdf#page=28'}
23
+ - {year: 2022, contest: Nanjing, problem: H, model: 树上边贡献与凹性背包合并优化, competencyIds: [dp-modeling, data-structures], solutionReviewed: true, sourceUrl: 'https://sua.ac/wiki/2022-icpc-nanjing/tutorial-sketch-zh.pdf#page=33'}
24
+ - {year: 2022, contest: Nanjing, problem: K, model: 堆排列概率与完全二叉树对称计数, competencyIds: [math-combinatorics], solutionReviewed: true, sourceUrl: 'https://sua.ac/wiki/2022-icpc-nanjing/tutorial-sketch-zh.pdf#page=36'}
25
+ - {year: 2022, contest: Nanjing, problem: L, model: 链上弦覆盖与二边割的增量维护, competencyIds: [graph-modeling, data-structures], solutionReviewed: true, sourceUrl: 'https://sua.ac/wiki/2022-icpc-nanjing/tutorial-sketch-zh.pdf#page=45'}
26
+ - {year: 2022, contest: Nanjing, problem: F, model: 锐角三角剖分的基例与递增构造, competencyIds: [math-combinatorics], solutionReviewed: true, sourceUrl: 'https://sua.ac/wiki/2022-icpc-nanjing/tutorial-sketch-zh.pdf#page=48'}
27
+ - {year: 2023, contest: Nanjing, problem: I, model: 按重置时间排序并检查约束区间一致性, competencyIds: [contest-reading, implementation-stability], solutionReviewed: true, sourceUrl: 'https://universal-cup-website.qoj.ac/files/ucup/tutorials/tutorials-2-11.pdf#page=3'}
28
+ - {year: 2023, contest: Nanjing, problem: F, model: 最后写入者依赖 DAG 与拓扑序交换, competencyIds: [graph-modeling], solutionReviewed: true, sourceUrl: 'https://universal-cup-website.qoj.ac/files/ucup/tutorials/tutorials-2-11.pdf#page=5'}
29
+ - {year: 2023, contest: Nanjing, problem: C, model: 异或计数同余与区间分解,题名不是算法标签, competencyIds: [math-combinatorics], solutionReviewed: true, sourceUrl: 'https://universal-cup-website.qoj.ac/files/ucup/tutorials/tutorials-2-11.pdf#page=7'}
30
+ - {year: 2023, contest: Nanjing, problem: G, model: 交换论证结合前缀选择与后缀背包, competencyIds: [dp-modeling, data-structures], solutionReviewed: true, sourceUrl: 'https://universal-cup-website.qoj.ac/files/ucup/tutorials/tutorials-2-11.pdf#page=9'}
31
+ - {year: 2023, contest: Nanjing, problem: A, model: 双位置可达状态与相对移动约束, competencyIds: [graph-modeling], solutionReviewed: true, sourceUrl: 'https://universal-cup-website.qoj.ac/files/ucup/tutorials/tutorials-2-11.pdf#page=11'}
32
+ - {year: 2023, contest: Nanjing, problem: L, model: 降序楼层贪心与放松下界的匹配证明, competencyIds: [math-combinatorics, contest-reading], solutionReviewed: true, sourceUrl: 'https://universal-cup-website.qoj.ac/files/ucup/tutorials/tutorials-2-11.pdf#page=13'}
33
+ selectionGuidance:
34
+ - 中国亚洲区域赛优先使用对应赛区和年份的真题;先诊断读题、模型选择和实现,不按 CF rating 连续解锁知识点。
35
+ - 结合本人 VP 已确认的失败原因选择本周重点;一道成功题可作为起点,不证明同模型已经掌握。
36
+ - I/G 用于条件提炼与贪心证明,D/A 用于变量转化和统计,B 用于从朴素转移推导优化;模型对应题解,训练取舍由教练决定。
37
+ - 西安目标优先看西安配置及对应原题;NWERC 只作扩展参考,不用欧洲样本代替中国赛区依据。
38
+ limitations:
39
+ - 当前采样南京 2022 全卷十三题和 2023 六题,未做跨年份与跨赛区专题频率统计;不能推出中国区域赛频率或本届西安预测。
40
+ - 这不是完整考纲,模型是教学解释而非唯一解法;验收阈值需按个人和团队事实调整。
41
+ - 未核验目标届次奖牌榜单,不给出金牌题数或奖牌保证。
42
+ - id: nwerc-reference
43
+ title: 区域赛通用能力诊断(NWERC 2024 全卷样本)
6
44
  scope: 用公开区域赛的题面与官方解题建立诊断任务,再结合本人 VP 证据取舍。
7
45
  status: provisional
8
46
  sources:
9
- - title: NWERC 2024 官方题面(采样 A、D、E、F、K、L)
47
+ - title: NWERC 2024 官方题面(A–M 全卷)
10
48
  url: https://2024.nwerc.eu/main/problem-set.pdf
11
- - title: NWERC 2024 官方解题(A/D/E/F/K/L 对应页)
49
+ - title: NWERC 2024 官方解题(A–M 对应页)
12
50
  url: https://2024.nwerc.eu/main/solutions.pdf
13
51
  - title: ICPC NCNA 公布的区域赛规则(通用背景,不提供专题频率)
14
52
  url: https://nc.na.icpc.global/rules/
15
53
  competencyIds: [contest-reading, contest-decision, implementation-stability, dp-modeling, graph-modeling, data-structures, math-combinatorics, code-review, team-communication]
54
+ samples:
55
+ - {year: 2024, contest: NWERC, problem: A, model: 自定义排序规则, competencyIds: [contest-reading, implementation-stability], solutionReviewed: true, sourceUrl: 'https://2024.nwerc.eu/main/solutions.pdf#page=4'}
56
+ - {year: 2024, contest: NWERC, problem: B, model: 周期标号游走与状态动态规划,题名不能决定算法, competencyIds: [graph-modeling, dp-modeling], solutionReviewed: true, sourceUrl: 'https://2024.nwerc.eu/main/solutions.pdf#page=24'}
57
+ - {year: 2024, contest: NWERC, problem: C, model: 网格极端点收缩与几何分类, competencyIds: [math-combinatorics, graph-modeling], solutionReviewed: true, sourceUrl: 'https://2024.nwerc.eu/main/solutions.pdf#page=19'}
58
+ - {year: 2024, contest: NWERC, problem: D, model: 排序后按最小元素分类计数与背包, competencyIds: [dp-modeling, math-combinatorics], solutionReviewed: true, sourceUrl: 'https://2024.nwerc.eu/main/solutions.pdf#page=11'}
59
+ - {year: 2024, contest: NWERC, problem: E, model: 下标变换与模幂, competencyIds: [math-combinatorics], solutionReviewed: true, sourceUrl: 'https://2024.nwerc.eu/main/solutions.pdf#page=8'}
60
+ - {year: 2024, contest: NWERC, problem: F, model: 单调栈和并查集跳过已满位置, competencyIds: [data-structures], solutionReviewed: true, sourceUrl: 'https://2024.nwerc.eu/main/solutions.pdf#page=13'}
61
+ - {year: 2024, contest: NWERC, problem: G, model: 双连通分量与排列奇偶性, competencyIds: [graph-modeling, math-combinatorics], solutionReviewed: true, sourceUrl: 'https://2024.nwerc.eu/main/solutions.pdf#page=31'}
62
+ - {year: 2024, contest: NWERC, problem: H, model: 交互函数迭代的等式构造,存在多种解法, competencyIds: [graph-modeling, math-combinatorics], solutionReviewed: true, sourceUrl: 'https://2024.nwerc.eu/main/solutions.pdf#page=16'}
63
+ - {year: 2024, contest: NWERC, problem: I, model: 素数指数参数化与容斥预处理, competencyIds: [math-combinatorics], solutionReviewed: true, sourceUrl: 'https://2024.nwerc.eu/main/solutions.pdf#page=28'}
64
+ - {year: 2024, contest: NWERC, problem: J, model: 独立优化与最近更高障碍限制, competencyIds: [contest-reading, implementation-stability], solutionReviewed: true, sourceUrl: 'https://2024.nwerc.eu/main/solutions.pdf#page=7'}
65
+ - {year: 2024, contest: NWERC, problem: K, model: 最短路与按距离排序的概率期望, competencyIds: [graph-modeling, math-combinatorics], solutionReviewed: true, sourceUrl: 'https://2024.nwerc.eu/main/solutions.pdf#page=15'}
66
+ - {year: 2024, contest: NWERC, problem: L, model: 单调可行性与贪心配对二分, competencyIds: [contest-reading, implementation-stability], solutionReviewed: true, sourceUrl: 'https://2024.nwerc.eu/main/solutions.pdf#page=10'}
67
+ - {year: 2024, contest: NWERC, problem: M, model: 期望线性性与面积前后缀求和,检查整数溢出, competencyIds: [math-combinatorics, implementation-stability], solutionReviewed: true, sourceUrl: 'https://2024.nwerc.eu/main/solutions.pdf#page=22'}
16
68
  selectionGuidance:
17
69
  - 先用整场 VP 诊断读题、实现、调试与分工,再用经确认的失败原因选择专题;成功题用作能力基线。
18
70
  - 采样 A 检查从精确排序规则到实现的转化;D 检查计数状态;E 检查大次数变换与模幂,题名不决定算法。
@@ -20,9 +72,9 @@ profiles:
20
72
  - 赛前优先修复已证实、可在剩余时间完成验收的障碍;未证实的缺口先安排短诊断,不直接扩知识点。
21
73
  - 同一题允许多种正确解法;把能解释条件并独立迁移作为验收,不能将某个算法标签当唯一答案。
22
74
  limitations:
23
- - 当前算法样本只有 NWERC 2024 的六题,不能推出中国区域赛频率、任何场次金牌题数或西安出题预测。
75
+ - 当前算法样本只有 NWERC 2024 的一届全卷十三题,不能推出中国区域赛频率、任何场次金牌题数或西安出题预测。
24
76
  - 读题时间盒、换题条件、验收动作是插件教学设计,需要按个人与团队记录调整,规则来源不证明这些训练阈值。
25
- - 未采样其余题或跨年份统计;此配置不是西安考纲,排名与奖牌目标仍需对应比赛榜单。
77
+ - 未做跨年份统计;此配置不是西安考纲,排名与奖牌目标仍需对应比赛榜单。模型映射是教学解释,官方解题不证明该题在其他赛区高频。
26
78
  - id: wf-extension
27
79
  title: WF 扩展能力诊断(题面样本)
28
80
  scope: 在基础能力稳定后,以 World Finals 原题检查跨模型推理、约束解释和交互实现。
@@ -33,6 +85,10 @@ profiles:
33
85
  - title: ICPC 2024 交互题评测说明(输出刷新等要求)
34
86
  url: https://docs.icpc.global/wp-content/uploads/2024/08/2024-JudgingNotes.pdf
35
87
  competencyIds: [contest-reading, contest-decision, dp-modeling, graph-modeling, data-structures, math-combinatorics, code-review, team-communication]
88
+ samples:
89
+ - {year: 2024, contest: World Finals, problem: B, model: 概率对象与事件依赖诊断,题解尚未核验, competencyIds: [math-combinatorics], solutionReviewed: false, sourceUrl: 'https://icpc.global/worldfinals/problems/2024-ICPC-World-Finals/icpc2024.pdf'}
90
+ - {year: 2024, contest: World Finals, problem: I, model: 树上覆盖目标建模,题解尚未核验, competencyIds: [graph-modeling], solutionReviewed: false, sourceUrl: 'https://icpc.global/worldfinals/problems/2024-ICPC-World-Finals/icpc2024.pdf'}
91
+ - {year: 2024, contest: World Finals, problem: L, model: 交互信息状态诊断,策略尚未核验, competencyIds: [contest-reading], solutionReviewed: false, sourceUrl: 'https://icpc.global/worldfinals/problems/2024-ICPC-World-Finals/icpc2024.pdf'}
36
92
  selectionGuidance:
37
93
  - 采样 B 用于检查概率对象、重复元素与事件依赖的精确表达;不能仅凭概率题标签选一个模板。
38
94
  - 采样 I 用于检查树上覆盖目标与最坏响应时间的建模,要求先解释判定或优化方案再实现。
@@ -52,6 +108,7 @@ profiles:
52
108
  - title: ICPC NCNA 公布的区域赛规则(通用背景,非西安本届规则)
53
109
  url: https://nc.na.icpc.global/rules/
54
110
  competencyIds: [contest-reading, contest-decision, implementation-stability, code-review, team-communication]
111
+ samples: []
55
112
  selectionGuidance:
56
113
  - 使用本人和当前团队 VP 的经确认障碍安排短期动作,专题需求暂参考区域赛通用诊断并明示来源。
57
114
  - 先核验目标届次、原始题面、主办方题解和榜单,再记录逐题模型、可替代解法及样本年份。
@@ -1335,7 +1335,7 @@ const SNAP = 5;
1335
1335
  // 块型短名。**必须和 host 的 SCHED_KINDS[k].name 逐字一致** ——
1336
1336
  // 不一致时页面会拿 id 兜底显示(不会崩),但那块看着就像坏了。
1337
1337
  // host 是真相源,这里只管显示。verify.mjs 有一条盯着这个一致性。
1338
- const KIND_NAME = { cycle: "题目循环", review: "复习一题", exam: "章节检测" };
1338
+ const KIND_NAME = { cycle: "题目循环", review: "复习一题", exam: "章节检测", diagnostic: "短诊断", vp: "完整 VP" };
1339
1339
 
1340
1340
  const minToHM = (m) => {
1341
1341
  const t = HOUR_FROM * 60 + m;
@@ -1447,7 +1447,7 @@ function renderSched() {
1447
1447
  const kind = String(b.kind ?? "");
1448
1448
  // 地图改过名、甚至删过节点,而计划是历史数据 ——
1449
1449
  // 查不到名字就退回 id,**不能因为查不到就不画**(那等于计划凭空消失)。
1450
- const nm = byId(b.node)?.name ?? String(b.node ?? "");
1450
+ const nm = b.competency ? `能力:${b.competency}` : byId(b.node)?.name ?? String(b.node ?? "");
1451
1451
  const size = mins >= 75 ? "s-full" : mins >= 40 ? "s-mid" : "s-mini";
1452
1452
  // 中等高度(40~74 分钟,循环块正好 60)挤不下两行,但**时间要留住** ——
1453
1453
  // busy 那边中档只放事由就够(他自己知道标了什么),
@@ -1457,7 +1457,7 @@ function renderSched() {
1457
1457
  ? `<span class="t">${esc(b.from)}–${esc(b.to)}</span>${esc(nm)}`
1458
1458
  : size === "s-mid" ? `${esc(b.from)} ${esc(nm)}` : "";
1459
1459
  const tip = `${b.from}–${b.to} ${KIND_NAME[kind] ?? kind} ${nm}` +
1460
- (kind === "cycle" ? " (40 自己做 → 10 解决遗留 → 10 重写)" : "");
1460
+ (kind === "cycle" && mins === 60 ? " (40 自己做 → 10 解决遗留 → 10 重写)" : "");
1461
1461
  return `<div class="pbar k-${esc(kind)} ${size}" title="${esc(tip)}" ` +
1462
1462
  `style="top:${pct(a)};height:${pct(mins)}">${body}</div>`;
1463
1463
  }).join("");
@@ -13,6 +13,8 @@
13
13
  比赛目标不能悄悄覆盖长期目标。赛前在剩余实际容量内选择可验收的改善,赛后再按新
14
14
  证据修订;日期只提供约束,不证明某专题该学。无法装进预算时说明暂缓什么与代价。
15
15
  * `coach_target profiles` 提供有来源和样本限制的配置,再按需设置 `profileId`。
16
+ `samples` 是逐题模型与来源,`coverage` 是已核验覆盖;零样本就是没有依据。根据题目
17
+ 条件选模型,不能从题名或一次样本计数推出必考、高频或完整考纲。
16
18
  provisional 表示有限样本或尚待取样,不能称官方考纲、预测本届高频专题或保证奖牌。
17
19
  不因某知识点“低频”就永久排除,不把同一道题的某一种解法当唯一算法标签。
18
20
  * 原始 VP 判题、耗时和提交是观察;WA 不等于不会,AC 不证明独立完成。先用
@@ -402,6 +404,12 @@
402
404
 
403
405
  ### 二、验收:什么才算"会了"
404
406
 
407
+ * `coach_grade` 的 `passed` 只表示卷面分数和限时通过;本次做出题的 `independent`
408
+ 全部明确为 true 才新增 `verified`。未知就留未知,提示或题解帮助不能补成独立。
409
+ 节点 verified 不是保持率或迁移能力证明;这些按课程现有 `exitChecks` 分别验收。
410
+ 短诊断和整场 VP 可用 `coach_assign` 绑定真实 competency,再用 `coach_log` 记录
411
+ 实际交付物与独立性;计划、时长和一次交付物通过都不证明该能力全面掌握。
412
+
405
413
  * 只有 **无提示 + 换皮题(同知识点、他没练过的题)+ 间隔 ≥1 天** 的表现才算数;
406
414
  带提示做对的,**不计入"已验证"**。
407
415
  依据:延迟 1–6 天后测的效应 g=0.82,当天测只有 0.56(Adesope 2017,272 个效应量)。
@@ -1335,7 +1335,7 @@ const SNAP = 5;
1335
1335
  // 块型短名。**必须和 host 的 SCHED_KINDS[k].name 逐字一致** ——
1336
1336
  // 不一致时页面会拿 id 兜底显示(不会崩),但那块看着就像坏了。
1337
1337
  // host 是真相源,这里只管显示。verify.mjs 有一条盯着这个一致性。
1338
- const KIND_NAME = { cycle: "题目循环", review: "复习一题", exam: "章节检测" };
1338
+ const KIND_NAME = { cycle: "题目循环", review: "复习一题", exam: "章节检测", diagnostic: "短诊断", vp: "完整 VP" };
1339
1339
 
1340
1340
  const minToHM = (m) => {
1341
1341
  const t = HOUR_FROM * 60 + m;
@@ -1447,7 +1447,7 @@ function renderSched() {
1447
1447
  const kind = String(b.kind ?? "");
1448
1448
  // 地图改过名、甚至删过节点,而计划是历史数据 ——
1449
1449
  // 查不到名字就退回 id,**不能因为查不到就不画**(那等于计划凭空消失)。
1450
- const nm = byId(b.node)?.name ?? String(b.node ?? "");
1450
+ const nm = b.competency ? `能力:${b.competency}` : byId(b.node)?.name ?? String(b.node ?? "");
1451
1451
  const size = mins >= 75 ? "s-full" : mins >= 40 ? "s-mid" : "s-mini";
1452
1452
  // 中等高度(40~74 分钟,循环块正好 60)挤不下两行,但**时间要留住** ——
1453
1453
  // busy 那边中档只放事由就够(他自己知道标了什么),
@@ -1457,7 +1457,7 @@ function renderSched() {
1457
1457
  ? `<span class="t">${esc(b.from)}–${esc(b.to)}</span>${esc(nm)}`
1458
1458
  : size === "s-mid" ? `${esc(b.from)} ${esc(nm)}` : "";
1459
1459
  const tip = `${b.from}–${b.to} ${KIND_NAME[kind] ?? kind} ${nm}` +
1460
- (kind === "cycle" ? " (40 自己做 → 10 解决遗留 → 10 重写)" : "");
1460
+ (kind === "cycle" && mins === 60 ? " (40 自己做 → 10 解决遗留 → 10 重写)" : "");
1461
1461
  return `<div class="pbar k-${esc(kind)} ${size}" title="${esc(tip)}" ` +
1462
1462
  `style="top:${pct(a)};height:${pct(mins)}">${body}</div>`;
1463
1463
  }).join("");
@@ -2,6 +2,11 @@ import test from 'node:test'
2
2
  import assert from 'node:assert/strict'
3
3
  import { readFileSync } from 'node:fs'
4
4
  import YAML from 'yaml'
5
+ import { mkdtempSync, rmSync } from 'node:fs'
6
+ import { tmpdir } from 'node:os'
7
+ import { join } from 'node:path'
8
+ import { fileURLToPath } from 'node:url'
9
+ import { createStrategyStore } from '../lib/strategy.js'
5
10
 
6
11
  const knowledge = new URL('../assets/knowledge/', import.meta.url)
7
12
  const read = name => YAML.parse(readFileSync(new URL(name, knowledge), 'utf8'))
@@ -24,6 +29,37 @@ function textList(value, label) {
24
29
  assert.ok(value.every(item => typeof item === 'string' && item.trim()), `${label}: text required`)
25
30
  }
26
31
 
32
+ test('contest evidence exposes actual reviewed samples, never a complete syllabus', () => {
33
+ const dir = mkdtempSync(join(tmpdir(), 'coach-samples-'))
34
+ try {
35
+ const store = createStrategyStore({ dataDir: dir, competenciesPath: fileURLToPath(new URL('COMPETENCIES.yaml', knowledge)) })
36
+ const profiles = store.target('profiles').availableProfiles
37
+ const regional = profiles.find(profile => profile.id === 'regional-diagnostic')
38
+ assert.equal(regional.coverage.sampledProblems, 19)
39
+ assert.equal(regional.coverage.reviewedSolutions, 19)
40
+ assert.deepEqual(regional.coverage.years, [2022, 2023])
41
+ assert.equal(regional.coverage.isSyllabus, false)
42
+ assert.deepEqual(regional.samples.filter(sample => sample.year === 2022).map(sample => sample.problem).sort(), [...'ABCDEFGHIJKLM'])
43
+ assert.equal(new Set(regional.samples.map(sample => `${sample.contest}:${sample.year}:${sample.problem}`)).size, 19)
44
+ assert.match(regional.title, /中国|亚洲/)
45
+ for (const sample of regional.samples) {
46
+ assert.ok(sample.model && sample.solutionReviewed)
47
+ assert.match(sample.sourceUrl, sample.year === 2022
48
+ ? /^https:\/\/sua\.ac\/wiki\/2022-icpc-nanjing\/tutorial-sketch-zh\.pdf#page=\d+$/
49
+ : /^https:\/\/universal-cup-website\.qoj\.ac\/files\/ucup\/tutorials\/tutorials-2-11\.pdf#page=\d+$/)
50
+ assert.ok(sample.competencyIds.every(id => competencies.some(card => card.id === id)))
51
+ }
52
+ const xian = profiles.find(profile => profile.id === 'xian-pending')
53
+ assert.equal(xian.coverage.sampledProblems, 0)
54
+ assert.equal(xian.coverage.reviewedSolutions, 0)
55
+ assert.equal(xian.coverage.isSyllabus, false)
56
+ assert.equal(store.target('set', { expectedRevision: 0, contest: '区域赛', date: '2026-10-17', result: '稳定', profileId: regional.id, weeklyHours: 25,
57
+ priorities: [{ id: 'graph-modeling', weight: 10 }] }).ok, true)
58
+ assert.equal(store.focus().profile.coverage.sampledProblems, 19)
59
+ assert.equal(profiles.find(profile => profile.id === 'nwerc-reference').coverage.reviewedSolutions, 13)
60
+ } finally { rmSync(dir, { recursive: true, force: true }) }
61
+ })
62
+
27
63
  test('node teaching cards use exact map nodes and contain sourced transfer boundaries', () => {
28
64
  const catalog = read('NODE_SCOPES.yaml')
29
65
  assert.equal(catalog.version, 1)
@@ -17,6 +17,24 @@ const practice = (extra = {}) => ({ date, problemId: '1234A', solved: false,
17
17
  independent: true, minutes: 40, ...extra })
18
18
  const progress = (nodeRecord, node = '区间 DP') => ({ nodes: { [node]: nodeRecord } })
19
19
  const rowsFor = (record) => collectEvidence(progress(record))
20
+
21
+ test('competency diagnostics and VP completions become facts without fake knowledge nodes', () => {
22
+ const schedule = { days: { [date]: { actual: [
23
+ { node: '', competency: 'contest-reading', kind: 'diagnostic', actualMin: 15, solved: true, independent: true, problemId: 'probe' },
24
+ { node: '', competency: 'contest-decision', kind: 'vp', actualMin: 300, solved: false, independent: true, problemId: 'probe' },
25
+ ] } } }
26
+ const rows = collectEvidence({}, schedule)
27
+ assert.equal(rows.length, 2)
28
+ assert.equal(rows[0].node, '')
29
+ assert.equal(rows[0].competency, 'contest-reading')
30
+ assert.equal(rows[0].minutes, 15)
31
+ assert.equal(rows[0].signal, 'baseline')
32
+ assert.equal(rows[1].competency, 'contest-decision')
33
+ assert.equal(rows[1].signal, 'gap')
34
+ assert.notEqual(rows[0].eventId, rows[1].eventId)
35
+ assert.deepEqual(collectEvidence({}, { days: { [date]: { actual: [{ ...schedule.days[date].actual[0], independent: 'yes' }] } } }), [])
36
+ assert.deepEqual(collectEvidence({ pendingActions: { 'competency:contest-reading': { kind: 'diagnostic', minutes: 15 } } }), [])
37
+ })
20
38
  const reverseKeys = (value) => Array.isArray(value) ? value.map(reverseKeys)
21
39
  : value && typeof value === 'object'
22
40
  ? Object.fromEntries(Object.entries(value).reverse().map(([key, item]) => [key, reverseKeys(item)]))
@@ -0,0 +1,161 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { mkdtempSync, writeFileSync, readFileSync, rmSync } from 'node:fs'
4
+ import { tmpdir } from 'node:os'
5
+ import { join } from 'node:path'
6
+ import { parse, stringify } from 'yaml'
7
+ import { collectEvidence } from '../lib/curriculum-evidence.js'
8
+ import { evaluateExitChecks } from '../lib/curriculum-planning.js'
9
+
10
+ async function fixture(run, nodeRecord = { status: 'studying' }) {
11
+ const dir = mkdtempSync(join(tmpdir(), 'coach-grading-'))
12
+ const previous = { data: process.env.COACH_DATA_DIR, db: process.env.COACH_DB }
13
+ try {
14
+ process.env.COACH_DATA_DIR = dir
15
+ process.env.COACH_DB = join(dir, 'unused.db')
16
+ writeFileSync(join(dir, 'MAP.yaml'), 'meta: {version: 1}\nnodes:\n - {id: A, name: 基础, depends: []}\n')
17
+ const path = join(dir, 'PROGRESS.yaml')
18
+ writeFileSync(path, stringify({ version: 1, cursor: 'A', nodes: { A: nodeRecord }, pending: {
19
+ node: 'A', totalMinutes: 90, passScore: 4, maxScore: 6,
20
+ problems: [
21
+ { problemId: 'P1', band: '易', weight: 1, cfRating: 1200, source: 'pool' },
22
+ { problemId: 'P2', band: '中', weight: 2, cfRating: 1600, source: 'pool' },
23
+ { problemId: 'P3', band: '难', weight: 3, cfRating: 1900, source: 'pool' },
24
+ ],
25
+ } }))
26
+ const { apply } = await import(`../index.js?grading=${encodeURIComponent(dir)}`)
27
+ const tools = new Map()
28
+ apply({ tools: { register: tool => tools.set(tool.name, tool) }, inject() {}, on() {} })
29
+ await run({ grade: tools.get('coach_grade'), call: (name, args) => tools.get(name).execute(args),
30
+ ledger: () => parse(readFileSync(path, 'utf8')), bytes: () => readFileSync(path, 'utf8') })
31
+ } finally {
32
+ for (const [key, value] of [['COACH_DATA_DIR', previous.data], ['COACH_DB', previous.db]]) {
33
+ if (value === undefined) delete process.env[key]
34
+ else process.env[key] = value
35
+ }
36
+ rmSync(dir, { recursive: true, force: true })
37
+ }
38
+ }
39
+
40
+ const results = (extra = {}) => ['P1', 'P2', 'P3'].map(problemId => ({ problemId, solved: true, minutes: 15, ...extra }))
41
+ const independentExit = ledger => evaluateExitChecks({ exitChecks: [{ node: 'A', minEvents: 1, independent: true }] },
42
+ collectEvidence(ledger), [], '2000-01-01')[0]
43
+
44
+ test('a passing paper without independence facts cannot create a mastery certificate', async () => {
45
+ await fixture(async ({ grade, ledger }) => {
46
+ const result = await grade.execute({ results: results() })
47
+ assert.equal(result.ok, true)
48
+ assert.equal(result.outcome, 'passed')
49
+ assert.equal(result.status, 'studying')
50
+ assert.equal(result.verified, false)
51
+ assert.equal(result.independence, 'unknown')
52
+ assert.equal(result.verifiedAt, 0)
53
+ const record = ledger().nodes.A
54
+ assert.equal(record.checks[0].independent, undefined)
55
+ assert.equal(record.checks[0].verifiedAt, 0)
56
+ assert.equal(independentExit(ledger()).passed, false, 'unknown independence cannot satisfy the course exit condition')
57
+ assert.equal(ledger().pending, undefined)
58
+ assert.match(grade.output.render({}, result)[0].text, /独立性.*未|未.*独立/)
59
+ })
60
+ })
61
+
62
+ test('a passing paper with a helped solution stays a paper pass without a certificate', async () => {
63
+ await fixture(async ({ grade, ledger }) => {
64
+ const reported = results({ independent: true })
65
+ reported[1].independent = false
66
+ const result = await grade.execute({ results: reported })
67
+ assert.equal(result.outcome, 'passed')
68
+ assert.equal(result.independence, 'assisted')
69
+ assert.equal(result.verified, false)
70
+ assert.equal(result.status, 'studying')
71
+ assert.equal(result.verifiedAt, 0)
72
+ assert.equal(ledger().nodes.A.checks[0].independent, false)
73
+ assert.equal(independentExit(ledger()).passed, false)
74
+ assert.match(grade.output.render({}, result)[0].text, /有帮助/)
75
+ })
76
+ })
77
+
78
+ test('explicitly independent solved problems certify a passing paper and carry the evidence into course checks', async () => {
79
+ await fixture(async ({ grade, ledger }) => {
80
+ const result = await grade.execute({ results: results({ independent: true }) })
81
+ assert.equal(result.verified, true)
82
+ assert.equal(result.status, 'verified')
83
+ assert.equal(result.independence, 'independent')
84
+ assert.equal(result.verifiedAt, 1900)
85
+ assert.equal(ledger().nodes.A.checks[0].independent, true)
86
+ assert.equal(independentExit(ledger()).passed, true)
87
+ assert.equal(ledger().nodes.A.passes, undefined, 'exam records must not invent practice records')
88
+ assert.match(grade.output.render({}, result)[0].text, /不代表隔日.*迁移/)
89
+ })
90
+ })
91
+
92
+ test('weighted passing still requires explicit independence for every solved problem', async () => {
93
+ await fixture(async ({ grade }) => {
94
+ const reported = results({ independent: true })
95
+ delete reported[2].independent
96
+ const result = await grade.execute({ results: reported })
97
+ assert.equal(result.outcome, 'passed')
98
+ assert.equal(result.verified, false)
99
+ assert.equal(result.independence, 'unknown')
100
+ })
101
+ await fixture(async ({ grade }) => {
102
+ const reported = results({ independent: true })
103
+ reported[1] = { problemId: 'P2', solved: false, minutes: 15 }
104
+ const result = await grade.execute({ results: reported })
105
+ assert.equal(result.score, 4)
106
+ assert.equal(result.verified, true, 'the existing weighted pass line remains valid; unsolved problems do not prove mastery')
107
+ })
108
+ })
109
+
110
+ test('explicit independence never certifies a failing or overtime paper', async () => {
111
+ for (const [reported, outcome] of [
112
+ [results({ independent: true, solved: false }), 'failed'],
113
+ [results({ independent: true, minutes: 40 }), 'overTime'],
114
+ ]) await fixture(async ({ grade, ledger }) => {
115
+ const result = await grade.execute({ results: reported })
116
+ assert.equal(result.outcome, outcome)
117
+ assert.equal(result.verified, false)
118
+ assert.equal(result.status, 'studying')
119
+ assert.equal(result.verifiedAt, 0)
120
+ assert.equal(independentExit(ledger()).passed, false)
121
+ })
122
+ })
123
+
124
+ test('unknown or assisted passing records preserve historical verified and learned states', async () => {
125
+ const previousCheck = { date: '2026-09-01', outcome: 'passed', score: 6, maxScore: 6, passScore: 4,
126
+ minutes: 45, limit: 90, verifiedAt: 1600, selfReported: 0, problems: [] }
127
+ for (const status of ['verified', 'learned']) await fixture(async ({ grade, ledger }) => {
128
+ const result = await grade.execute({ results: results() })
129
+ assert.equal(result.status, status)
130
+ assert.equal(result.verified, false, 'status may be historical; verified describes this attempt only')
131
+ assert.equal(result.verifiedAt, 0)
132
+ assert.equal(ledger().nodes.A.at, '2026-09-01')
133
+ assert.deepEqual(ledger().nodes.A.checks[0], previousCheck)
134
+ assert.equal(ledger().nodes.A.checks.length, 2)
135
+ }, { status, at: '2026-09-01', checks: [previousCheck] })
136
+ })
137
+
138
+ test('malformed grading fields and contradictory IDs are rejected without changing the pending paper', async () => {
139
+ await fixture(async ({ grade, bytes }) => {
140
+ const before = bytes()
141
+ for (const invalid of [
142
+ { independent: 'true' }, { independent: null }, { solved: 'true' },
143
+ { minutes: -1 }, { minutes: Infinity }, { minutes: Number.MAX_VALUE }, { minutes: '15' }, { problemId: '' }, { problemId: 'P2' },
144
+ ]) {
145
+ const reported = results()
146
+ Object.assign(reported[0], invalid)
147
+ let rejected
148
+ try { rejected = await grade.execute({ results: reported }) }
149
+ catch (error) {
150
+ assert.equal(error.code, 'INVALID_ARGS', 'the native SDK may reject malformed types before tool execution')
151
+ }
152
+ if (rejected) {
153
+ assert.equal(rejected.ok, false, JSON.stringify(invalid))
154
+ assert.equal(rejected.verified, false)
155
+ assert.equal(rejected.independence, 'unknown')
156
+ for (const key of grade.output.schema.required) assert.ok(key in rejected, `missing refusal field ${key}`)
157
+ }
158
+ assert.equal(bytes(), before)
159
+ }
160
+ })
161
+ })
@@ -135,6 +135,10 @@ for (const [tool, args, label] of CALLS) {
135
135
  delete ra.targetAligned; delete ra.competencies
136
136
  }
137
137
  // A legacy host cannot return the new empty continuation field.
138
+ if (tool === 'coach_schedule' && Array.isArray(ra?.blockKinds) && Array.isArray(rb?.blockKinds) && rb.blockKinds.length === 3) {
139
+ const added = ra.blockKinds.slice(3)
140
+ if (JSON.stringify(added) === JSON.stringify([{ kind: 'diagnostic', minutes: 15, name: '短诊断' }, { kind: 'vp', minutes: 300, name: '完整 VP' }])) ra.blockKinds = ra.blockKinds.slice(0, 3)
141
+ }
138
142
  if (tool === 'coach_status' && rb && !('teachingContext' in rb) && ra?.teachingContext === '') {
139
143
  delete ra.teachingContext
140
144
  }
@@ -165,6 +169,7 @@ for (const [tool, args, label] of CALLS.filter((c) => c[0] !== 'coach_diagnose')
165
169
  : legacyText)
166
170
  // Only the changed goal-led advice is excluded; schedule facts still match byte for byte.
167
171
  const scheduleAdvice = (text) => text
172
+ .replace('/diagnostic 15min(短诊断)/vp 300min(完整 VP)', '')
168
173
  .replace(/^⚠️ \*\*复习别把新知识挤没了。\*\*.*剩下的时间留给推进。$/m, '<COACH_ADVICE>')
169
174
  .replace(/^根据目标、剩余时间和实际表现,选择新课、复习、迁移或 VP。上面的间隔只供判断;不要为了填满课表持续开新知识点。$/m, '<COACH_ADVICE>')
170
175
  const same = tool === 'coach_schedule' ? scheduleAdvice(ra) === scheduleAdvice(rb) : ra === rb
package/dev/run-all.sh CHANGED
@@ -69,6 +69,8 @@ run "strategy" node --test dev/strategy-check.mjs
69
69
  run "catalog" node --test dev/catalog-check.mjs
70
70
  run "coach-contract" node --test dev/coaching-contract-check.mjs
71
71
  run "teaching" node --test dev/teaching-check.mjs
72
+ run "grading" node --test dev/grading-check.mjs
73
+ run "training" node --test dev/training-check.mjs
72
74
  run "VP-client" node dev/vp-client-check.mjs
73
75
  run "VP-sync" node --test dev/vp-sync-check.mjs
74
76
  run "migration" node --test dev/workspace-migration-check.mjs