File size: 8,154 Bytes
e2cf529
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# -*- coding: utf-8 -*-
"""B3 用途二次分类:1229 条技能 → K/M/T/X/U 五值 → data/skill_usage_labels.csv

用法:
  python scripts/classify_skill_usage.py            # 生成 CSV + 分布回执
  python scripts/classify_skill_usage.py --check    # 复跑字节一致性

判定口径(确定性,无随机、无时间戳):
  stage1 逐类打分:score(c) = Σ(关键词权重 × 命中) + 0.6 × 2~4 字滑窗重叠率
         —— 逐条对各标签打分再取最大,绝不用"整类文本聚合"打分(那样所有条目会塌到同一类)。
         argmax 且 max >= TH 才算判定;否则进 stage2。
  stage2 兜底:按索引侧 domain / mechanism 查表定性;仍无依据则标 U(待判)。
  U 上限:判据要求待判 <= 20,超出即视为分类器失效而不是"数据太怪"。

标签语义:
  K 知识源:提供内容/资料/知识结构,可被检索与引用
  M 方法学:提供方法/模型/算法/流程/范式,可被调用为"怎么做"
  T 工具链:提供可执行的命令、脚本、部署与平台能力
  X 无关:与中西医融合康复域无关,明确不纳入
  U 待判:证据不足,挂账不猜
"""
import csv
import hashlib
import io
import json
import os
import sys

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
INDEX = os.path.join(ROOT, "data", "skill_matrix_index.json")
CSV_OUT = os.path.join(ROOT, "data", "skill_usage_labels.csv")
HEADER = ["name", "domain", "mechanism", "lane", "label", "score", "stage", "reason"]

TH = 0.80
TH_WEAK = 0.35   # 弱证据档:仅用于索引域未归域的条目,低于此判 U 挂账

# 关键词权重:域名词面证据,不做同义词扩张,避免误伤
KW = {
    "K": {"知识": 1.2, "导学": 1.0, "学派": 0.9, "医案": 0.9, "文献": 1.0, "教材": 1.0,
          "经典": 1.0, "讲稿": 0.9, "讲义": 0.9, "传承": 0.8, "综述": 0.8, "词条": 0.8,
          "方剂": 0.7, "本草": 0.7, "内经": 0.8, "伤寒": 0.8, "百科": 0.8, "典籍": 0.8,
          "ontology": 1.0, "知识库": 1.2, "索引": 0.6, "图谱": 0.8, "概览": 0.8,
          "archive": 0.8, "corpus": 0.8, "timeline": 0.6, "规范": 0.7},
    "M": {"方法": 1.2, "方法论": 1.2, "框架": 1.0, "矩阵": 0.9, "体系": 0.9, "机制": 1.0,
          "模型": 1.0, "算法": 1.0, "算子": 1.0, "流程": 1.0, "sop": 1.0, "协议": 0.9,
          "策略": 0.9, "推理": 0.9, "范式": 1.0, "拆解": 0.8, "定位": 0.6, "解析": 0.7,
          "判定": 0.8, "分类": 0.6, "建模": 0.8, "调度": 0.8, "校准": 0.7, "打分": 0.7,
          "design": 0.6, "analysis": 0.6, "pipeline": 0.9, "迭代": 0.7, "打磨": 0.6,
          "车间": 1.0, "匹配": 0.7, "收敛": 0.8, "调度": 0.8},
    "T": {"工具": 1.2, "脚本": 1.2, "cli": 1.2, "命令": 1.0, "操作": 1.0, "基准": 1.2,
          "引导": 0.9, "适配": 1.0, "安装": 1.0, "部署": 1.2, "路由": 1.0, "api": 1.0,
          "mcp": 1.0, "打包": 0.9, "上传": 0.8, "下载": 0.8, "渲染": 0.8, "docker": 1.2,
          "向量库": 0.9, "bridge": 0.9, "热插拔": 0.9, "网关": 0.9, "codec": 0.9,
          "sidecar": 0.9, "harness": 0.9, "质检": 1.2, "检测": 0.6, "读取": 0.7, "编辑": 0.7,
          "交由": 0.8, "自动化": 0.6},
    "X": {"娱乐": 1.0, "星座": 1.0, "彩票": 1.0, "游戏攻略": 1.0, "八卦": 1.0, "影视": 0.8,
          "小说": 0.8, "美食": 0.8, "旅游": 0.8, "穿搭": 0.8, "美妆": 0.8, "宠物": 0.6,
          "股票": 0.6, "量化交易": 0.8, "招聘": 0.6, "房产": 0.6,
          # 农业/养殖/园艺类:与中西医融合康复域无关,属明确不纳入(写进票据,不藏着)
          "养殖": 1.0, "鱼塘": 1.0, "鹅": 0.8, "藻": 0.8, "朴门": 1.0, "果园": 0.8,
          "化肥": 0.8, "农药": 0.8, "蚯蚓": 0.8, "梯级水系": 0.8,
          # 纯想象/猎奇场景:不属于康复域
          "虫洞": 1.0, "星际": 0.8, "探险": 0.8, "趣味": 0.6},
}

