@longzai-intelligence-knowledge-graph/diagnosis 0.0.0-stage → 0.0.2
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 +10 -2
- package/dist/index.d.ts +389 -0
- package/dist/index.js +1 -0
- package/package.json +38 -4
package/README.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @longzai-intelligence-knowledge-graph/diagnosis
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Longzai Intelligence Knowledge Graph 的通用诊断引擎包,面向先修图提供自适应出题、判题与学习路径汇总的纯函数实现。
|
|
4
|
+
|
|
5
|
+
## 简介
|
|
6
|
+
|
|
7
|
+
引擎以先修有向无环图、知识点与题库为输入,按目标知识点展开先修子图并逐点出题。每个知识点最多两题,两题未过才沿先修边回溯且硬先修在前,一错一对记不稳不回溯,答对即推断全部祖先已掌握。出题由种子确定性驱动,同种子同作答必出同题。会话对象全程可 JSON 序列化,撤销、更正与刷新恢复一律经作答记录重放实现。
|
|
8
|
+
|
|
9
|
+
## 用法
|
|
10
|
+
|
|
11
|
+
从 `@longzai-intelligence-knowledge-graph/diagnosis` 导入 `createSession`、`nextQuestion`、`applyAnswer`、`replay` 与 `summarize`,配合 `DiagnosisGraph` 数据束驱动一次完整诊断,判题器结论可被调用方 `selfJudged` 裁定覆盖。包内不持有任何运行时状态,全部函数均为纯函数,邻接构建、稳定拓扑排序、祖先闭包与判题器等底层算法一并导出供复用。
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* 诊断引擎类型定义
|
|
4
|
+
*
|
|
5
|
+
* 会话对象全程可 JSON 序列化,刷新恢复、撤销与更正均由作答记录重放实现
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* 知识点诊断状态
|
|
9
|
+
*
|
|
10
|
+
* @remarks
|
|
11
|
+
* `mastered` 即直接答对判定,`mastered-inferred` 即由后代答对推断,
|
|
12
|
+
* `shaky` 即一错一对,`weak` 即两题未过或单题即判,`pending` 即尚未判定
|
|
13
|
+
*/
|
|
14
|
+
type TopicStatus = 'mastered' | 'mastered-inferred' | 'shaky' | 'weak' | 'pending';
|
|
15
|
+
/**
|
|
16
|
+
* 作答结果分类
|
|
17
|
+
*
|
|
18
|
+
* @remarks
|
|
19
|
+
* `manual` 即判题器不下结论交学习者自评
|
|
20
|
+
*/
|
|
21
|
+
type JudgeResult = 'correct' | 'wrong' | 'manual';
|
|
22
|
+
/**
|
|
23
|
+
* 引擎侧题目形态即数据束题目的最小消费面
|
|
24
|
+
*/
|
|
25
|
+
type EngineQuestion = {
|
|
26
|
+
id: string;
|
|
27
|
+
topicId: string;
|
|
28
|
+
difficulty: 'basic' | 'standard' | 'advanced';
|
|
29
|
+
type: 'choice' | 'short' | 'self';
|
|
30
|
+
prompt: string;
|
|
31
|
+
choices?: {
|
|
32
|
+
key: string;
|
|
33
|
+
text: string;
|
|
34
|
+
}[];
|
|
35
|
+
answer: string;
|
|
36
|
+
accept?: string[];
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* 引擎侧知识点形态
|
|
40
|
+
*/
|
|
41
|
+
type EngineTopic = {
|
|
42
|
+
id: string;
|
|
43
|
+
name: string;
|
|
44
|
+
stage: number;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* 引擎侧先修边形态
|
|
48
|
+
*/
|
|
49
|
+
type EngineDependency = {
|
|
50
|
+
topicId: string;
|
|
51
|
+
prerequisiteId: string;
|
|
52
|
+
strength: 'hard' | 'soft';
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* 诊断图即引擎消费的先修有向无环图
|
|
56
|
+
*/
|
|
57
|
+
type DiagnosisGraph = {
|
|
58
|
+
topics: EngineTopic[];
|
|
59
|
+
dependencies: EngineDependency[];
|
|
60
|
+
questions: EngineQuestion[];
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* 单次作答记录
|
|
64
|
+
*/
|
|
65
|
+
type AnswerRecord = {
|
|
66
|
+
questionId: string;
|
|
67
|
+
topicId: string;
|
|
68
|
+
nth: number;
|
|
69
|
+
userAnswer: string;
|
|
70
|
+
result: Exclude<JudgeResult, 'manual'> | 'manual-correct' | 'manual-wrong';
|
|
71
|
+
judgedBy: 'auto' | 'self';
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* 诊断会话状态
|
|
75
|
+
*
|
|
76
|
+
* @remarks
|
|
77
|
+
* 全部字段可 JSON 序列化,`status` 以记录形态存各知识点状态
|
|
78
|
+
*/
|
|
79
|
+
type SessionState = {
|
|
80
|
+
seed: string;
|
|
81
|
+
targets: string[];
|
|
82
|
+
limit: number;
|
|
83
|
+
subgraph: string[];
|
|
84
|
+
status: Record<string, TopicStatus>;
|
|
85
|
+
stack: string[];
|
|
86
|
+
askedQuestionIds: string[];
|
|
87
|
+
answers: AnswerRecord[];
|
|
88
|
+
finished: boolean;
|
|
89
|
+
limitReached: boolean;
|
|
90
|
+
current: PendingQuestion | null;
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* 当前待答题目
|
|
94
|
+
*/
|
|
95
|
+
type PendingQuestion = {
|
|
96
|
+
question: EngineQuestion;
|
|
97
|
+
topicId: string;
|
|
98
|
+
nth: number;
|
|
99
|
+
reason: string;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* 薄弱根因条目
|
|
103
|
+
*/
|
|
104
|
+
type WeakRoot = {
|
|
105
|
+
topicId: string;
|
|
106
|
+
reason: string;
|
|
107
|
+
untestedPrereqs: string[];
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* 学习路径条目
|
|
111
|
+
*/
|
|
112
|
+
type PathEntry = {
|
|
113
|
+
topicId: string;
|
|
114
|
+
status: TopicStatus;
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* 诊断结果汇总
|
|
118
|
+
*/
|
|
119
|
+
type DiagnosisSummary = {
|
|
120
|
+
targets: string[];
|
|
121
|
+
weakRoots: WeakRoot[];
|
|
122
|
+
shaky: string[];
|
|
123
|
+
path: PathEntry[];
|
|
124
|
+
masteredDirect: string[];
|
|
125
|
+
masteredInferred: string[];
|
|
126
|
+
weak: string[];
|
|
127
|
+
untested: string[];
|
|
128
|
+
limitReached: boolean;
|
|
129
|
+
retestPending: string[];
|
|
130
|
+
};
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/graph.utils.d.ts
|
|
133
|
+
/**
|
|
134
|
+
* 邻接表即先修方向与被依赖方向
|
|
135
|
+
*/
|
|
136
|
+
type Adjacency = {
|
|
137
|
+
/**
|
|
138
|
+
* 知识点列表按入参顺序保留
|
|
139
|
+
*/
|
|
140
|
+
topics: EngineTopic[];
|
|
141
|
+
/**
|
|
142
|
+
* id 到知识点的映射
|
|
143
|
+
*/
|
|
144
|
+
byId: Map<string, EngineTopic>;
|
|
145
|
+
/**
|
|
146
|
+
* 先修邻接即 id 到直接先修边列表,硬边在前软边在后
|
|
147
|
+
*/
|
|
148
|
+
pre: Map<string, EngineDependency[]>;
|
|
149
|
+
/**
|
|
150
|
+
* 被依赖邻接即 id 到直接依赖边列表
|
|
151
|
+
*/
|
|
152
|
+
post: Map<string, EngineDependency[]>;
|
|
153
|
+
/**
|
|
154
|
+
* 知识点在 topics 中的序号即稳定排序基准
|
|
155
|
+
*/
|
|
156
|
+
order: Map<string, number>;
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* 按阶段折叠的聚合节点
|
|
160
|
+
*/
|
|
161
|
+
type StageFoldGroup = {
|
|
162
|
+
/**
|
|
163
|
+
* 阶段序号
|
|
164
|
+
*/
|
|
165
|
+
stage: number;
|
|
166
|
+
/**
|
|
167
|
+
* 成员 id 列表
|
|
168
|
+
*/
|
|
169
|
+
members: string[];
|
|
170
|
+
};
|
|
171
|
+
/**
|
|
172
|
+
* 可达方向即先修方向或被依赖方向
|
|
173
|
+
*/
|
|
174
|
+
type ReachDirection = 'pre' | 'post';
|
|
175
|
+
/**
|
|
176
|
+
* 构建邻接表
|
|
177
|
+
*
|
|
178
|
+
* @param graph - 诊断图
|
|
179
|
+
* @returns 邻接表
|
|
180
|
+
*/
|
|
181
|
+
declare function buildAdjacency(graph: DiagnosisGraph): Adjacency;
|
|
182
|
+
/**
|
|
183
|
+
* 稳定拓扑排序
|
|
184
|
+
*
|
|
185
|
+
* @remarks
|
|
186
|
+
* 先修在前,同层按知识点在 topics 列表中的原始顺序排,保证结果可复现
|
|
187
|
+
* @param graph - 诊断图
|
|
188
|
+
* @returns 拓扑序的 id 列表
|
|
189
|
+
* @throws {@link Error} 图含环时抛出并列出环成员
|
|
190
|
+
*/
|
|
191
|
+
declare function topologicalSort(graph: DiagnosisGraph): string[];
|
|
192
|
+
/**
|
|
193
|
+
* 祖先闭包即目标及其全部先修
|
|
194
|
+
*
|
|
195
|
+
* @param adjacency - 邻接表
|
|
196
|
+
* @param targets - 目标 id 列表
|
|
197
|
+
* @returns 闭包内 id 集合
|
|
198
|
+
*/
|
|
199
|
+
declare function ancestorClosure(adjacency: Adjacency, targets: string[]): Set<string>;
|
|
200
|
+
/**
|
|
201
|
+
* 后代闭包即起点及其全部依赖者
|
|
202
|
+
*
|
|
203
|
+
* @param adjacency - 邻接表
|
|
204
|
+
* @param seeds - 起点 id 列表
|
|
205
|
+
* @returns 闭包内 id 集合
|
|
206
|
+
*/
|
|
207
|
+
declare function descendantClosure(adjacency: Adjacency, seeds: string[]): Set<string>;
|
|
208
|
+
/**
|
|
209
|
+
* 深度限界的上游子图即向上指定层数的先修集合
|
|
210
|
+
*
|
|
211
|
+
* @param adjacency - 邻接表
|
|
212
|
+
* @param id - 中心知识点 id
|
|
213
|
+
* @param depth - 层数,传 `Infinity` 即全部祖先
|
|
214
|
+
* @returns 含中心点在内的 id 集合
|
|
215
|
+
*/
|
|
216
|
+
declare function upstreamWithin(adjacency: Adjacency, id: string, depth: number): Set<string>;
|
|
217
|
+
/**
|
|
218
|
+
* 传递约简即子图内可删除的传递边集合
|
|
219
|
+
*
|
|
220
|
+
* @remarks
|
|
221
|
+
* 对每条边检查是否存在更长路径绕行,绕行可达即该边冗余,
|
|
222
|
+
* 结果供路径图省略中间点时保真呈现
|
|
223
|
+
* @param adjacency - 邻接表
|
|
224
|
+
* @param nodeSet - 子图 id 集合
|
|
225
|
+
* @returns 冗余边的 `先修>后继` 键集合
|
|
226
|
+
*/
|
|
227
|
+
declare function transitiveRedundancy(adjacency: Adjacency, nodeSet: Set<string>): Set<string>;
|
|
228
|
+
/**
|
|
229
|
+
* 按阶段折叠祖先集合
|
|
230
|
+
*
|
|
231
|
+
* @remarks
|
|
232
|
+
* 中心点与其直接后代不参与折叠,其余祖先按阶段聚合成超节点
|
|
233
|
+
* @param adjacency - 邻接表
|
|
234
|
+
* @param nodeSet - 待折叠的 id 集合
|
|
235
|
+
* @param center - 中心点 id
|
|
236
|
+
* @returns 各阶段聚合组列表按阶段升序
|
|
237
|
+
*/
|
|
238
|
+
declare function foldByStage(adjacency: Adjacency, nodeSet: Set<string>, center: string): StageFoldGroup[];
|
|
239
|
+
/**
|
|
240
|
+
* 可达计数即沿指定方向的全部可达节点数
|
|
241
|
+
*
|
|
242
|
+
* @param adjacency - 邻接表
|
|
243
|
+
* @param id - 起点
|
|
244
|
+
* @param direction - `pre` 即先修方向,`post` 即被依赖方向
|
|
245
|
+
* @returns 不含起点的可达数
|
|
246
|
+
*/
|
|
247
|
+
declare function reachCount(adjacency: Adjacency, id: string, direction: ReachDirection): number;
|
|
248
|
+
//#endregion
|
|
249
|
+
//#region src/judge.utils.d.ts
|
|
250
|
+
/**
|
|
251
|
+
* 短答文本规范化
|
|
252
|
+
*
|
|
253
|
+
* @remarks
|
|
254
|
+
* 去全部空白、全角转半角、统一常见中文标点,保证书写差异不影响比对
|
|
255
|
+
* @param text - 原始作答文本
|
|
256
|
+
* @returns 规范化后文本
|
|
257
|
+
*/
|
|
258
|
+
declare function normalizeAnswer(text: string): string;
|
|
259
|
+
/**
|
|
260
|
+
* 选择题选项键规范化
|
|
261
|
+
*
|
|
262
|
+
* @remarks
|
|
263
|
+
* 接受 `B`、`b`、`(B)`、`B.` 等常见写法,也接受与选项文本规范化后一致的全串
|
|
264
|
+
* @param input - 作答输入
|
|
265
|
+
* @param question - 题目
|
|
266
|
+
* @returns 命中的选项键,无法识别返回 `null`
|
|
267
|
+
*/
|
|
268
|
+
declare function matchChoiceKey(input: string, question: EngineQuestion): string | null;
|
|
269
|
+
/**
|
|
270
|
+
* 判定一道题的作答
|
|
271
|
+
*
|
|
272
|
+
* @param question - 题目
|
|
273
|
+
* @param userAnswer - 学习者作答文本
|
|
274
|
+
* @returns `correct` 即判对,`wrong` 即判错,`manual` 即判题器不下结论须自评
|
|
275
|
+
*/
|
|
276
|
+
declare function judgeAnswer(question: EngineQuestion, userAnswer: string): JudgeResult;
|
|
277
|
+
//#endregion
|
|
278
|
+
//#region src/random.utils.d.ts
|
|
279
|
+
/**
|
|
280
|
+
* 确定性伪随机源
|
|
281
|
+
*
|
|
282
|
+
* mulberry32 实现,同一随机种子产出完全一致的序列,保证同种子同作答即同出题
|
|
283
|
+
*/
|
|
284
|
+
/**
|
|
285
|
+
* 返回 `0` 到 `1` 区间随机数的生成函数
|
|
286
|
+
*/
|
|
287
|
+
type RandomSource = () => number;
|
|
288
|
+
/**
|
|
289
|
+
* 由任意字符串派生 32 位随机种子
|
|
290
|
+
*
|
|
291
|
+
* @param text - 种子文本
|
|
292
|
+
* @returns 32 位无符号整数种子
|
|
293
|
+
*/
|
|
294
|
+
declare function hashSeed(text: string): number;
|
|
295
|
+
/**
|
|
296
|
+
* mulberry32 随机数生成器
|
|
297
|
+
*
|
|
298
|
+
* @param seed - 32 位无符号整数种子
|
|
299
|
+
* @returns 随机数生成函数
|
|
300
|
+
*/
|
|
301
|
+
declare function mulberry32(seed: number): RandomSource;
|
|
302
|
+
/**
|
|
303
|
+
* 由种子文本与命名空间派生的定点随机数
|
|
304
|
+
*
|
|
305
|
+
* @remarks
|
|
306
|
+
* 用于选题确定性,同一 `seed` 与 `scope` 永远返回同一个小数
|
|
307
|
+
* @param seed - 会话种子文本
|
|
308
|
+
* @param scope - 作用域文本如知识点加次序
|
|
309
|
+
* @returns `0` 到 `1` 区间随机数
|
|
310
|
+
*/
|
|
311
|
+
declare function scopedRandom(seed: string, scope: string): number;
|
|
312
|
+
//#endregion
|
|
313
|
+
//#region src/session.engine.d.ts
|
|
314
|
+
/**
|
|
315
|
+
* 会话创建选项
|
|
316
|
+
*/
|
|
317
|
+
type SessionOptions = {
|
|
318
|
+
/**
|
|
319
|
+
* 题量上限按题计
|
|
320
|
+
*
|
|
321
|
+
* @default 16
|
|
322
|
+
*/
|
|
323
|
+
limit?: number;
|
|
324
|
+
/**
|
|
325
|
+
* 选题随机种子文本
|
|
326
|
+
*
|
|
327
|
+
* @default 随机生成
|
|
328
|
+
*/
|
|
329
|
+
seed?: string;
|
|
330
|
+
/**
|
|
331
|
+
* 历史已出题标识列表即跨会话避免重复出题
|
|
332
|
+
*
|
|
333
|
+
* @default 空列表
|
|
334
|
+
*/
|
|
335
|
+
avoid?: string[];
|
|
336
|
+
};
|
|
337
|
+
/**
|
|
338
|
+
* 创建诊断会话
|
|
339
|
+
*
|
|
340
|
+
* @param graph - 诊断图
|
|
341
|
+
* @param targets - 目标知识点 id 列表
|
|
342
|
+
* @param options - 会话选项
|
|
343
|
+
* @returns 初始会话状态,先修子图已按稳定拓扑序展开
|
|
344
|
+
* @throws {@link Error} 诊断图含环时由拓扑排序抛出
|
|
345
|
+
*/
|
|
346
|
+
declare function createSession(graph: DiagnosisGraph, targets: string[], options?: SessionOptions): SessionState;
|
|
347
|
+
/**
|
|
348
|
+
* 取当前待答题目
|
|
349
|
+
*
|
|
350
|
+
* @param graph - 诊断图
|
|
351
|
+
* @param session - 会话状态
|
|
352
|
+
* @param avoid - 历史已出题标识列表
|
|
353
|
+
* @returns 待答题目及其提问理由,会话结束或达题量上限时返回 `null`
|
|
354
|
+
*/
|
|
355
|
+
declare function nextQuestion(graph: DiagnosisGraph, session: SessionState, avoid?: string[]): SessionState['current'];
|
|
356
|
+
/**
|
|
357
|
+
* 登记一次作答并推进状态
|
|
358
|
+
*
|
|
359
|
+
* @remarks
|
|
360
|
+
* 调用方给出 `selfJudged` 时以其为准即覆盖判题器结论,用于自评题与手动改判两种场景
|
|
361
|
+
* @param graph - 诊断图
|
|
362
|
+
* @param session - 会话状态
|
|
363
|
+
* @param userAnswer - 作答文本
|
|
364
|
+
* @param selfJudged - 学习者裁定的结论即自评或改判
|
|
365
|
+
*/
|
|
366
|
+
declare function applyAnswer(graph: DiagnosisGraph, session: SessionState, userAnswer: string, selfJudged?: 'correct' | 'wrong'): void;
|
|
367
|
+
/**
|
|
368
|
+
* 由作答记录重放会话
|
|
369
|
+
*
|
|
370
|
+
* @remarks
|
|
371
|
+
* 刷新恢复、撤销与更正统一走本函数,保证状态与记录一致
|
|
372
|
+
* @param graph - 诊断图
|
|
373
|
+
* @param targets - 目标知识点 id 列表
|
|
374
|
+
* @param options - 会话选项
|
|
375
|
+
* @param answers - 作答记录列表
|
|
376
|
+
* @returns 重放后的会话状态
|
|
377
|
+
* @throws {@link Error} 诊断图含环时由会话创建抛出
|
|
378
|
+
*/
|
|
379
|
+
declare function replay(graph: DiagnosisGraph, targets: string[], options: SessionOptions, answers: AnswerRecord[]): SessionState;
|
|
380
|
+
/**
|
|
381
|
+
* 汇总诊断结果
|
|
382
|
+
*
|
|
383
|
+
* @param graph - 诊断图
|
|
384
|
+
* @param session - 会话状态
|
|
385
|
+
* @returns 含薄弱根因、不稳清单、学习路径与掌握清单的汇总
|
|
386
|
+
*/
|
|
387
|
+
declare function summarize(graph: DiagnosisGraph, session: SessionState): DiagnosisSummary;
|
|
388
|
+
//#endregion
|
|
389
|
+
export { type Adjacency, type AnswerRecord, type DiagnosisGraph, type DiagnosisSummary, type EngineDependency, type EngineQuestion, type EngineTopic, type JudgeResult, type PathEntry, type PendingQuestion, type RandomSource, type ReachDirection, type SessionOptions, type SessionState, type StageFoldGroup, type TopicStatus, type WeakRoot, ancestorClosure, applyAnswer, buildAdjacency, createSession, descendantClosure, foldByStage, hashSeed, judgeAnswer, matchChoiceKey, mulberry32, nextQuestion, normalizeAnswer, reachCount, replay, scopedRandom, summarize, topologicalSort, transitiveRedundancy, upstreamWithin };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function e(e){let t=new Map(e.topics.map(e=>[e.id,e])),n=new Map(e.topics.map((e,t)=>[e.id,t])),r=/* @__PURE__ */ new Map,i=/* @__PURE__ */ new Map;for(let t of e.topics)r.set(t.id,[]),i.set(t.id,[]);for(let t of e.dependencies)r.get(t.topicId)?.push(t),i.get(t.prerequisiteId)?.push(t);for(let e of r.values())e.sort((e,t)=>e.strength===t.strength?0:e.strength===`hard`?-1:1);return{topics:e.topics,byId:t,pre:r,post:i,order:n}}function t(t){let n=e(t),r=/* @__PURE__ */ new Map;for(let e of t.topics)r.set(e.id,n.pre.get(e.id)?.length??0);let i=t.topics.filter(e=>(r.get(e.id)??0)===0).map(e=>e.id),a=[];for(;i.length>0;){let e=i.shift();if(e===void 0)break;a.push(e);for(let t of n.post.get(e)??[]){let e=(r.get(t.topicId)??0)-1;r.set(t.topicId,e),e===0&&i.push(t.topicId)}}if(a.length!==t.topics.length){let e=t.topics.filter(e=>!a.includes(e.id)).map(e=>e.id);throw Error(`图含环,环成员即 ${e.join(`、`)}`)}return a}function n(e,t){let n=/* @__PURE__ */ new Set,r=t=>{if(!n.has(t)){n.add(t);for(let n of e.pre.get(t)??[])r(n.prerequisiteId)}};for(let e of t)r(e);return n}function r(e,t){let n=/* @__PURE__ */ new Set,r=t=>{if(!n.has(t)){n.add(t);for(let n of e.post.get(t)??[])r(n.topicId)}};for(let e of t)r(e);return n}function i(e,t,n){let r=/* @__PURE__ */ new Set([t]),i=[t],a=n===1/0?2**53-1:n;for(let t=0;t<a;t+=1){let t=[];for(let n of i)for(let i of e.pre.get(n)??[])r.has(i.prerequisiteId)||(r.add(i.prerequisiteId),t.push(i.prerequisiteId));if(t.length===0)break;i=t}return r}function a(e,t){let n=/* @__PURE__ */ new Set,r=[];for(let n of e.topics)for(let i of e.pre.get(n.id)??[])t.has(i.topicId)&&t.has(i.prerequisiteId)&&r.push(i);for(let i of r){let r=`${i.prerequisiteId}>${i.topicId}`;if(n.has(r))continue;let a=/* @__PURE__ */ new Set,o=n=>{for(let r of e.pre.get(n)??[])t.has(r.prerequisiteId)&&(n!==i.topicId||r.prerequisiteId!==i.prerequisiteId)&&(a.has(r.prerequisiteId)||(a.add(r.prerequisiteId),o(r.prerequisiteId)))};o(i.topicId),a.has(i.prerequisiteId)&&n.add(r)}return n}function o(e,t,n){let r=new Set((e.post.get(n)??[]).map(e=>e.topicId)),i=/* @__PURE__ */ new Map;for(let a of t){if(a===n||r.has(a))continue;let t=e.byId.get(a)?.stage??0,o=i.get(t)??[];o.push(a),i.set(t,o)}return[...i.entries()].map(([e,t])=>({stage:e,members:t})).sort((e,t)=>e.stage-t.stage)}function s(e,t,n){let r=/* @__PURE__ */ new Set,i=t=>{for(let a of e[n].get(t)??[]){let e=n===`pre`?a.prerequisiteId:a.topicId;r.has(e)||(r.add(e),i(e))}};return i(t),r.size}function c(e){return e.normalize(`NFKC`).replace(/\s+/g,``).replace(/、/g,`,`).replace(/。/g,``).replace(/,/g,`,`).replace(/:/g,`:`).toLowerCase()}function l(e,t){let n=c(e),r=/^[((]?([abcd])[)..]?$/.exec(n)?.[1];if(r)return r.toUpperCase();for(let e of t.choices??[])if(c(e.text)===n)return e.key;return null}function u(e,t){if(e.type===`self`)return`manual`;if(e.type===`choice`){let n=l(t,e);return n===null?`manual`:n===e.answer?`correct`:`wrong`}let n=c(t);if(n===``)return`manual`;let r=[e.answer,...e.accept??[]];for(let e of r)if(c(e)===n)return`correct`;return e.answer.length<=24?`wrong`:`manual`}function d(e){let t=2166136261;for(let n=0;n<e.length;n+=1)t^=e.charCodeAt(n),t=Math.imul(t,16777619);return t>>>0}function f(e){let t=e>>>0;return()=>{t=t+1831565813>>>0;let e=t;return e=Math.imul(e^e>>>15,e|1),e^=e+Math.imul(e^e>>>7,e|61),((e^e>>>14)>>>0)/4294967296}}function p(e,t){return f(d(`${e}|${t}`))()}const m={basic:0,standard:1,advanced:2};function h(r,i,a={}){let o=n(e(r),i),s=t(r).filter(e=>o.has(e)),c={};for(let e of s)c[e]=`pending`;let l=i.filter(e=>o.has(e)),u=new Map(s.map((e,t)=>[e,t])),d=[...l].sort((e,t)=>(u.get(e)??0)-(u.get(t)??0));return{seed:a.seed??`auto-${Date.now()}-${Math.floor(Math.random()*1e6)}`,targets:l,limit:a.limit??16,subgraph:s,status:c,stack:d,askedQuestionIds:[],answers:[],finished:l.length===0,limitReached:!1,current:null}}function g(e,t,n,r){return p(e.seed,`${t}|${n}|${r.id}`)}function _(e,t,n,r){if(r.length===0)return null;let i=r[0];if(!i)return null;for(let a of r)g(e,t,n,a)<g(e,t,n,i)&&(i=a);return i}function v(e,t,n,r,i){let a=e.questions.filter(e=>e.topicId===n&&!t.askedQuestionIds.includes(e.id));if(a.length===0)return null;let o=a.filter(e=>!i.includes(e.id)),s=o.length>0?o:a;if(r===1){let e=s.filter(e=>e.difficulty!==`advanced`);return _(t,n,r,e.length>0?e:s)}let c=t.answers.find(e=>e.topicId===n&&e.nth===1),l=e.questions.find(e=>e.id===c?.questionId),u=m[l?.difficulty??`standard`],d=[[],[],[]];for(let e of a)d[m[e.difficulty]]?.push(e);let f=[0,u,...[0,1,2].filter(e=>e<u),...[0,1,2].filter(e=>e>u)],p=[...new Set(f)];for(let e of p){let i=d[e],a=i?_(t,n,r,i):null;if(a)return a}return null}function y(e,t,n=[]){if(t.finished||t.current)return t.current;if(t.askedQuestionIds.length>=t.limit)return t.limitReached=!0,t.finished=!0,null;for(;t.stack.length>0;){let r=t.stack.at(-1);if(r===void 0)break;if(t.status[r]!==`pending`){t.stack.pop();continue}let i=t.answers.filter(e=>e.topicId===r).length+1===1?1:2,a=v(e,t,r,i,n);if(!a){t.stack.pop();continue}let o=t.targets.includes(r);return t.current={question:a,topicId:r,nth:i,reason:o?`目标本身`:i===2?`同一知识点的复测`:!o&&i===1?`因后继知识点两题未过而回溯`:`目标链上的知识点`},t.current}return t.finished=!0,null}function b(t,r,i,a){let o=r.current;if(!o)return;let s=u(o.question,i),c=a??(s===`manual`?`manual`:s);if(c===`manual`)return;let l={questionId:o.question.id,topicId:o.topicId,nth:o.nth,userAnswer:i,result:a?a===`correct`?`manual-correct`:`manual-wrong`:c,judgedBy:a?`self`:`auto`};r.answers.push(l),r.askedQuestionIds.push(o.question.id),r.current=null;let d=e(t),f=c===`correct`;if(o.nth===1&&f){r.status[o.topicId]=`mastered`;for(let e of n(d,[o.topicId]))r.status[e]===`pending`&&(r.status[e]=`mastered-inferred`)}else if(o.nth===1&&!f)r.stack.push(o.topicId);else if(o.nth===2&&!f){r.status[o.topicId]=`weak`;let e=(d.pre.get(o.topicId)??[]).filter(e=>r.subgraph.includes(e.prerequisiteId)&&r.status[e.prerequisiteId]===`pending`);for(let t=e.length-1;t>=0;--t){let n=e[t];n&&r.stack.push(n.prerequisiteId)}}else r.status[o.topicId]=`shaky`;r.askedQuestionIds.length>=r.limit&&(r.limitReached=!0,r.finished=!0)}function x(e,t,n,r){let i=h(e,t,n);for(let t of r){let r=y(e,i,n.avoid??[]);if(!r||r.question.id!==t.questionId)break;let a=t.result===`correct`||t.result===`manual-correct`?`correct`:`wrong`;b(e,i,t.userAnswer,a)}return i}function S(t,n){let i=e(t),a=[],o=[],s=[],c=[],l=[],u=[],d=new Set(n.answers.map(e=>e.topicId));for(let e of n.subgraph){let t=n.status[e];if(t===`weak`)a.push(e);else if(t===`shaky`)s.push(e);else if(t===`mastered`)c.push(e);else if(t===`mastered-inferred`)l.push(e);else if(u.push(e),d.has(e)){let t=n.answers.filter(t=>t.topicId===e),r=t[0];t.length===1&&r&&r.result!==`correct`&&r.result!==`manual-correct`&&(a.push(e),o.push(e))}}let f=new Set(a),p=a.map(e=>({topicId:e,reason:`两题未过且其直接先修未发现薄弱,推断薄弱从这里开始`,untestedPrereqs:(i.pre.get(e)??[]).filter(e=>n.status[e.prerequisiteId]===`pending`).map(e=>e.prerequisiteId)})).filter(e=>!(i.pre.get(e.topicId)??[]).some(e=>f.has(e.prerequisiteId))),m=r(i,[...p.map(e=>e.topicId),...s]),h=/* @__PURE__ */ new Set([...c,...l]),g=new Map(n.subgraph.map((e,t)=>[e,t])),_=n.subgraph.filter(e=>m.has(e)&&!h.has(e)).map(e=>({topicId:e,status:n.status[e]??`pending`})).sort((e,t)=>(i.byId.get(e.topicId)?.stage??0)-(i.byId.get(t.topicId)?.stage??0)||(g.get(e.topicId)??0)-(g.get(t.topicId)??0));return{targets:n.targets,weakRoots:p,shaky:s,path:_,masteredDirect:c,masteredInferred:l,weak:a,untested:u.filter(e=>!o.includes(e)),limitReached:n.limitReached,retestPending:o}}export{n as ancestorClosure,b as applyAnswer,e as buildAdjacency,h as createSession,r as descendantClosure,o as foldByStage,d as hashSeed,u as judgeAnswer,l as matchChoiceKey,f as mulberry32,y as nextQuestion,c as normalizeAnswer,s as reachCount,x as replay,p as scopedRandom,S as summarize,t as topologicalSort,a as transitiveRedundancy,i as upstreamWithin};
|
package/package.json
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@longzai-intelligence-knowledge-graph/diagnosis",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "知识图谱先修图通用诊断引擎即自适应出题、判题与学习路径汇总的纯函数包",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"diagnosis",
|
|
7
|
+
"engine",
|
|
8
|
+
"knowledge-graph",
|
|
9
|
+
"longzai-intelligence",
|
|
10
|
+
"prerequisite-graph"
|
|
11
|
+
],
|
|
12
|
+
"license": "UNLICENSED",
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "lzi-builder",
|
|
27
|
+
"build:prod": "NODE_ENV=production bun run build",
|
|
28
|
+
"prepublishOnly": "bun run build:prod",
|
|
29
|
+
"typecheck": "bun run typecheck:app && bun run typecheck:node && bun run typecheck:test",
|
|
30
|
+
"typecheck:app": "lzi-tsgo typecheck tsconfig/app.json",
|
|
31
|
+
"typecheck:node": "lzi-tsgo typecheck tsconfig/node.json",
|
|
32
|
+
"typecheck:test": "lzi-tsgo typecheck tsconfig/test.json",
|
|
33
|
+
"lint": "oxlint && oxfmt --check",
|
|
34
|
+
"lint:fix": "oxlint --fix && oxfmt",
|
|
35
|
+
"test": "lzi-bun-cli test",
|
|
36
|
+
"test:watch": "lzi-bun-cli test --watch",
|
|
37
|
+
"test:coverage": "lzi-bun-cli test --coverage",
|
|
38
|
+
"clean": "lzi-dev-cli clean"
|
|
39
|
+
}
|
|
40
|
+
}
|