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
|
@@ -0,0 +1,183 @@
|
|
|
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 fs from 'node:fs'
|
|
8
|
+
import { syncBuiltinESMExports } from 'node:module'
|
|
9
|
+
|
|
10
|
+
async function fixture(run, { cursor = 'A' } = {}) {
|
|
11
|
+
const dir = mkdtempSync(join(tmpdir(), 'coach-training-'))
|
|
12
|
+
const old = { data: process.env.COACH_DATA_DIR, db: process.env.COACH_DB }
|
|
13
|
+
writeFileSync(join(dir, 'MAP.yaml'), stringify({ meta: { version: 1 }, nodes: [{ id: 'A', name: '基础', depends: [] }] }))
|
|
14
|
+
writeFileSync(join(dir, 'PROGRESS.yaml'), stringify({ version: 1, cursor, nodes: { A: { status: 'studying' } } }))
|
|
15
|
+
process.env.COACH_DATA_DIR = dir
|
|
16
|
+
process.env.COACH_DB = join(dir, 'unused.db')
|
|
17
|
+
try {
|
|
18
|
+
const { apply } = await import(`../index.js?training-${Date.now()}-${Math.random()}`)
|
|
19
|
+
const tools = new Map()
|
|
20
|
+
apply({ tools: { register: tool => tools.set(tool.name, tool) }, inject() {}, on() {} })
|
|
21
|
+
const call = async (name, args) => {
|
|
22
|
+
const tool = tools.get(name)
|
|
23
|
+
const value = await tool.execute(args)
|
|
24
|
+
shape(tool.output.schema, value)
|
|
25
|
+
return value
|
|
26
|
+
}
|
|
27
|
+
await run({ call, tools, dir, read: file => parse(readFileSync(join(dir, file), 'utf8')), write: (file, doc) => writeFileSync(join(dir, file), stringify(doc)) })
|
|
28
|
+
} finally {
|
|
29
|
+
for (const [key, value] of [['COACH_DATA_DIR', old.data], ['COACH_DB', old.db]]) {
|
|
30
|
+
if (value === undefined) delete process.env[key]
|
|
31
|
+
else process.env[key] = value
|
|
32
|
+
}
|
|
33
|
+
rmSync(dir, { recursive: true, force: true })
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
test('a failed progress write cannot report the pending action as cleared', async () => fixture(async ({ call, read }) => {
|
|
38
|
+
assert.equal((await call('coach_assign', { competency: 'contest-reading', kind: 'diagnostic', minutes: 15, deliverable: '说明三题的模型条件' })).accepted, true)
|
|
39
|
+
const originalWrite = fs.writeFileSync
|
|
40
|
+
let result
|
|
41
|
+
try {
|
|
42
|
+
fs.writeFileSync = (path, ...args) => {
|
|
43
|
+
if (String(path).includes('PROGRESS.yaml')) throw new Error('simulated disk write failure')
|
|
44
|
+
return originalWrite(path, ...args)
|
|
45
|
+
}
|
|
46
|
+
syncBuiltinESMExports()
|
|
47
|
+
result = await call('coach_log', { competency: 'contest-reading', actualMin: 15, solved: true, independent: true })
|
|
48
|
+
} finally { fs.writeFileSync = originalWrite; syncBuiltinESMExports() }
|
|
49
|
+
assert.equal(result.recorded, false)
|
|
50
|
+
assert.equal(result.actionCleared, false)
|
|
51
|
+
assert.ok(read('PROGRESS.yaml').pendingActions['competency:contest-reading'])
|
|
52
|
+
assert.equal(read('SCHEDULE.yaml').days[result.date].actual.length, 1, 'the successfully saved actual remains visible')
|
|
53
|
+
}))
|
|
54
|
+
|
|
55
|
+
function shape(schema, value) {
|
|
56
|
+
if (schema.type === 'object') {
|
|
57
|
+
assert.ok(value && typeof value === 'object' && !Array.isArray(value))
|
|
58
|
+
if (schema.additionalProperties === false) for (const key of Object.keys(value)) assert.ok(key in schema.properties, `unexpected ${key}`)
|
|
59
|
+
for (const [key, field] of Object.entries(schema.properties ?? {})) {
|
|
60
|
+
if (field.required === true || schema.required?.includes(key)) assert.ok(key in value, `missing ${key}`)
|
|
61
|
+
if (key in value) shape(field, value[key])
|
|
62
|
+
}
|
|
63
|
+
} else if (schema.type === 'array') {
|
|
64
|
+
assert.ok(Array.isArray(value)); value.forEach(item => shape(schema.items, item))
|
|
65
|
+
} else if (schema.type === 'integer') assert.ok(Number.isInteger(value))
|
|
66
|
+
else assert.equal(typeof value, schema.type)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
test('15-minute node diagnostic fits one action without pretending a lecture or mastery happened', async () => fixture(async ({ call, read }) => {
|
|
70
|
+
const assigned = await call('coach_assign', { node: 'A', kind: 'diagnostic', minutes: 15, deliverable: '说出状态定义并解释为什么保留这些信息' })
|
|
71
|
+
assert.equal(assigned.accepted, true, assigned.reject)
|
|
72
|
+
assert.equal(assigned.totalMinutes, 15)
|
|
73
|
+
assert.deepEqual(assigned.blocks.map(block => block.minutes), [15])
|
|
74
|
+
const progress = read('PROGRESS.yaml')
|
|
75
|
+
assert.equal(progress.nodes.A.status, 'studying')
|
|
76
|
+
assert.equal(progress.nodes.A.taught, undefined)
|
|
77
|
+
assert.equal(progress.pendingActions.A.kind, 'diagnostic')
|
|
78
|
+
}))
|
|
79
|
+
|
|
80
|
+
test('a full 300-minute VP occupies one schedule block within the physical day', async () => fixture(async ({ call }) => {
|
|
81
|
+
const now = new Date()
|
|
82
|
+
const date = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
|
|
83
|
+
const planned = await call('coach_plan', { date, blocks: [{ from: '10:00', node: 'A', kind: 'vp', minutes: 300 }] })
|
|
84
|
+
assert.equal(planned.ok, true, planned.reject)
|
|
85
|
+
assert.equal(planned.planned.length, 1)
|
|
86
|
+
assert.equal(planned.planned[0].to, '15:00')
|
|
87
|
+
assert.equal(planned.planMin, 300)
|
|
88
|
+
assert.equal((await call('coach_plan', { date, replace: true, blocks: [{ from: '20:00', node: 'A', kind: 'vp', minutes: 300 }] })).ok, false)
|
|
89
|
+
const schedule = await call('coach_schedule', { from: date, days: 1 })
|
|
90
|
+
assert.equal(schedule.days[0].planned[0].minutes, 300)
|
|
91
|
+
}))
|
|
92
|
+
|
|
93
|
+
test('competency diagnostic works without a cursor and completes through coach_log without a fake map node', async () => fixture(async ({ call, read }) => {
|
|
94
|
+
const assigned = await call('coach_assign', { competency: 'contest-reading', kind: 'diagnostic', minutes: 15, deliverable: '扫描题面,提交题目筛选顺序和放弃理由' })
|
|
95
|
+
assert.equal(assigned.accepted, true, assigned.reject)
|
|
96
|
+
assert.equal(assigned.node, '')
|
|
97
|
+
assert.equal(assigned.competency, 'contest-reading')
|
|
98
|
+
assert.equal(assigned.totalMinutes, 15)
|
|
99
|
+
assert.equal(read('PROGRESS.yaml').pendingActions['competency:contest-reading'].competency, 'contest-reading')
|
|
100
|
+
const status = await call('coach_status', {})
|
|
101
|
+
const pending = status.pendingActions[0]
|
|
102
|
+
assert.equal(pending.node, '')
|
|
103
|
+
assert.equal(pending.competency, 'contest-reading')
|
|
104
|
+
assert.equal(pending.kind, 'diagnostic')
|
|
105
|
+
assert.equal(pending.nodeName, '比赛读题与筛选')
|
|
106
|
+
const logged = await call('coach_log', { competency: 'contest-reading', actualMin: 15, solved: false, independent: false, note: '遗漏了最后一道题的输入限制' })
|
|
107
|
+
assert.equal(logged.ok, true, logged.reject)
|
|
108
|
+
assert.equal(logged.actionCleared, true)
|
|
109
|
+
assert.equal(logged.recorded, true)
|
|
110
|
+
assert.deepEqual(Object.keys(read('PROGRESS.yaml').nodes), ['A'])
|
|
111
|
+
assert.equal(read('SCHEDULE.yaml').days[logged.date].actual[0].competency, 'contest-reading')
|
|
112
|
+
assert.equal(read('PROGRESS.yaml').pendingActions, undefined)
|
|
113
|
+
const assessed = await call('coach_curriculum', { action: 'assess' })
|
|
114
|
+
assert.equal(assessed.evidence.find(entry => entry.competency === 'contest-reading')?.signal, 'gap')
|
|
115
|
+
assert.equal((await call('coach_assign', { competency: 'invented', kind: 'diagnostic', minutes: 15, deliverable: '提交观察' })).accepted, false)
|
|
116
|
+
assert.equal((await call('coach_log', { competency: 'invented', actualMin: 15, solved: true, independent: true })).ok, false)
|
|
117
|
+
}, { cursor: '' }))
|
|
118
|
+
|
|
119
|
+
test('competency actions bind the diagnostic phase, reject stale or other-unit work, and yield usable evidence', async () => fixture(async ({ call, read }) => {
|
|
120
|
+
const phase = { id: 'read-probe', title: '读题诊断', kind: 'diagnostic', finding: 'unknown', nodes: [], competencies: ['contest-reading'],
|
|
121
|
+
outcome: '确认读题筛选遗漏发生在哪个环节', exitCriteria: '记录一次新的读题诊断观察结果',
|
|
122
|
+
basis: '目前没有读题过程的独立观察记录', goalContribution: '识别区域赛首轮漏读与错误筛选问题', priorityReason: '先观察当前筛选过程再决定训练重点', estimatedHours: 1,
|
|
123
|
+
evidenceRefs: [], uncertainty: '目前缺少读题过程记录无法判断原因', diagnosticMethod: '限时扫描完整题面并解释筛选和放弃理由' }
|
|
124
|
+
const created = await call('coach_curriculum', { action: 'create', expectedRevision: 0, goal: '区域赛读题筛选更稳定', reason: '缺少当前读题事实,先安排一次小诊断建立依据', phases: [phase] })
|
|
125
|
+
assert.equal(created.ok, true, created.reject)
|
|
126
|
+
const action = { competency: 'contest-reading', kind: 'diagnostic', minutes: 15, deliverable: '提交筛选顺序和放弃理由', why: '观察是否遗漏输入限制和可做题', curriculumRevision: created.revision }
|
|
127
|
+
assert.equal((await call('coach_assign', { ...action, curriculumRevision: created.revision - 1 })).accepted, false)
|
|
128
|
+
assert.equal((await call('coach_assign', { ...action, competency: 'contest-decision' })).accepted, false)
|
|
129
|
+
const assigned = await call('coach_assign', action)
|
|
130
|
+
assert.equal(assigned.accepted, true, assigned.reject)
|
|
131
|
+
assert.equal(read('PROGRESS.yaml').pendingActions['competency:contest-reading'].curriculum.phaseId, 'read-probe')
|
|
132
|
+
assert.equal((await call('coach_assign', { node: 'A', deliverable: '提交代码', purpose: 'remediation', routeReason: '补充当前读题阶段的实现基础再回到读题诊断', why: '补齐基础', curriculumRevision: created.revision })).accepted, false)
|
|
133
|
+
const logged = await call('coach_log', { competency: 'contest-reading', problemId: 'probe-1', actualMin: 15, solved: false, independent: false })
|
|
134
|
+
assert.equal(logged.actionCleared, true)
|
|
135
|
+
const state = await call('coach_curriculum', { action: 'read' })
|
|
136
|
+
assert.equal(state.pendingNodes.length, 0)
|
|
137
|
+
assert.equal(state.missingFreshEvidence.length, 0)
|
|
138
|
+
assert.equal(state.evidence[0].competency, 'contest-reading')
|
|
139
|
+
}, { cursor: '' }))
|
|
140
|
+
|
|
141
|
+
test('competency VP can be scheduled and withdrawn without fake nodes', async () => fixture(async ({ call }) => {
|
|
142
|
+
const now = new Date()
|
|
143
|
+
const date = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
|
|
144
|
+
const planned = await call('coach_plan', { date, blocks: [{ from: '10:00', competency: 'contest-reading', kind: 'vp', minutes: 300 }] })
|
|
145
|
+
assert.equal(planned.ok, true, planned.reject)
|
|
146
|
+
assert.equal(planned.planned[0].node, '')
|
|
147
|
+
assert.equal(planned.planned[0].competency, 'contest-reading')
|
|
148
|
+
const schedule = await call('coach_schedule', { from: date, days: 1 })
|
|
149
|
+
assert.equal(schedule.days[0].planned[0].nodeName, '比赛读题与筛选')
|
|
150
|
+
assert.equal(schedule.days[0].planned[0].competency, 'contest-reading')
|
|
151
|
+
assert.equal((await call('coach_assign', { competency: 'contest-reading', kind: 'vp', minutes: 300, deliverable: '完成比赛并提交首轮筛选过程' })).accepted, true)
|
|
152
|
+
const withdrawn = await call('coach_unassign', { competency: 'contest-reading', reason: '队友临时有事,本次合练取消' })
|
|
153
|
+
assert.equal(withdrawn.ok, true, withdrawn.reject)
|
|
154
|
+
assert.equal(withdrawn.node, '')
|
|
155
|
+
assert.equal(withdrawn.competency, 'contest-reading')
|
|
156
|
+
const logged = await call('coach_log', { competency: 'contest-reading', problemId: 'VP-test', actualMin: 300, solved: true, independent: true })
|
|
157
|
+
assert.equal(logged.plannedMin, 300)
|
|
158
|
+
assert.equal((await call('coach_schedule', { from: date, days: 1 })).days[0].actual[0].competency, 'contest-reading')
|
|
159
|
+
assert.equal((await call('coach_unplan', { date, all: true })).removed[0].competency, 'contest-reading')
|
|
160
|
+
}, { cursor: '' }))
|
|
161
|
+
|
|
162
|
+
test('explicit durations preserve defaults and reject invalid lengths, busy collisions and overlap atomically', async () => fixture(async ({ call, read, write }) => {
|
|
163
|
+
const now = new Date()
|
|
164
|
+
const date = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
|
|
165
|
+
const defaults = await call('coach_plan', { date, blocks: [{ from: '08:00', node: 'A', kind: 'cycle' }, { from: '09:00', node: 'A', kind: 'review' }, { from: '10:00', node: 'A', kind: 'exam' }] })
|
|
166
|
+
assert.equal(defaults.ok, true, defaults.reject)
|
|
167
|
+
assert.deepEqual(defaults.planned.map(block => block.minutes), [60, 25, 90])
|
|
168
|
+
const previous = read('SCHEDULE.yaml')
|
|
169
|
+
for (const minutes of [0, -1, 961]) {
|
|
170
|
+
assert.equal((await call('coach_plan', { date, replace: true, blocks: [{ from: '08:00', node: 'A', kind: 'cycle', minutes }] })).ok, false)
|
|
171
|
+
assert.equal((await call('coach_assign', { node: 'A', kind: 'diagnostic', minutes, deliverable: '提交观察' })).accepted, false)
|
|
172
|
+
}
|
|
173
|
+
for (const minutes of [NaN, Infinity, 1.5]) await assert.rejects(call('coach_plan', { date, replace: true, blocks: [{ from: '08:00', node: 'A', kind: 'cycle', minutes }] }), /invalid arguments/)
|
|
174
|
+
assert.deepEqual(read('SCHEDULE.yaml'), previous)
|
|
175
|
+
assert.equal((await call('coach_assign', { node: 'A', competency: 'contest-reading', deliverable: '提交观察' })).accepted, false)
|
|
176
|
+
assert.equal((await call('coach_assign', { node: 'A', minutes: 15, deliverable: '完成一个很小的迁移题' })).totalMinutes, 35)
|
|
177
|
+
const busy = { version: 1, days: { [date]: { busy: [{ from: '12:00', to: '13:00', label: '上课' }] } } }
|
|
178
|
+
write('SCHEDULE.yaml', busy)
|
|
179
|
+
assert.equal((await call('coach_plan', { date, blocks: [{ from: '10:00', node: 'A', kind: 'vp', minutes: 300 }] })).ok, false)
|
|
180
|
+
assert.deepEqual(read('SCHEDULE.yaml'), busy)
|
|
181
|
+
assert.equal((await call('coach_plan', { date, blocks: [{ from: '08:00', node: 'A', kind: 'diagnostic', minutes: 15 }, { from: '08:10', node: 'A', kind: 'diagnostic', minutes: 15 }] })).ok, false)
|
|
182
|
+
assert.deepEqual(read('SCHEDULE.yaml'), busy)
|
|
183
|
+
}))
|
package/dev/vp-sync-check.mjs
CHANGED
|
@@ -9,7 +9,7 @@ function shape(schema, value) {
|
|
|
9
9
|
assert.ok(value && typeof value === 'object' && !Array.isArray(value))
|
|
10
10
|
if (schema.additionalProperties === false) for (const key of Object.keys(value)) assert.ok(key in schema.properties, `unexpected field ${key}`)
|
|
11
11
|
for (const [key, field] of Object.entries(schema.properties ?? {})) {
|
|
12
|
-
if (field.required) assert.ok(key in value, `missing field ${key}`)
|
|
12
|
+
if (field.required === true || (Array.isArray(schema.required) && schema.required.includes(key))) assert.ok(key in value, `missing field ${key}`)
|
|
13
13
|
if (key in value) shape(field, value[key])
|
|
14
14
|
}
|
|
15
15
|
} else if (schema.type === 'array') {
|
package/dev.sh
CHANGED
|
@@ -35,7 +35,7 @@ done
|
|
|
35
35
|
# 从副本目录跑 `bash dev.sh` 走的是另一条路(`dsh plugin remove/add`,而那条
|
|
36
36
|
# 路不可靠),和这份的行为不一样。换句话说,**同一个命令,从哪个目录按回车
|
|
37
37
|
# 决定跑哪份脚本** —— 两份 dev.sh 就是两份真相,正是这份文件在防的那种坑。
|
|
38
|
-
SYNC_FILES='index.js package.json verify.mjs verify-shape.mjs progress-cli.mjs build-node-meta.mjs cordis.patch.yml README.md lib/client.js lib/vp-client.js lib/workspace-migration.js lib/setup.js lib/curriculum.js lib/curriculum-evidence.js lib/curriculum-planning.js lib/strategy.js assets/knowledge/COMPETENCIES.yaml assets/knowledge/NODE_SCOPES.yaml assets/knowledge/CONTEST_PROFILES.yaml assets/knowledge/vp.html assets/rules/coach-rules.md dev/curriculum-check.mjs dev/curriculum-evidence-check.mjs dev/strategy-check.mjs dev/workspace-migration-check.mjs dev/vp-client-check.mjs dev/vp-sync-check.mjs dev/catalog-check.mjs dev/coaching-contract-check.mjs docs/长期课程机制.md docs/比赛与教学依据.md docs/设计方针.md dev.sh'
|
|
38
|
+
SYNC_FILES='index.js package.json verify.mjs verify-shape.mjs progress-cli.mjs build-node-meta.mjs cordis.patch.yml README.md lib/client.js lib/vp-client.js lib/workspace-migration.js lib/setup.js lib/curriculum.js lib/curriculum-evidence.js lib/curriculum-planning.js lib/strategy.js lib/teaching.js assets/knowledge/COMPETENCIES.yaml assets/knowledge/NODE_SCOPES.yaml assets/knowledge/CONTEST_PROFILES.yaml assets/knowledge/vp.html assets/knowledge/skilltree.html assets/rules/coach-rules.md dev/curriculum-check.mjs dev/curriculum-evidence-check.mjs dev/strategy-check.mjs dev/workspace-migration-check.mjs dev/vp-client-check.mjs dev/vp-sync-check.mjs dev/catalog-check.mjs dev/coaching-contract-check.mjs dev/teaching-check.mjs dev/grading-check.mjs dev/training-check.mjs coach/skilltree_template.html dev/run-all.sh docs/长期课程机制.md docs/比赛与教学依据.md docs/设计方针.md dev.sh'
|
|
39
39
|
STALE=''
|
|
40
40
|
for f in $SYNC_FILES; do
|
|
41
41
|
for d in "${DSTS[@]}"; do
|
|
@@ -120,7 +120,7 @@ EVIDENCE_RC=${EVIDENCE_RC:-0}
|
|
|
120
120
|
|
|
121
121
|
echo
|
|
122
122
|
echo '── 策略、教学目录、真实工具与账号数据回归 ──'
|
|
123
|
-
node --test "$SRC_BASH/dev/strategy-check.mjs" "$SRC_BASH/dev/catalog-check.mjs" "$SRC_BASH/dev/coaching-contract-check.mjs" "$SRC_BASH/dev/workspace-migration-check.mjs" "$SRC_BASH/dev/vp-client-check.mjs" "$SRC_BASH/dev/vp-sync-check.mjs" || CONTRACT_RC=$?
|
|
123
|
+
node --test "$SRC_BASH/dev/strategy-check.mjs" "$SRC_BASH/dev/catalog-check.mjs" "$SRC_BASH/dev/coaching-contract-check.mjs" "$SRC_BASH/dev/teaching-check.mjs" "$SRC_BASH/dev/grading-check.mjs" "$SRC_BASH/dev/training-check.mjs" "$SRC_BASH/dev/workspace-migration-check.mjs" "$SRC_BASH/dev/vp-client-check.mjs" "$SRC_BASH/dev/vp-sync-check.mjs" || CONTRACT_RC=$?
|
|
124
124
|
CONTRACT_RC=${CONTRACT_RC:-0}
|
|
125
125
|
|
|
126
126
|
LEDGER_AFTER=$(ledger_fp)
|
package/docs//346/257/224/350/265/233/344/270/216/346/225/231/345/255/246/344/276/235/346/215/256.md
CHANGED
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
|
|
13
13
|
`NODE_SCOPES` 的 `id` 必须与 MAP 节点完全一致。`recognition` 用于读题识别;`proofQuestions` 用于确认学生能解释;`notFor` 用于主动暴露边界;`transferProblems` 是练习任务描述,不是假造的题目编号;`exitEvidence` 是教学验收建议。来源证明技术条件,练习设计由插件负责。
|
|
14
14
|
|
|
15
|
+
## 2.6:中国亚洲区域赛的逐题依据
|
|
16
|
+
|
|
17
|
+
默认通用配置 `regional-diagnostic` 面向中国亚洲区域赛:南京 2022 全卷十三题和南京 2023 六题,共十九题,逐题列年份、模型、能力、题解核验状态和页码。来源是 [SUA 2022 命题组题解](https://sua.ac/wiki/2022-icpc-nanjing/tutorial-sketch-zh.pdf)与 [QOJ 镜像的 SUA 2023 题解](https://universal-cup-website.qoj.ac/files/ucup/tutorials/tutorials-2-11.pdf)。NWERC 放在独立的 `nwerc-reference` 配置,供扩展讨论。
|
|
18
|
+
|
|
19
|
+
`coverage` 自动汇总真实样本数量、题解核验数量及年份;没有样本就返回零。西安 [QOJ 历年目录](https://qoj.ac/category/87)已核对,原始题解附件未获取,因此维持待采样,不从题名补造算法。十九题不是中国区域赛专题频率统计;已核对模型也不保证唯一解法、完整考纲或本届命题。教练结合个人和团队的新证据决定重点,不按 rating 解锁下一章。
|
|
20
|
+
|
|
15
21
|
三个 profile 当前均是 `provisional`:`regional-diagnostic`、`wf-extension`、`xian-pending`。这个状态表示可用于带限制的诊断,不能输出“已掌握本比赛考纲”。`reviewed` 也只能表示记录的样本已人工核验,不能去掉样本限制。
|
|
16
22
|
|
|
17
23
|
## 本轮采样范围
|
|
@@ -31,6 +37,37 @@ WF 扩展诊断只读了 [2024 World Finals 官方题面](https://icpc.global/wo
|
|
|
31
37
|
|
|
32
38
|
用户临时问新知识点时可以回答。回答不自动成为主线任务,也不自动改变游标或阶段。
|
|
33
39
|
|
|
40
|
+
## 2.5:给教练续课依据,不代替教练判断
|
|
41
|
+
|
|
42
|
+
本版只增加 `coach_note` 的读写入口,笔记放进现有 `PROGRESS.nodes.<id>.lessonNotes`。
|
|
43
|
+
记录是“当前教学判断 + 下一次接续点”;可选学生原话通过 dsh 原生 `sessionQuery`
|
|
44
|
+
核对当前会话的真实用户消息,来源由程序生成。插件消息或压缩摘要不能成为学生原话。
|
|
45
|
+
核对存在不证明理解,笔记也不改变游标、掌握状态、待办或检测结果。
|
|
46
|
+
|
|
47
|
+
`coach_status` 和原有会话首步注入只提供短笔记,详细材料按需读取。
|
|
48
|
+
默认读取优先接续最近有笔记的未完动作,再看游标;无游标时看最近笔记。
|
|
49
|
+
写入默认落在当前游标,指定节点时使用该节点,不改变游标本身。
|
|
50
|
+
教学进展或中断值得保存时才写,不要求每轮留笔记,不规定固定工具顺序。
|
|
51
|
+
能力排名作为参考,前三之外或尚未映射到能力的节点仍允许教练安排;
|
|
52
|
+
课程、前置和写入证据的检查保留。`teachMinutes` 是计划,不自动生成已教学日期,
|
|
53
|
+
实际开始学习仍由教练显式记录;旧记录保留,不重新推断发生过什么。
|
|
54
|
+
|
|
55
|
+
这个取舍是结合本插件所做的设计判断:
|
|
56
|
+
|
|
57
|
+
* [Anthropic:Building effective agents](https://www.anthropic.com/engineering/building-effective-agents)
|
|
58
|
+
与 [OpenAI:A practical guide to building agents](https://openai.com/business/guides-and-resources/a-practical-guide-to-building-ai-agents/)
|
|
59
|
+
都建议从简单、可验证的方案开始。因此沿用小鲸这个 agent,未增加多教练编排或框架。
|
|
60
|
+
* [Anthropic:Effective context engineering](https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents)
|
|
61
|
+
支持保留必要摘要、按需获取详情;本版沿用原进度文件与宿主会话查询,不复制聊天数据库。
|
|
62
|
+
* [Anthropic:Writing tools for agents](https://www.anthropic.com/engineering/writing-tools-for-agents)
|
|
63
|
+
强调清楚、职责明确的工具与真实任务评测;本版把续课笔记与已有检测、日程工具分开,
|
|
64
|
+
不用固定调用次序判断教练是否正确。
|
|
65
|
+
* [Anthropic:Demystifying evals for AI agents](https://www.anthropic.com/engineering/demystifying-evals-for-ai-agents)
|
|
66
|
+
区分任务结果与交互质量;工具回归只能验证记录和来源,不能据此声称学生学习效果已证实。
|
|
67
|
+
|
|
68
|
+
验证覆盖跨模块实例读取、原话来源、拒绝伪引用和并行更新时保留已有进度。
|
|
69
|
+
没有做真实学生的长期对照试验,也没有把笔记数量当教学质量分数。
|
|
70
|
+
|
|
34
71
|
## 四个明确的教学边界
|
|
35
72
|
|
|
36
73
|
最短路卡提供有向边 `s→a:2`、`s→b:5`、`b→a:-4`:确定后不再更新的 Dijkstra 会过早确定 a;真实最短距离是 1。算法仍偶尔得到对的结果不能证明它适用于负权图。[技术依据](https://oi-wiki.org/graph/shortest-path/)
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
## 一、一句话命题
|
|
9
9
|
|
|
10
|
-
>
|
|
10
|
+
> **教练把比赛目标、当下理解和下一步教学连起来。**
|
|
11
11
|
>
|
|
12
12
|
> 它站在局外、手里有地图、且不投入自尊。
|
|
13
13
|
> 在局里的人看不见河的全貌 —— 这跟聪明程度无关,是位置问题。
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
排优先级需要「什么重要、什么先什么后、哪里是坑」——
|
|
17
17
|
而这些恰好是初学者没有的知识。**这是循环依赖,必须由外部打破。**
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
资料和题目已经很多。教练要选择重点、讲清推理、核对理解,
|
|
20
|
+
根据真实回应调整讲法,并在下次接着教;排顺序只是其中一项。
|
|
20
21
|
|
|
21
22
|
---
|
|
22
23
|
|
|
@@ -91,7 +92,7 @@
|
|
|
91
92
|
不写 = 未学
|
|
92
93
|
studying = 在学 开了头、还没达标。跨天是常态
|
|
93
94
|
learned = 学过 自评学过,没做检测
|
|
94
|
-
verified = 已验证
|
|
95
|
+
verified = 已验证 新检测须明确独立且过卷;旧证书不补造独立事实
|
|
95
96
|
```
|
|
96
97
|
|
|
97
98
|
把 learned 和 verified 合成一个「会/不会」,就等于退回原则 2 要治的那个病。
|
|
@@ -116,9 +117,9 @@ verified 是有日期和条件的检测结论,不是永久标签。阶段验
|
|
|
116
117
|
「今天说了明天又忘」不是记忆问题,是**强制力问题**。治法见第五节。
|
|
117
118
|
|
|
118
119
|
**但这条有边界,得说清楚**:结构只能强制**机械可判**的那一半。
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
新知识点的工具可以要求留出讲解预算(`teachMinutes`),但预算只证明有计划,
|
|
121
|
+
不能核实授课完成或学生理解。续课笔记保存教学线索,原话核对也只证明消息存在。
|
|
122
|
+
讲法、重点与复习取舍交给教练;程序约束证据来源、账本写入和任务执行边界。
|
|
122
123
|
|
|
123
124
|
### 原则 6:命名是终点,不是入口(讲题规范)
|
|
124
125
|
|