gaokao-pro 0.1.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 +109 -0
- package/data/datasets/gaoshui-yundongdui-2024.json +33 -0
- package/data/datasets/junjing-jingxiao.json +75 -0
- package/data/datasets/provinces-specialty-2024.json +139 -0
- package/data/datasets/qiangji-2024.json +52 -0
- package/data/datasets/schools-adapters-2024.json +123 -0
- package/data/datasets/sushe-shitang.json +40 -0
- package/data/datasets/tijian-shouxian-zhuanye.json +29 -0
- package/data/datasets/xiaoyuzhong-zhaosheng.json +18 -0
- package/data/datasets/xueke-pinggu-disculun.json +28 -0
- package/data/datasets/zhongwai-hezuo-2024.json +36 -0
- package/data/datasets/zonghepingjia-2024.json +53 -0
- package/data/school-index.json.gz +0 -0
- package/data/yifenyiduan/_ocr-pipeline/extract-pdf.py +117 -0
- package/data/yifenyiduan/beijing-2023-combined.json +1 -0
- package/data/yifenyiduan/beijing-2024-combined.json +1 -0
- package/data/yifenyiduan/beijing-2025-combined.json +1 -0
- package/data/yifenyiduan/henan-2024-liberal.json +1 -0
- package/data/yifenyiduan/hunan-2024-history.json +1 -0
- package/dist/aliases.d.ts +2 -0
- package/dist/aliases.js +120 -0
- package/dist/chart-check.d.ts +20 -0
- package/dist/chart-check.js +99 -0
- package/dist/codes.d.ts +162 -0
- package/dist/codes.js +59 -0
- package/dist/compare.d.ts +39 -0
- package/dist/compare.js +112 -0
- package/dist/datasets.d.ts +65 -0
- package/dist/datasets.js +82 -0
- package/dist/find.d.ts +48 -0
- package/dist/find.js +87 -0
- package/dist/format.d.ts +17 -0
- package/dist/format.js +109 -0
- package/dist/gaokao-cn.d.ts +122 -0
- package/dist/gaokao-cn.js +49 -0
- package/dist/index-loader.d.ts +33 -0
- package/dist/index-loader.js +59 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +727 -0
- package/dist/match.d.ts +40 -0
- package/dist/match.js +118 -0
- package/dist/mcp.d.ts +1 -0
- package/dist/mcp.js +535 -0
- package/dist/memory.d.ts +31 -0
- package/dist/memory.js +69 -0
- package/dist/paiming.d.ts +29 -0
- package/dist/paiming.js +66 -0
- package/dist/probe.d.ts +1 -0
- package/dist/probe.js +73 -0
- package/dist/provinces/guangdong.d.ts +27 -0
- package/dist/provinces/guangdong.js +68 -0
- package/dist/provinces/index.d.ts +1 -0
- package/dist/provinces/index.js +17 -0
- package/dist/rank-table.d.ts +31 -0
- package/dist/rank-table.js +74 -0
- package/dist/recommend-major.d.ts +34 -0
- package/dist/recommend-major.js +119 -0
- package/dist/recommend.d.ts +54 -0
- package/dist/recommend.js +147 -0
- package/dist/selftest.d.ts +11 -0
- package/dist/selftest.js +48 -0
- package/dist/top.d.ts +29 -0
- package/dist/top.js +66 -0
- package/dist/xuanke.d.ts +8 -0
- package/dist/xuanke.js +35 -0
- package/package.json +40 -0
package/dist/codes.d.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
export declare const PROVINCES: {
|
|
2
|
+
readonly 11: {
|
|
3
|
+
readonly name: "北京";
|
|
4
|
+
readonly pinyin: "beijing";
|
|
5
|
+
readonly reform: "3+3";
|
|
6
|
+
};
|
|
7
|
+
readonly 12: {
|
|
8
|
+
readonly name: "天津";
|
|
9
|
+
readonly pinyin: "tianjin";
|
|
10
|
+
readonly reform: "3+3";
|
|
11
|
+
};
|
|
12
|
+
readonly 13: {
|
|
13
|
+
readonly name: "河北";
|
|
14
|
+
readonly pinyin: "hebei";
|
|
15
|
+
readonly reform: "3+1+2";
|
|
16
|
+
};
|
|
17
|
+
readonly 14: {
|
|
18
|
+
readonly name: "山西";
|
|
19
|
+
readonly pinyin: "shanxi";
|
|
20
|
+
readonly reform: "old";
|
|
21
|
+
};
|
|
22
|
+
readonly 15: {
|
|
23
|
+
readonly name: "内蒙古";
|
|
24
|
+
readonly pinyin: "neimenggu";
|
|
25
|
+
readonly reform: "old";
|
|
26
|
+
};
|
|
27
|
+
readonly 21: {
|
|
28
|
+
readonly name: "辽宁";
|
|
29
|
+
readonly pinyin: "liaoning";
|
|
30
|
+
readonly reform: "3+1+2";
|
|
31
|
+
};
|
|
32
|
+
readonly 22: {
|
|
33
|
+
readonly name: "吉林";
|
|
34
|
+
readonly pinyin: "jilin";
|
|
35
|
+
readonly reform: "3+1+2";
|
|
36
|
+
};
|
|
37
|
+
readonly 23: {
|
|
38
|
+
readonly name: "黑龙江";
|
|
39
|
+
readonly pinyin: "heilongjiang";
|
|
40
|
+
readonly reform: "3+1+2";
|
|
41
|
+
};
|
|
42
|
+
readonly 31: {
|
|
43
|
+
readonly name: "上海";
|
|
44
|
+
readonly pinyin: "shanghai";
|
|
45
|
+
readonly reform: "3+3";
|
|
46
|
+
};
|
|
47
|
+
readonly 32: {
|
|
48
|
+
readonly name: "江苏";
|
|
49
|
+
readonly pinyin: "jiangsu";
|
|
50
|
+
readonly reform: "3+1+2";
|
|
51
|
+
};
|
|
52
|
+
readonly 33: {
|
|
53
|
+
readonly name: "浙江";
|
|
54
|
+
readonly pinyin: "zhejiang";
|
|
55
|
+
readonly reform: "3+3";
|
|
56
|
+
};
|
|
57
|
+
readonly 34: {
|
|
58
|
+
readonly name: "安徽";
|
|
59
|
+
readonly pinyin: "anhui";
|
|
60
|
+
readonly reform: "3+1+2";
|
|
61
|
+
};
|
|
62
|
+
readonly 35: {
|
|
63
|
+
readonly name: "福建";
|
|
64
|
+
readonly pinyin: "fujian";
|
|
65
|
+
readonly reform: "3+1+2";
|
|
66
|
+
};
|
|
67
|
+
readonly 36: {
|
|
68
|
+
readonly name: "江西";
|
|
69
|
+
readonly pinyin: "jiangxi";
|
|
70
|
+
readonly reform: "3+1+2";
|
|
71
|
+
};
|
|
72
|
+
readonly 37: {
|
|
73
|
+
readonly name: "山东";
|
|
74
|
+
readonly pinyin: "shandong";
|
|
75
|
+
readonly reform: "3+3";
|
|
76
|
+
};
|
|
77
|
+
readonly 41: {
|
|
78
|
+
readonly name: "河南";
|
|
79
|
+
readonly pinyin: "henan";
|
|
80
|
+
readonly reform: "3+1+2";
|
|
81
|
+
};
|
|
82
|
+
readonly 42: {
|
|
83
|
+
readonly name: "湖北";
|
|
84
|
+
readonly pinyin: "hubei";
|
|
85
|
+
readonly reform: "3+1+2";
|
|
86
|
+
};
|
|
87
|
+
readonly 43: {
|
|
88
|
+
readonly name: "湖南";
|
|
89
|
+
readonly pinyin: "hunan";
|
|
90
|
+
readonly reform: "3+1+2";
|
|
91
|
+
};
|
|
92
|
+
readonly 44: {
|
|
93
|
+
readonly name: "广东";
|
|
94
|
+
readonly pinyin: "guangdong";
|
|
95
|
+
readonly reform: "3+1+2";
|
|
96
|
+
};
|
|
97
|
+
readonly 45: {
|
|
98
|
+
readonly name: "广西";
|
|
99
|
+
readonly pinyin: "guangxi";
|
|
100
|
+
readonly reform: "3+1+2";
|
|
101
|
+
};
|
|
102
|
+
readonly 46: {
|
|
103
|
+
readonly name: "海南";
|
|
104
|
+
readonly pinyin: "hainan";
|
|
105
|
+
readonly reform: "3+3";
|
|
106
|
+
};
|
|
107
|
+
readonly 50: {
|
|
108
|
+
readonly name: "重庆";
|
|
109
|
+
readonly pinyin: "chongqing";
|
|
110
|
+
readonly reform: "3+1+2";
|
|
111
|
+
};
|
|
112
|
+
readonly 51: {
|
|
113
|
+
readonly name: "四川";
|
|
114
|
+
readonly pinyin: "sichuan";
|
|
115
|
+
readonly reform: "3+1+2";
|
|
116
|
+
};
|
|
117
|
+
readonly 52: {
|
|
118
|
+
readonly name: "贵州";
|
|
119
|
+
readonly pinyin: "guizhou";
|
|
120
|
+
readonly reform: "3+1+2";
|
|
121
|
+
};
|
|
122
|
+
readonly 53: {
|
|
123
|
+
readonly name: "云南";
|
|
124
|
+
readonly pinyin: "yunnan";
|
|
125
|
+
readonly reform: "3+1+2";
|
|
126
|
+
};
|
|
127
|
+
readonly 54: {
|
|
128
|
+
readonly name: "西藏";
|
|
129
|
+
readonly pinyin: "xizang";
|
|
130
|
+
readonly reform: "old";
|
|
131
|
+
};
|
|
132
|
+
readonly 61: {
|
|
133
|
+
readonly name: "陕西";
|
|
134
|
+
readonly pinyin: "shaanxi";
|
|
135
|
+
readonly reform: "3+1+2";
|
|
136
|
+
};
|
|
137
|
+
readonly 62: {
|
|
138
|
+
readonly name: "甘肃";
|
|
139
|
+
readonly pinyin: "gansu";
|
|
140
|
+
readonly reform: "3+1+2";
|
|
141
|
+
};
|
|
142
|
+
readonly 63: {
|
|
143
|
+
readonly name: "青海";
|
|
144
|
+
readonly pinyin: "qinghai";
|
|
145
|
+
readonly reform: "3+1+2";
|
|
146
|
+
};
|
|
147
|
+
readonly 64: {
|
|
148
|
+
readonly name: "宁夏";
|
|
149
|
+
readonly pinyin: "ningxia";
|
|
150
|
+
readonly reform: "3+1+2";
|
|
151
|
+
};
|
|
152
|
+
readonly 65: {
|
|
153
|
+
readonly name: "新疆";
|
|
154
|
+
readonly pinyin: "xinjiang";
|
|
155
|
+
readonly reform: "3+1+2";
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
export type ProvinceId = keyof typeof PROVINCES;
|
|
159
|
+
export declare const TRACK_NAMES: Record<string, string>;
|
|
160
|
+
export declare function resolveProvince(input: string | number): ProvinceId | null;
|
|
161
|
+
export type Subject = "物理" | "历史" | "化学" | "生物" | "政治" | "地理";
|
|
162
|
+
export declare const ALL_SUBJECTS: Subject[];
|
package/dist/codes.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Province codes used by static-data.gaokao.cn (GB/T 2260 prefix).
|
|
2
|
+
export const PROVINCES = {
|
|
3
|
+
11: { name: "北京", pinyin: "beijing", reform: "3+3" },
|
|
4
|
+
12: { name: "天津", pinyin: "tianjin", reform: "3+3" },
|
|
5
|
+
13: { name: "河北", pinyin: "hebei", reform: "3+1+2" },
|
|
6
|
+
14: { name: "山西", pinyin: "shanxi", reform: "old" },
|
|
7
|
+
15: { name: "内蒙古", pinyin: "neimenggu", reform: "old" },
|
|
8
|
+
21: { name: "辽宁", pinyin: "liaoning", reform: "3+1+2" },
|
|
9
|
+
22: { name: "吉林", pinyin: "jilin", reform: "3+1+2" },
|
|
10
|
+
23: { name: "黑龙江", pinyin: "heilongjiang", reform: "3+1+2" },
|
|
11
|
+
31: { name: "上海", pinyin: "shanghai", reform: "3+3" },
|
|
12
|
+
32: { name: "江苏", pinyin: "jiangsu", reform: "3+1+2" },
|
|
13
|
+
33: { name: "浙江", pinyin: "zhejiang", reform: "3+3" },
|
|
14
|
+
34: { name: "安徽", pinyin: "anhui", reform: "3+1+2" },
|
|
15
|
+
35: { name: "福建", pinyin: "fujian", reform: "3+1+2" },
|
|
16
|
+
36: { name: "江西", pinyin: "jiangxi", reform: "3+1+2" },
|
|
17
|
+
37: { name: "山东", pinyin: "shandong", reform: "3+3" },
|
|
18
|
+
41: { name: "河南", pinyin: "henan", reform: "3+1+2" },
|
|
19
|
+
42: { name: "湖北", pinyin: "hubei", reform: "3+1+2" },
|
|
20
|
+
43: { name: "湖南", pinyin: "hunan", reform: "3+1+2" },
|
|
21
|
+
44: { name: "广东", pinyin: "guangdong", reform: "3+1+2" },
|
|
22
|
+
45: { name: "广西", pinyin: "guangxi", reform: "3+1+2" },
|
|
23
|
+
46: { name: "海南", pinyin: "hainan", reform: "3+3" },
|
|
24
|
+
50: { name: "重庆", pinyin: "chongqing", reform: "3+1+2" },
|
|
25
|
+
51: { name: "四川", pinyin: "sichuan", reform: "3+1+2" },
|
|
26
|
+
52: { name: "贵州", pinyin: "guizhou", reform: "3+1+2" },
|
|
27
|
+
53: { name: "云南", pinyin: "yunnan", reform: "3+1+2" },
|
|
28
|
+
54: { name: "西藏", pinyin: "xizang", reform: "old" },
|
|
29
|
+
61: { name: "陕西", pinyin: "shaanxi", reform: "3+1+2" },
|
|
30
|
+
62: { name: "甘肃", pinyin: "gansu", reform: "3+1+2" },
|
|
31
|
+
63: { name: "青海", pinyin: "qinghai", reform: "3+1+2" },
|
|
32
|
+
64: { name: "宁夏", pinyin: "ningxia", reform: "3+1+2" },
|
|
33
|
+
65: { name: "新疆", pinyin: "xinjiang", reform: "3+1+2" }
|
|
34
|
+
};
|
|
35
|
+
// Subject-track codes seen in static-data.gaokao.cn `type` / `pro_type` fields.
|
|
36
|
+
export const TRACK_NAMES = {
|
|
37
|
+
"1": "理工",
|
|
38
|
+
"2": "文史",
|
|
39
|
+
"3": "综合改革", // 3+3 provinces
|
|
40
|
+
"2073": "物理类", // 3+1+2 物理首选
|
|
41
|
+
"2074": "历史类" // 3+1+2 历史首选
|
|
42
|
+
};
|
|
43
|
+
export function resolveProvince(input) {
|
|
44
|
+
if (typeof input === "number") {
|
|
45
|
+
return (input in PROVINCES) ? input : null;
|
|
46
|
+
}
|
|
47
|
+
const trimmed = String(input).trim().toLowerCase();
|
|
48
|
+
const numeric = Number(trimmed);
|
|
49
|
+
if (Number.isFinite(numeric) && numeric in PROVINCES) {
|
|
50
|
+
return numeric;
|
|
51
|
+
}
|
|
52
|
+
for (const [id, p] of Object.entries(PROVINCES)) {
|
|
53
|
+
if (p.name === trimmed || p.pinyin === trimmed) {
|
|
54
|
+
return Number(id);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
export const ALL_SUBJECTS = ["物理", "历史", "化学", "生物", "政治", "地理"];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type SchoolAdapter } from "./datasets.js";
|
|
2
|
+
import { type ProvinceId } from "./codes.js";
|
|
3
|
+
export type CompareSide = {
|
|
4
|
+
name: string;
|
|
5
|
+
zsCode: string;
|
|
6
|
+
province: string;
|
|
7
|
+
city: string;
|
|
8
|
+
belong: string;
|
|
9
|
+
labels: string[];
|
|
10
|
+
rank: {
|
|
11
|
+
ruanke: string | null;
|
|
12
|
+
qsWorld: string | null;
|
|
13
|
+
usNews: string | null;
|
|
14
|
+
xyh: string | null;
|
|
15
|
+
};
|
|
16
|
+
xuekePinggu: Record<string, string>;
|
|
17
|
+
recentMinScores: Record<string, Array<{
|
|
18
|
+
year: number;
|
|
19
|
+
track: string;
|
|
20
|
+
min: number;
|
|
21
|
+
}>>;
|
|
22
|
+
zswUrl: string | null;
|
|
23
|
+
programs: SchoolAdapter["programs"] | null;
|
|
24
|
+
contact: SchoolAdapter["contact"] | null;
|
|
25
|
+
};
|
|
26
|
+
export type CompareOutput = {
|
|
27
|
+
a: CompareSide;
|
|
28
|
+
b: CompareSide;
|
|
29
|
+
diff: {
|
|
30
|
+
labels: {
|
|
31
|
+
only_a: string[];
|
|
32
|
+
only_b: string[];
|
|
33
|
+
both: string[];
|
|
34
|
+
};
|
|
35
|
+
ruanke_rank_delta: number | null;
|
|
36
|
+
province_score_delta?: Record<string, number>;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
export declare function compare(queryA: string, queryB: string, focusProvince?: ProvinceId): CompareOutput;
|
package/dist/compare.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// compare — side-by-side comparison of two schools across all surface dimensions.
|
|
2
|
+
//
|
|
3
|
+
// Pulls from the local school-index (for rank/labels/historical scores) and the
|
|
4
|
+
// schools-adapters dataset (for 招生网/special programs). No network.
|
|
5
|
+
//
|
|
6
|
+
// 22 of the 100 candidate-validator agents drafted "A vs B" questions; this
|
|
7
|
+
// verb gives Claude a single call that returns everything those questions need.
|
|
8
|
+
import { loadIndex } from "./index-loader.js";
|
|
9
|
+
import { findSchoolAdapter } from "./datasets.js";
|
|
10
|
+
import { PROVINCES, TRACK_NAMES } from "./codes.js";
|
|
11
|
+
import { resolveAlias } from "./aliases.js";
|
|
12
|
+
function findSchool(rows, query) {
|
|
13
|
+
const canonical = resolveAlias(query);
|
|
14
|
+
// exact alias → exact name match
|
|
15
|
+
if (canonical !== query) {
|
|
16
|
+
const exact = rows.find((r) => r.name === canonical);
|
|
17
|
+
if (exact)
|
|
18
|
+
return exact;
|
|
19
|
+
}
|
|
20
|
+
// exact zs_code
|
|
21
|
+
const byCode = rows.find((r) => r.zs_code === query);
|
|
22
|
+
if (byCode)
|
|
23
|
+
return byCode;
|
|
24
|
+
// exact name
|
|
25
|
+
const byExact = rows.find((r) => r.name === query);
|
|
26
|
+
if (byExact)
|
|
27
|
+
return byExact;
|
|
28
|
+
// substring, prefer shortest name
|
|
29
|
+
const substr = rows.filter((r) => r.name.includes(query)).sort((a, b) => a.name.length - b.name.length);
|
|
30
|
+
return substr[0];
|
|
31
|
+
}
|
|
32
|
+
function buildSide(query, focusProvince) {
|
|
33
|
+
const index = loadIndex();
|
|
34
|
+
const row = findSchool(index.rows, query);
|
|
35
|
+
if (!row)
|
|
36
|
+
throw new Error(`no school matched "${query}". Try the full Chinese name or zs_code; supported aliases: 清华/北大/复旦/上交/浙大/南大/中科大/哈工大/西交/人大/北航/北理/...`);
|
|
37
|
+
const adapter = findSchoolAdapter(row.zs_code) ?? findSchoolAdapter(row.name);
|
|
38
|
+
const labels = [];
|
|
39
|
+
if (row.f985)
|
|
40
|
+
labels.push("985");
|
|
41
|
+
if (row.f211)
|
|
42
|
+
labels.push("211");
|
|
43
|
+
if (row.dual_class === "双一流")
|
|
44
|
+
labels.push("双一流");
|
|
45
|
+
const rank = {
|
|
46
|
+
ruanke: null,
|
|
47
|
+
qsWorld: null,
|
|
48
|
+
usNews: null,
|
|
49
|
+
xyh: null
|
|
50
|
+
};
|
|
51
|
+
// recent min scores: if focusProvince supplied, only that province; else top 5 provinces.
|
|
52
|
+
const recentMinScores = {};
|
|
53
|
+
const provincesToShow = focusProvince ? [String(focusProvince)] : ["11", "31", "44", "41", "37"];
|
|
54
|
+
for (const provId of provincesToShow) {
|
|
55
|
+
const entries = row.pro_type_min?.[provId] ?? [];
|
|
56
|
+
const flat = [];
|
|
57
|
+
for (const e of entries) {
|
|
58
|
+
for (const [t, v] of Object.entries(e.type ?? {})) {
|
|
59
|
+
const n = Number(v);
|
|
60
|
+
if (Number.isFinite(n) && n > 0) {
|
|
61
|
+
flat.push({ year: e.year, track: TRACK_NAMES[t] ?? t, min: n });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (flat.length > 0) {
|
|
66
|
+
flat.sort((x, y) => y.year - x.year);
|
|
67
|
+
const provName = PROVINCES[Number(provId)]?.name ?? provId;
|
|
68
|
+
recentMinScores[provName] = flat.slice(0, 6);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
name: row.name,
|
|
73
|
+
zsCode: row.zs_code,
|
|
74
|
+
province: row.province,
|
|
75
|
+
city: row.city,
|
|
76
|
+
belong: row.belong,
|
|
77
|
+
labels,
|
|
78
|
+
rank,
|
|
79
|
+
xuekePinggu: {},
|
|
80
|
+
recentMinScores,
|
|
81
|
+
zswUrl: adapter?.zsw_url ?? null,
|
|
82
|
+
programs: adapter?.programs ?? null,
|
|
83
|
+
contact: adapter?.contact ?? null
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export function compare(queryA, queryB, focusProvince) {
|
|
87
|
+
const a = buildSide(queryA, focusProvince);
|
|
88
|
+
const b = buildSide(queryB, focusProvince);
|
|
89
|
+
const labelsA = new Set(a.labels);
|
|
90
|
+
const labelsB = new Set(b.labels);
|
|
91
|
+
const both = a.labels.filter((l) => labelsB.has(l));
|
|
92
|
+
const onlyA = a.labels.filter((l) => !labelsB.has(l));
|
|
93
|
+
const onlyB = b.labels.filter((l) => !labelsA.has(l));
|
|
94
|
+
// Province-score delta (latest matching year per shown province)
|
|
95
|
+
const provinceScoreDelta = {};
|
|
96
|
+
for (const prov of Object.keys(a.recentMinScores)) {
|
|
97
|
+
const aLatest = a.recentMinScores[prov]?.[0];
|
|
98
|
+
const bLatest = b.recentMinScores[prov]?.[0];
|
|
99
|
+
if (aLatest && bLatest && aLatest.track === bLatest.track && aLatest.year === bLatest.year) {
|
|
100
|
+
provinceScoreDelta[prov] = aLatest.min - bLatest.min;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
a,
|
|
105
|
+
b,
|
|
106
|
+
diff: {
|
|
107
|
+
labels: { only_a: onlyA, only_b: onlyB, both },
|
|
108
|
+
ruanke_rank_delta: null,
|
|
109
|
+
province_score_delta: provinceScoreDelta
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export type ProgramFlag = {
|
|
2
|
+
offers: boolean | null;
|
|
3
|
+
url: string | null;
|
|
4
|
+
};
|
|
5
|
+
export type GaoxiaoZhuanxiang = {
|
|
6
|
+
offers: boolean | null;
|
|
7
|
+
name: string | null;
|
|
8
|
+
url: string | null;
|
|
9
|
+
};
|
|
10
|
+
export type SchoolAdapter = {
|
|
11
|
+
name: string;
|
|
12
|
+
zs_code: string;
|
|
13
|
+
zsw_url: string | null;
|
|
14
|
+
programs: {
|
|
15
|
+
qiangji: ProgramFlag;
|
|
16
|
+
zonghepingjia: ProgramFlag;
|
|
17
|
+
zhongwai_hezuo: ProgramFlag;
|
|
18
|
+
guojia_zhuanxiang: boolean | null;
|
|
19
|
+
gaoxiao_zhuanxiang: GaoxiaoZhuanxiang;
|
|
20
|
+
minzu_ban: boolean | null;
|
|
21
|
+
yuke_ban: boolean | null;
|
|
22
|
+
gao_shui_yundong: boolean | null;
|
|
23
|
+
high_art: boolean | null;
|
|
24
|
+
};
|
|
25
|
+
contact: {
|
|
26
|
+
phone: string | null;
|
|
27
|
+
email: string | null;
|
|
28
|
+
};
|
|
29
|
+
note?: string;
|
|
30
|
+
};
|
|
31
|
+
export type SchoolsAdaptersFile = {
|
|
32
|
+
schools: SchoolAdapter[];
|
|
33
|
+
};
|
|
34
|
+
export type ProvinceSpecialty = {
|
|
35
|
+
province: string;
|
|
36
|
+
year: number;
|
|
37
|
+
data_quality?: "verified" | "partial";
|
|
38
|
+
tiqian?: {
|
|
39
|
+
types?: string[];
|
|
40
|
+
rules?: string;
|
|
41
|
+
};
|
|
42
|
+
qiangji_implementing?: string[];
|
|
43
|
+
zonghepingjia_implementing?: string[];
|
|
44
|
+
guojia_zhuanxiang?: boolean;
|
|
45
|
+
gaoxiao_zhuanxiang?: boolean;
|
|
46
|
+
difang_zhuanxiang?: boolean;
|
|
47
|
+
source?: string[];
|
|
48
|
+
};
|
|
49
|
+
export type CrossProvincePrograms = {
|
|
50
|
+
guojia_zhuanxiang?: Record<string, unknown>;
|
|
51
|
+
gaoxiao_zhuanxiang?: Record<string, unknown>;
|
|
52
|
+
difang_zhuanxiang?: Record<string, unknown>;
|
|
53
|
+
gangaotai_lianzhao?: Record<string, unknown>;
|
|
54
|
+
};
|
|
55
|
+
export type ProvincesSpecialtyFile = {
|
|
56
|
+
provinces: ProvinceSpecialty[];
|
|
57
|
+
cross_province_special_programs: CrossProvincePrograms;
|
|
58
|
+
};
|
|
59
|
+
export declare function loadSchoolsAdapters(): SchoolsAdaptersFile;
|
|
60
|
+
export declare function loadProvincesSpecialty(): ProvincesSpecialtyFile;
|
|
61
|
+
export declare function findSchoolAdapter(query: string): SchoolAdapter | null;
|
|
62
|
+
export declare function listSchoolsOfferingProgram(program: "qiangji" | "zonghepingjia" | "zhongwai_hezuo" | "guojia_zhuanxiang" | "gaoxiao_zhuanxiang" | "minzu_ban" | "yuke_ban" | "gao_shui_yundong" | "high_art"): SchoolAdapter[];
|
|
63
|
+
export declare function findProvinceSpecialty(provinceKey: string): ProvinceSpecialty | null;
|
|
64
|
+
export declare function listProvinceKeys(): string[];
|
|
65
|
+
export declare function getCrossProvincePrograms(): CrossProvincePrograms;
|
package/dist/datasets.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Loaders for the static datasets shipped in cli/data/datasets/.
|
|
2
|
+
// These are the human-curated artifacts produced by the multi-agent fan-outs:
|
|
3
|
+
// schools-adapters-2024.json: per-school 招生网 URL + 强基/综评/中外/专项/民族/预科/运动/艺术 flags
|
|
4
|
+
// provinces-specialty-2024.json: per-province 提前批 + cross-province 国家/高校/地方专项 + 港澳台联招
|
|
5
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
6
|
+
import { resolve, dirname } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const SRC_DIR = dirname(__filename);
|
|
10
|
+
const CANDIDATE_DATA_DIRS = [
|
|
11
|
+
resolve(SRC_DIR, "..", "data", "datasets"),
|
|
12
|
+
resolve(SRC_DIR, "..", "..", "data", "datasets")
|
|
13
|
+
];
|
|
14
|
+
function findDir() {
|
|
15
|
+
for (const d of CANDIDATE_DATA_DIRS) {
|
|
16
|
+
if (existsSync(d))
|
|
17
|
+
return d;
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
function load(filename) {
|
|
22
|
+
const dir = findDir();
|
|
23
|
+
if (!dir)
|
|
24
|
+
return null;
|
|
25
|
+
const path = resolve(dir, filename);
|
|
26
|
+
if (!existsSync(path))
|
|
27
|
+
return null;
|
|
28
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
29
|
+
}
|
|
30
|
+
// ---- Caches ----
|
|
31
|
+
let schoolsCache = null;
|
|
32
|
+
let provincesCache = null;
|
|
33
|
+
export function loadSchoolsAdapters() {
|
|
34
|
+
if (schoolsCache)
|
|
35
|
+
return schoolsCache;
|
|
36
|
+
const data = load("schools-adapters-2024.json");
|
|
37
|
+
if (!data)
|
|
38
|
+
throw new Error("schools-adapters-2024.json not found in cli/data/datasets/");
|
|
39
|
+
schoolsCache = data;
|
|
40
|
+
return data;
|
|
41
|
+
}
|
|
42
|
+
export function loadProvincesSpecialty() {
|
|
43
|
+
if (provincesCache)
|
|
44
|
+
return provincesCache;
|
|
45
|
+
const data = load("provinces-specialty-2024.json");
|
|
46
|
+
if (!data)
|
|
47
|
+
throw new Error("provinces-specialty-2024.json not found in cli/data/datasets/");
|
|
48
|
+
provincesCache = data;
|
|
49
|
+
return data;
|
|
50
|
+
}
|
|
51
|
+
// ---- Queries ----
|
|
52
|
+
export function findSchoolAdapter(query) {
|
|
53
|
+
const file = loadSchoolsAdapters();
|
|
54
|
+
const q = query.trim();
|
|
55
|
+
// try zs_code exact match first
|
|
56
|
+
const byCode = file.schools.find((s) => s.zs_code === q);
|
|
57
|
+
if (byCode)
|
|
58
|
+
return byCode;
|
|
59
|
+
// then by name substring (case-insensitive on Chinese)
|
|
60
|
+
return file.schools.find((s) => s.name.includes(q)) ?? null;
|
|
61
|
+
}
|
|
62
|
+
export function listSchoolsOfferingProgram(program) {
|
|
63
|
+
const file = loadSchoolsAdapters();
|
|
64
|
+
return file.schools.filter((s) => {
|
|
65
|
+
const v = s.programs[program];
|
|
66
|
+
if (typeof v === "boolean")
|
|
67
|
+
return v === true;
|
|
68
|
+
if (v && typeof v === "object" && "offers" in v)
|
|
69
|
+
return v.offers === true;
|
|
70
|
+
return false;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
export function findProvinceSpecialty(provinceKey) {
|
|
74
|
+
const file = loadProvincesSpecialty();
|
|
75
|
+
return file.provinces.find((p) => p.province === provinceKey) ?? null;
|
|
76
|
+
}
|
|
77
|
+
export function listProvinceKeys() {
|
|
78
|
+
return loadProvincesSpecialty().provinces.map((p) => p.province);
|
|
79
|
+
}
|
|
80
|
+
export function getCrossProvincePrograms() {
|
|
81
|
+
return loadProvincesSpecialty().cross_province_special_programs;
|
|
82
|
+
}
|
package/dist/find.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type IndexFilter } from "./index-loader.js";
|
|
2
|
+
import { type ProvinceId } from "./codes.js";
|
|
3
|
+
export type FindInput = {
|
|
4
|
+
keyword: string;
|
|
5
|
+
provinceId: ProvinceId;
|
|
6
|
+
year: number;
|
|
7
|
+
filter?: IndexFilter;
|
|
8
|
+
limit?: number;
|
|
9
|
+
concurrency?: number;
|
|
10
|
+
};
|
|
11
|
+
export type FindHit = {
|
|
12
|
+
schoolId: number;
|
|
13
|
+
zsCode: string;
|
|
14
|
+
schoolName: string;
|
|
15
|
+
schoolCity: string;
|
|
16
|
+
is985: boolean;
|
|
17
|
+
is211: boolean;
|
|
18
|
+
dualClass: string;
|
|
19
|
+
spcode: string;
|
|
20
|
+
spname: string;
|
|
21
|
+
sp_name: string;
|
|
22
|
+
num: number;
|
|
23
|
+
tuition: string;
|
|
24
|
+
length: string;
|
|
25
|
+
batch: string;
|
|
26
|
+
track: string;
|
|
27
|
+
xuanke: {
|
|
28
|
+
first: string | null;
|
|
29
|
+
reselect: string | null;
|
|
30
|
+
raw: string | null;
|
|
31
|
+
};
|
|
32
|
+
category: string;
|
|
33
|
+
info: string | null;
|
|
34
|
+
};
|
|
35
|
+
export type FindOutput = {
|
|
36
|
+
query: {
|
|
37
|
+
keyword: string;
|
|
38
|
+
province: {
|
|
39
|
+
id: ProvinceId;
|
|
40
|
+
name: string;
|
|
41
|
+
};
|
|
42
|
+
year: number;
|
|
43
|
+
filter?: IndexFilter;
|
|
44
|
+
};
|
|
45
|
+
schoolsScanned: number;
|
|
46
|
+
hits: FindHit[];
|
|
47
|
+
};
|
|
48
|
+
export declare function find(input: FindInput): Promise<FindOutput>;
|
package/dist/find.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// find — search for a major keyword across schools in a province for a given year.
|
|
2
|
+
// Concurrent fetches against schoolspecialplan endpoint.
|
|
3
|
+
import { loadIndex, filterIndex } from "./index-loader.js";
|
|
4
|
+
import { getAdmissionPlan } from "./gaokao-cn.js";
|
|
5
|
+
import { PROVINCES, TRACK_NAMES } from "./codes.js";
|
|
6
|
+
function matchItem(item, keyword) {
|
|
7
|
+
const k = keyword.toLowerCase();
|
|
8
|
+
return (item.sp_name?.toLowerCase().includes(k) ||
|
|
9
|
+
item.spname?.toLowerCase().includes(k) ||
|
|
10
|
+
item.level3_name?.toLowerCase().includes(k) ||
|
|
11
|
+
item.spcode?.toLowerCase().includes(k) ||
|
|
12
|
+
false);
|
|
13
|
+
}
|
|
14
|
+
function toHit(row, item) {
|
|
15
|
+
return {
|
|
16
|
+
schoolId: row.gaokao_cn_id,
|
|
17
|
+
zsCode: row.zs_code,
|
|
18
|
+
schoolName: row.name,
|
|
19
|
+
schoolCity: row.city,
|
|
20
|
+
is985: row.f985,
|
|
21
|
+
is211: row.f211,
|
|
22
|
+
dualClass: row.dual_class,
|
|
23
|
+
spcode: item.spcode,
|
|
24
|
+
spname: item.spname,
|
|
25
|
+
sp_name: item.sp_name,
|
|
26
|
+
num: item.num,
|
|
27
|
+
tuition: item.tuition,
|
|
28
|
+
length: item.length,
|
|
29
|
+
batch: item.local_batch_name,
|
|
30
|
+
track: TRACK_NAMES[item.type] ?? item.type,
|
|
31
|
+
xuanke: {
|
|
32
|
+
first: item.sp_fxk || item.sg_fxk || null,
|
|
33
|
+
reselect: item.sp_sxk || item.sg_sxk || null,
|
|
34
|
+
raw: item.sp_xuanke || item.sg_xuanke || null
|
|
35
|
+
},
|
|
36
|
+
category: `${item.level2_name} · ${item.level3_name}`,
|
|
37
|
+
info: item.info || item.remark || null
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export async function find(input) {
|
|
41
|
+
const index = loadIndex();
|
|
42
|
+
let rows = index.rows;
|
|
43
|
+
if (input.filter) {
|
|
44
|
+
rows = filterIndex({ generated_at: index.generated_at, rows }, input.filter);
|
|
45
|
+
}
|
|
46
|
+
// Only consider schools known to recruit in the target province (have pro_type_min entry).
|
|
47
|
+
rows = rows.filter((r) => Array.isArray(r.pro_type_min?.[String(input.provinceId)]));
|
|
48
|
+
const concurrency = input.concurrency ?? 12;
|
|
49
|
+
const hits = [];
|
|
50
|
+
let cursor = 0;
|
|
51
|
+
const workers = Array.from({ length: concurrency }, async () => {
|
|
52
|
+
while (cursor < rows.length) {
|
|
53
|
+
const row = rows[cursor++];
|
|
54
|
+
if (!row)
|
|
55
|
+
break;
|
|
56
|
+
try {
|
|
57
|
+
const plan = await getAdmissionPlan(row.gaokao_cn_id, input.year, input.provinceId);
|
|
58
|
+
for (const item of plan) {
|
|
59
|
+
if (matchItem(item, input.keyword))
|
|
60
|
+
hits.push(toHit(row, item));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// skip on error
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
await Promise.all(workers);
|
|
69
|
+
hits.sort((a, b) => {
|
|
70
|
+
// Prefer 985 > 211 > 双一流 > rest, then by plan count desc.
|
|
71
|
+
const ra = (a.is985 ? 3 : 0) + (a.is211 ? 2 : 0) + (a.dualClass === "双一流" ? 1 : 0);
|
|
72
|
+
const rb = (b.is985 ? 3 : 0) + (b.is211 ? 2 : 0) + (b.dualClass === "双一流" ? 1 : 0);
|
|
73
|
+
if (ra !== rb)
|
|
74
|
+
return rb - ra;
|
|
75
|
+
return b.num - a.num;
|
|
76
|
+
});
|
|
77
|
+
return {
|
|
78
|
+
query: {
|
|
79
|
+
keyword: input.keyword,
|
|
80
|
+
province: { id: input.provinceId, name: PROVINCES[input.provinceId].name },
|
|
81
|
+
year: input.year,
|
|
82
|
+
filter: input.filter
|
|
83
|
+
},
|
|
84
|
+
schoolsScanned: rows.length,
|
|
85
|
+
hits: input.limit ? hits.slice(0, input.limit) : hits
|
|
86
|
+
};
|
|
87
|
+
}
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { RecommendOutput } from "./recommend.js";
|
|
2
|
+
export declare function isTty(): boolean;
|
|
3
|
+
export declare function renderTable(rows: string[][], opts?: {
|
|
4
|
+
gap?: number;
|
|
5
|
+
}): string;
|
|
6
|
+
export declare function formatRecommend(out: RecommendOutput, opts?: {
|
|
7
|
+
explain?: boolean;
|
|
8
|
+
}): string;
|
|
9
|
+
export declare function formatTop(rows: Array<{
|
|
10
|
+
schoolName: string;
|
|
11
|
+
baselineMinScore: number;
|
|
12
|
+
delta: number;
|
|
13
|
+
baselineYear: number;
|
|
14
|
+
city: string;
|
|
15
|
+
tags: string;
|
|
16
|
+
belong: string;
|
|
17
|
+
}>): string;
|