# stage2 兜底:索引侧既有字段 → 标签(依据单一,不叠加猜测)
DOMAIN_FALLBACK = {
    "literature": "K", "gaoyi": "K",
    "research": "M", "optimization": "M", "quant": "M",
    "data": "T", "growth": "T",
    "writing": "X", "business": "X",
    "unmapped": "U",
}
MECH_FALLBACK = {
    "meridian": "M", "cytoskeleton": "M",
    "semantic_bus": "T", "skin_v6": "M", "holographic_heartbeat": "M",
    "hebbian_field": "M", "unmapped": "U",
}


def shingles(text, lo=2, hi=4):
    out = set()
    for i in range(len(text)):
        for n in range(lo, hi + 1):
            if i + n <= len(text):
                out.add(text[i:i + n])
    return out


def overlap_rate(a, b):
    if not a or not b:
        return 0.0
    return len(a & b) / float(len(a))


def score_all(text, ref_shingles):
    sc = {}
    low = text.lower()
    for label, kws in KW.items():
        s = 0.0
        for w, weight in kws.items():
            if w in low:
                s += weight
        s += 0.6 * overlap_rate(shingles(text), ref_shingles[label])
        sc[label] = round(s, 4)
    return sc


def refs_by_label():
    return {label: shingles("".join(kws.keys())) for label, kws in KW.items()}


def classify(rec, ref):
    text = (rec.get("name", "") + " " + rec.get("description", "")).strip()
    sc = score_all(text, ref)
    order = ["K", "M", "T", "X"]
    best = max(order, key=lambda k: (sc[k], -order.index(k)))
    if sc[best] >= TH:
        return best, sc[best], "stage1", "kw+shingle argmax={} score={}".format(best, sc[best])
    d = DOMAIN_FALLBACK.get(rec.get("domain"), "U")
    has_domain_evidence = (rec.get("domain_score") or 0) > 0 and rec.get("domain") != "unmapped"
    if d != "U" and has_domain_evidence:
        return d, sc[best], "stage2", "domain 定性={}(索引域有证据,score={})".format(
            d, rec.get("domain_score"))
    if sc[best] >= TH_WEAK:
        return best, sc[best], "stage2-weak", "弱证据 argmax={} score={}(域未归域,仅词面)".format(
            best, sc[best])
    return "U", sc[best], "undecided", "domain={} mechanism={} 证据不足(score={}<{})".format(
        rec.get("domain"), rec.get("mechanism"), sc[best], TH_WEAK)


def build():
    with open(INDEX, encoding="utf-8") as f:
        idx = json.load(f)
    ref = refs_by_label()
    rows = []
    for rec in idx["skills"]:
        label, score, stage, reason = classify(rec, ref)
        rows.append({"name": rec.get("name", ""), "domain": rec.get("domain", ""),
                     "mechanism": rec.get("mechanism", ""), "lane": rec.get("lane", ""),
                     "label": label, "score": "{:.4f}".format(score), "stage": stage,
                     "reason": reason})
    counts = {"K": 0, "M": 0, "T": 0, "X": 0, "U": 0}
    for r in rows:
        counts[r["label"]] += 1
    digest = hashlib.sha256(
        json.dumps(rows, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
    ).hexdigest()[:16]
    return rows, counts, digest


def csv_text(rows):
    buf = io.StringIO()
    w = csv.DictWriter(buf, fieldnames=HEADER, lineterminator="\n")
    w.writeheader()
    for r in rows:
        w.writerow(r)
    return buf.getvalue()


def main(argv):
    rows, counts, digest = build()
    text = csv_text(rows)
    if "--check" in argv:
        if not os.path.isfile(CSV_OUT):
            print(json.dumps({"verdict": "FAIL", "reason": "missing"}, ensure_ascii=False))
            return 1
        with open(CSV_OUT, encoding="utf-8") as f:
            same = f.read() == text
        print(json.dumps({"verdict": "PASS" if same else "FAIL", "byte_consistent": same,
                          "digest": digest, "counts": counts}, ensure_ascii=False))
        return 0 if same else 1
    with open(CSV_OUT, "w", encoding="utf-8", newline="\n") as f:
        f.write(text)
    print(json.dumps({"out": CSV_OUT, "rows": len(rows), "counts": counts,
                      "digest": digest, "sha256": hashlib.sha256(text.encode("utf-8")).hexdigest()},
                     ensure_ascii=False))
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv))