kba-cli 0.3.0rc4__py3-none-any.whl

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.
kba/tags.py ADDED
@@ -0,0 +1,108 @@
1
+ """Three-layer tag/domain inference: frontmatter + path/filename + content keywords."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+
6
+ # Conservative subset of packs/generic/tags.md; only infer a tag when a path/name
7
+ # keyword matches or at least two distinct content keywords are present.
8
+ TAG_RULES = {
9
+ "行业研究": ("行业", "研报", "市场", "竞品", "公司研究", "半导体", "新能源", "消费", "医药", "策略"),
10
+ "财务": ("财务", "估值", "收入", "利润", "现金流", "预算", "成本", "模型", "三张表"),
11
+ "合同": ("合同", "协议", "条款", "法务", "甲方", "乙方"),
12
+ "合规": ("合规", "法规", "审计", "隐私", "监管"),
13
+ "安全": ("安全", "权限", "风控", "密钥", "漏洞"),
14
+ "AI": ("AI", "大模型", "LLM", "RAG", "Agent", "提示词", "机器学习"),
15
+ "数据治理": ("数据治理", "主数据", "数据质量", "数据标准", "数据口径"),
16
+ "指标体系": ("指标", "KPI", "口径", "度量", "报表"),
17
+ "知识库": ("知识库", "知识管理", "检索", "标签体系", "语料"),
18
+ "方案": ("方案", "建设", "规划", "实施"),
19
+ "架构": ("架构", "技术选型", "接口", "部署", "微服务"),
20
+ "需求": ("需求", "PRD", "用户故事", "验收标准"),
21
+ "复盘": ("复盘", "回顾", "总结", "经验"),
22
+ "教程": ("教程", "指南", "操作手册", "步骤"),
23
+ "培训": ("培训", "课程", "内训", "教材"),
24
+ "营销": ("营销", "品牌", "活动", "增长", "投放"),
25
+ "销售": ("销售", "线索", "客户", "转化", "商机"),
26
+ "客户案例": ("案例", "标杆", "客户故事"),
27
+ "项目管理": ("项目", "排期", "里程碑", "风险"),
28
+ "模板": ("模板", "清单", "可复用"),
29
+ }
30
+
31
+ DOMAIN_NAMES = (
32
+ "业务方案", "技术架构", "产品设计", "数据与指标",
33
+ "学习与考试", "内容创作", "个人事务", "参考资料",
34
+ )
35
+
36
+ DOMAIN_BY_TAG = {
37
+ "架构": "技术架构", "接口": "技术架构", "研发": "技术架构", "安全": "技术架构",
38
+ "运维": "技术架构", "AI": "技术架构", "效率工具": "技术架构",
39
+ "产品设计": "产品设计", "用户研究": "产品设计", "需求": "产品设计",
40
+ "数据治理": "数据与指标", "指标体系": "数据与指标", "报表": "数据与指标",
41
+ "教程": "学习与考试", "考试": "学习与考试", "学习笔记": "学习与考试", "培训": "学习与考试",
42
+ "内容创作": "内容创作", "自媒体": "内容创作", "品牌": "内容创作", "营销": "内容创作",
43
+ "求职": "个人事务", "职业发展": "个人事务", "个人效率": "个人事务",
44
+ "方案": "业务方案", "项目管理": "业务方案", "复盘": "业务方案", "方法论": "业务方案",
45
+ "客户案例": "业务方案", "销售": "业务方案", "客户成功": "业务方案",
46
+ "合同": "业务方案", "合规": "业务方案",
47
+ "行业研究": "参考资料", "财务": "参考资料", "参考资料": "参考资料",
48
+ }
49
+
50
+
51
+ def _contains(keyword: str, text: str) -> bool:
52
+ """ASCII keywords need word boundaries; Chinese uses substring."""
53
+ if not keyword:
54
+ return False
55
+ if keyword.isascii():
56
+ return re.search(rf"(?<![a-z0-9]){re.escape(keyword)}(?![a-z0-9])", text, re.I) is not None
57
+ return keyword in text
58
+
59
+
60
+ def infer_metadata(rel: str, name: str, text: str, fm_tags, fm_domain: str = "",
61
+ max_tags: int = 6):
62
+ """Return (tags, sources, inferred_tags, domain, domain_source)."""
63
+ fm_tags = [str(t).strip() for t in (fm_tags or []) if str(t).strip()]
64
+ tags: list[str] = list(dict.fromkeys(fm_tags))
65
+ sources: set[str] = {"frontmatter"} if fm_tags else set()
66
+ inferred: list[str] = []
67
+
68
+ haystack = f"{rel or ''} {name or ''}".lower()
69
+ for tag, keywords in TAG_RULES.items():
70
+ if tag in tags:
71
+ continue
72
+ if any(_contains(k.lower(), haystack) for k in keywords):
73
+ tags.append(tag)
74
+ inferred.append(tag)
75
+ sources.add("path")
76
+
77
+ sample = (text or "")[:60_000].lower()
78
+ for tag, keywords in TAG_RULES.items():
79
+ if tag in tags:
80
+ continue
81
+ hits = sum(1 for k in keywords if _contains(k.lower(), sample))
82
+ if hits >= 2:
83
+ tags.append(tag)
84
+ inferred.append(tag)
85
+ sources.add("content")
86
+
87
+ if len(tags) > max_tags:
88
+ front = [t for t in tags if t in fm_tags]
89
+ extra = [t for t in tags if t not in fm_tags][:max(0, max_tags - len(front))]
90
+ tags = front + extra
91
+ inferred = [t for t in inferred if t in tags]
92
+
93
+ domain = (fm_domain or "").strip()
94
+ domain_source = "frontmatter" if domain else ""
95
+ if not domain:
96
+ for segment in (rel or "").split("/"):
97
+ if segment in DOMAIN_NAMES:
98
+ domain = segment
99
+ domain_source = "path"
100
+ break
101
+ if not domain:
102
+ for tag in tags:
103
+ mapped = DOMAIN_BY_TAG.get(tag)
104
+ if mapped:
105
+ domain = mapped
106
+ domain_source = "tags"
107
+ break
108
+ return tags, sorted(sources), inferred, domain, domain_source
kba/telemetry.py ADDED
@@ -0,0 +1,78 @@
1
+ """Opt-in, aggregated telemetry. No network code in v0.1 — preview only."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import uuid
6
+ from pathlib import Path
7
+
8
+ from . import util
9
+ from .jobs import kba_home
10
+
11
+
12
+ def _cfg_path() -> Path:
13
+ return kba_home() / "config.json"
14
+
15
+
16
+ def _load() -> dict:
17
+ return util.read_json(_cfg_path(), {}) or {}
18
+
19
+
20
+ def enabled() -> bool:
21
+ return bool(_load().get("telemetry", False))
22
+
23
+
24
+ def set_enabled(value: bool) -> dict:
25
+ cfg = _load()
26
+ cfg["telemetry"] = bool(value)
27
+ cfg.setdefault("anon_id", str(uuid.uuid4()))
28
+ util.write_json(_cfg_path(), cfg)
29
+ return cfg
30
+
31
+
32
+ def anon_id() -> str:
33
+ cfg = _load()
34
+ if not cfg.get("anon_id"):
35
+ cfg["anon_id"] = str(uuid.uuid4())
36
+ util.write_json(_cfg_path(), cfg)
37
+ return cfg["anon_id"]
38
+
39
+
40
+ def build_payload(job: Path) -> dict:
41
+ job = Path(job)
42
+ fp = util.read_json(job / "fp.json") or {}
43
+ analysis = util.read_json(job / "analysis.json") or {}
44
+ m = analysis.get("metrics", {})
45
+ s = fp.get("summary", {})
46
+ return {
47
+ "schema": "kba.telemetry/1",
48
+ "anon_id": anon_id(),
49
+ "event": "audit_completed",
50
+ "ts": util.now_iso(),
51
+ "props": {
52
+ "files": s.get("files", 0),
53
+ "bytes": s.get("bytes", 0),
54
+ "by_ext": s.get("by_ext", {}),
55
+ "dup_rate": m.get("dup_rate"),
56
+ "stale_rate": m.get("stale_rate"),
57
+ "tag_coverage": m.get("tag_coverage"),
58
+ "valid_tag_coverage": m.get("valid_tag_coverage"),
59
+ "top5_hit": m.get("top5_hit"),
60
+ "extract_fail": m.get("extract_fail"),
61
+ "health_score": analysis.get("health_score"),
62
+ "kba_version": __import__("kba").__version__,
63
+ },
64
+ }
65
+
66
+
67
+ def record(job: Path) -> None:
68
+ if not enabled():
69
+ return
70
+ payload = build_payload(job)
71
+ q = kba_home() / "telemetry_queue.jsonl"
72
+ q.parent.mkdir(parents=True, exist_ok=True)
73
+ with open(q, "a", encoding="utf-8") as f:
74
+ f.write(json.dumps(payload, ensure_ascii=False) + "\n")
75
+
76
+
77
+ def preview(job: Path) -> dict:
78
+ return build_payload(job)
kba/util.py ADDED
@@ -0,0 +1,204 @@
1
+ """Shared helpers: encoding, frontmatter, file walking, hashing."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import json
6
+ import os
7
+ import re
8
+ import time
9
+ import unicodedata
10
+ from datetime import datetime
11
+ from pathlib import Path
12
+
13
+ TEXT_EXTS = {".md", ".markdown", ".txt", ".rst", ".org"}
14
+ # Binaries with a local text extractor (via `kba extract` sidecars).
15
+ EXTRACTABLE_EXTS = {".pdf", ".docx", ".pptx", ".xlsx"}
16
+ # Legacy binaries that are counted but not content-analysed yet.
17
+ # LibreOffice conversion is a later option; do not silently drop them.
18
+ UNSUPPORTED_TEXT_EXTS = {".ppt", ".xls", ".doc"}
19
+ BINARY_EXTS = EXTRACTABLE_EXTS | UNSUPPORTED_TEXT_EXTS
20
+ SUPPORTED_EXTS = TEXT_EXTS | BINARY_EXTS
21
+ DEFAULT_EXCLUDE = {
22
+ "_index", "_extract", "_hub", "_organize", ".git", "__pycache__",
23
+ "node_modules", ".venv", "venv", "dist", "build",
24
+ }
25
+ DEFAULT_TEXT_TTL_DAYS = 7
26
+
27
+
28
+ def now_iso() -> str:
29
+ return datetime.now().astimezone().isoformat(timespec="seconds")
30
+
31
+
32
+ def text_ttl_days() -> int:
33
+ """Days to keep job/text/ content; 0 disables retention (purge on maintenance)."""
34
+ raw = os.environ.get("KBA_TEXT_TTL_DAYS")
35
+ try:
36
+ return max(0, int(raw)) if raw not in (None, "") else DEFAULT_TEXT_TTL_DAYS
37
+ except (TypeError, ValueError):
38
+ return DEFAULT_TEXT_TTL_DAYS
39
+
40
+
41
+ def ext_kind(ext: str) -> str:
42
+ """text | extractable | unsupported | other."""
43
+ ext = (ext or "").lower()
44
+ if ext in TEXT_EXTS:
45
+ return "text"
46
+ if ext in EXTRACTABLE_EXTS:
47
+ return "extractable"
48
+ if ext in UNSUPPORTED_TEXT_EXTS:
49
+ return "unsupported"
50
+ return "other"
51
+
52
+
53
+ FINAL_MARK_RE = re.compile(r"终稿|定稿|终版|发布版|已发布|final(?:ized)?", re.I)
54
+ DRAFT_MARK_RE = re.compile(r"底稿|草稿|初稿|过程稿|中间稿|draft|wip|working", re.I)
55
+ VERSION_NUM_RE = re.compile(r"(?:^|[\s_\-((])v(?:er)?\.?\s?(\d+(?:\.\d+)*)", re.I)
56
+
57
+
58
+ def version_stage(name: str, rel: str = "") -> str:
59
+ """final | draft | normal, inferred from filename/path markers only."""
60
+ s = f"{rel or ''} {name or ''}"
61
+ if DRAFT_MARK_RE.search(s):
62
+ return "draft"
63
+ if FINAL_MARK_RE.search(s):
64
+ return "final"
65
+ return "normal"
66
+
67
+
68
+ def version_number(name: str, rel: str = "") -> str:
69
+ m = VERSION_NUM_RE.search(f"{rel or ''} {name or ''}")
70
+ return m.group(1) if m else ""
71
+
72
+
73
+ def text_status_of(rec: dict, extraction_enabled: bool) -> str:
74
+ """Per-file extraction state, tolerating fingerprints written before P0."""
75
+ st = rec.get("text_status")
76
+ if st:
77
+ return st
78
+ kind = ext_kind(rec.get("ext", ""))
79
+ if kind == "text":
80
+ return "text"
81
+ if kind == "unsupported":
82
+ return "unsupported"
83
+ if rec.get("has_text"):
84
+ return "extracted"
85
+ return "not_enabled" if not extraction_enabled else "failed"
86
+
87
+
88
+ def read_text(path: Path) -> str:
89
+ """BOM-aware UTF-8/UTF-16, then strict gb18030/big5/shift_jis; else empty."""
90
+ try:
91
+ raw = Path(path).read_bytes()
92
+ except OSError:
93
+ return ""
94
+ if raw.startswith((b"\xff\xfe", b"\xfe\xff")):
95
+ try:
96
+ return raw.decode("utf-16")
97
+ except UnicodeDecodeError:
98
+ pass
99
+ for enc in ("utf-8-sig", "gb18030", "big5", "shift_jis"):
100
+ try:
101
+ return raw.decode(enc)
102
+ except (UnicodeDecodeError, UnicodeError):
103
+ continue
104
+ return ""
105
+
106
+
107
+ FM_RE = re.compile(r"^--- *\r?\n(.*?)\r?\n?--- *\r?\n?(.*)$", re.S)
108
+
109
+
110
+ def parse_frontmatter(text: str):
111
+ m = FM_RE.match(text)
112
+ if not m:
113
+ return {}, text
114
+ raw, body = m.group(1), m.group(2)
115
+ fm = {}
116
+ for line in raw.splitlines():
117
+ if not line.strip() or line.strip().startswith("#") or ":" not in line:
118
+ continue
119
+ k, v = line.split(":", 1)
120
+ k, v = k.strip(), v.strip().strip("'\"").strip()
121
+ if v.startswith("[") and v.endswith("]"):
122
+ v = [x.strip().strip("'\"") for x in v[1:-1].split(",") if x.strip()]
123
+ fm[k] = v
124
+ return fm, body
125
+
126
+
127
+ def iter_files(root: Path, exclude=None, exts=None):
128
+ root = Path(root).resolve()
129
+ exclude = set(exclude or DEFAULT_EXCLUDE)
130
+ exts = {e.lower() for e in (exts or SUPPORTED_EXTS)}
131
+ for dirpath, dirnames, filenames in os.walk(root):
132
+ dirnames[:] = sorted(d for d in dirnames if d not in exclude)
133
+ for fn in sorted(filenames):
134
+ # Office lock/temp files (~$deck.pptx) are not real documents.
135
+ if fn.startswith("~$"):
136
+ continue
137
+ p = Path(dirpath) / fn
138
+ if p.suffix.lower() not in exts:
139
+ continue
140
+ if p.is_symlink():
141
+ try:
142
+ if root not in p.resolve().parents and p.resolve() != root:
143
+ continue
144
+ except OSError:
145
+ continue
146
+ yield p
147
+
148
+
149
+ def sha256_file(path: Path) -> str:
150
+ h = hashlib.sha256()
151
+ with open(path, "rb") as f:
152
+ for chunk in iter(lambda: f.read(1 << 20), b""):
153
+ h.update(chunk)
154
+ return h.hexdigest()
155
+
156
+
157
+ def human_bytes(n: float) -> str:
158
+ for unit in ("B", "KB", "MB", "GB", "TB"):
159
+ if abs(n) < 1024.0:
160
+ return f"{n:.1f}{unit}"
161
+ n /= 1024.0
162
+ return f"{n:.1f}PB"
163
+
164
+
165
+ def normalize_name(name: str) -> str:
166
+ s = unicodedata.normalize("NFKC", name.lower())
167
+ s = re.sub(r"__dup\d+", "", s)
168
+ s = re.sub(r"[\s\-_()()\[\]【】]+", "", s)
169
+ return s
170
+
171
+
172
+ def write_json(path: Path, obj, indent: int = 2) -> None:
173
+ path = Path(path)
174
+ path.parent.mkdir(parents=True, exist_ok=True)
175
+ tmp = path.with_name(f".{path.name}.{os.getpid()}.{time.time_ns()}.tmp")
176
+ tmp.write_text(json.dumps(obj, ensure_ascii=False, indent=indent), encoding="utf-8")
177
+ # Windows can briefly lock a JSON target (AV/indexer); retry the atomic replace.
178
+ for attempt in range(8):
179
+ try:
180
+ tmp.replace(path)
181
+ break
182
+ except PermissionError:
183
+ if attempt == 7:
184
+ raise
185
+ time.sleep(0.02)
186
+
187
+
188
+ def read_json(path: Path, default=None):
189
+ try:
190
+ return json.loads(Path(path).read_text(encoding="utf-8"))
191
+ except (OSError, ValueError):
192
+ return default
193
+
194
+
195
+ def rel_posix(path: Path, root: Path) -> str:
196
+ return Path(path).resolve().relative_to(Path(root).resolve()).as_posix()
197
+
198
+
199
+ def safe_name(rel: str) -> str:
200
+ return hashlib.sha256(rel.encode("utf-8")).hexdigest()[:12]
201
+
202
+
203
+ def mtime_days_ago(ts: float) -> float:
204
+ return max(0.0, (datetime.now().timestamp() - ts) / 86400.0)
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.4
2
+ Name: kba-cli
3
+ Version: 0.3.0rc4
4
+ Summary: KBA - read-only knowledge base auditor (local-first)
5
+ Author: KBA
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Provides-Extra: extract
11
+ Requires-Dist: pypdf>=4.0; extra == "extract"
12
+ Requires-Dist: python-docx>=1.1; extra == "extract"
13
+ Requires-Dist: python-pptx>=0.6.23; extra == "extract"
14
+ Requires-Dist: openpyxl>=3.1; extra == "extract"
15
+ Dynamic: license-file
16
+
17
+ # 知库体检(KBA)
18
+
19
+ > 只读体检本地知识库:健康分、重复裁决建议、治理计划;数据不出本机。
20
+ > 独立项目,不修改 `D:\content_hub`;复用其算法与规则(独立实现)。
21
+
22
+ ## 状态(2026-11-05)
23
+
24
+ **v0.3.0rc4(内部预发布):P0 报告诚实层 + P1 抽取层 + P1 准确性前两批 + 抽取并行/TTL + 标注评测。**
25
+ **Gate 1 外部语料验证未通过,不能对外称稳定版或“完整审阅”。**
26
+
27
+ - 真实内部语料(215 文件 / 58.2MB)只读跑通:抽取 110/113(失败 0、no_text 3)、文本覆盖 97.4%、健康 80、重复 2 组、标签有效覆盖 75.8%、自检索 48%、owner 0%;仍是单个内部语料,不是 Gate 1
28
+ - 真实语料 54 文件 AI 辅助标注样本:抽取 97.35%、PII 8/8;近重复/同名/页级标签由预测簇反推,仅验证链路;标签 exact 0、Jaccard 0.13,说明推断标签过标,需人工校准
29
+ - P0:A/B/C 数据置信度、健康分按可用指标归一化并显示权重覆盖度、抽取三态、Office 计入盘点、正文缓存 TTL 与 `--purge-text`、报告“数据覆盖与结论适用范围”;C 级元数据报告不展示综合健康分
30
+ - P1 抽取:`kba extract` 用 pypdf / python-docx / python-pptx / openpyxl 抽取 PDF、Word、PPT、Excel 正文;Excel 结构指纹(sheet/公式模式/named range)识别“同模型不同版本”;旧版 `.ppt/.xls/.doc` 显式标 unsupported
31
+ - P1 准确性:PDF 去页眉页脚、PDF/PPT 页级切分、跨文档页级重合(不自动合并)、终稿/底稿信号 + 正文字数/相似度差异、人工裁决队列、PII 正则初筛(仅计数)、标签/领域三层推断(frontmatter + 路径/文件名 + 内容)、负责人覆盖与本地断链检测、`kba clean --purge-extract`
32
+ - P1 运维:`kba extract --workers` 并行抽取、默认 7 天 sidecar TTL、`tools/bench_scan.py` 合成基准
33
+ - 评测:`kba eval` 读取标注 JSON,输出近重复/同名/页级/版本/PII 的 P/R/F1、标签 exact/Jaccard、抽取成功率;合成语料只证明回归,不代表外部准确率
34
+ - 本地连接器:免费 `connector-local`(kba-local)+ `kba scan-summary`;付费专家依赖它,连接器不可用才回退粘贴摘要
35
+ - 产出:[demo/report.html](demo/report.html)、[demo/share.md](demo/share.md)(均为 P0 前版本,重新生成后含新章节)
36
+ - 38 个测试通过(P0 验收 + 抽取/结构指纹/失败态/UTF-16/页级/版本/PII/标签/断链/purge/并行/TTL/评测)
37
+ - **免费入口已上线**:专家 v0.1.4 审核通过、平台可召唤,prompt-only,不依赖 CLI;**付费未开通**,付费专家/连接器包已就绪但未提交平台
38
+
39
+ ## 快速开始
40
+
41
+ ```bash
42
+ cd D:\kba
43
+ pip install -e ".[extract]" # 含 PDF/Office 抽取依赖;不装也能跑元数据模式
44
+ # 或安装内部预发布 wheel(本地构建,不对外发布):
45
+ # python -m pip install "dist/kba_cli-0.3.0rc4-py3-none-any.whl[extract]"
46
+
47
+ python -m kba doctor # 环境自检
48
+ python -m kba extract --root "D:\你的资料" --out "D:\kba-extract"
49
+ python -m kba audit start --root "D:\你的资料" --extract-dir "D:\kba-extract" --foreground
50
+ python -m kba audit analyze
51
+ python -m kba audit report --out report.html
52
+ python -m kba audit share --out share.md
53
+ python -m kba eval --root "D:\你的资料" --labels labels.json --json # 标注语料评测
54
+ python -m kba clean --purge-text # 立即清理 job/text 正文缓存(默认 TTL 7 天)
55
+ python -m kba clean --purge-extract --dir "D:\kba-extract" # 清理抽取 sidecar
56
+ ```
57
+
58
+ 只做元数据盘点时可跳过 `kba extract` 与 `--extract-dir`;报告会标 C 级且不展示综合健康分。
59
+
60
+ 平台内使用(WorkBuddy):
61
+
62
+ ```
63
+ 连接器:connector/(CLI + Skill 方案)
64
+ 技能:kb-audit / kb-clusters / kb-taxonomy
65
+ ```
66
+
67
+ ## 目录
68
+
69
+ ```
70
+ kba/
71
+ ├── kba/ # Python 包(cli/doctor/jobs/fingerprint/extract/tags/links/pii/analyze/report/telemetry)
72
+ ├── connector/ # WorkBuddy 发布包(meta / cli.json / 3 个 SKILL)
73
+ ├── plugin-free/ # 免费引导插件包(技能入口:.codebuddy-plugin + skills)
74
+ ├── plugin-free-expert/ # 免费引导专家包(专家入口:plugin.json + agents + avatar)
75
+ ├── plugin-paid-expert/ # 付费专家包 v0.1.0(报告解读;不读本地文件)
76
+ ├── plugin-paid/ # ¥1 付费试用插件包(旧方案)
77
+ ├── packs/generic/ # 通用行业包(taxonomy/tags/rules/report)
78
+ ├── tests/ # smoke tests
79
+ ├── tools/ # vendor 同步 / 插件打包 / 提审检查 / 合成基准
80
+ ├── demo/ # 真实语料报告(本地生成,不入库)
81
+ └── docs/ # PRD、平台 6 问、访谈提纲、定价实验
82
+ ```
83
+
84
+ ## 文档
85
+
86
+ | 文档 | 说明 |
87
+ |---|---|
88
+ | [CHANGELOG.md](CHANGELOG.md) | 版本变更与预发布说明(0.3.0rc4) |
89
+ | [docs/eval-labels.md](docs/eval-labels.md) | `kba eval` 标注格式、指标口径与 Gate 1 用法 |
90
+ | [docs/free-entry-runbook.md](docs/free-entry-runbook.md) | 免费专家 v0.1.4 召唤验收、冻结规则与反馈表 |
91
+ | [plugin-paid-expert/README.md](plugin-paid-expert/README.md) | 付费专家包 v0.1.0:定位、发布字段与提审检查 |
92
+ | [connector-local/README.md](connector-local/README.md) | 免费本地连接器 kba-local:结构、命令与包源前置 |
93
+ | [docs/handoff.md](docs/handoff.md) | **新任务交接**:现状、决策、P0/P1 状态、验收与启动提示词 |
94
+ | [docs/master-assessment-and-plan.md](docs/master-assessment-and-plan.md) | **单一事实源**:完整缺口、可信度分级、P0/P1/P2 计划与验收门 |
95
+ | [docs/prd-conformance-review.md](docs/prd-conformance-review.md) | PRD 与 WorkBuddy 专家规范符合性评审、偏差与修正清单 |
96
+ | [docs/PRD.md](docs/PRD.md) | PRD v1.8:需求、FR、接口、决策建议、v0.3.0rc4 实现状态 |
97
+ | [docs/platform-questions.md](docs/platform-questions.md) | 平台 6 问调研答案、提审避坑清单与剩余 2 个待验证项 |
98
+ | [docs/interview-guide.md](docs/interview-guide.md) | 10 人次访谈提纲与评分 |
99
+ | [docs/interview-guide-investment-research.md](docs/interview-guide-investment-research.md) | 投研首站的 10 人次访谈提纲(卖方/买方/独立/IT) |
100
+ | [docs/vertical-investment-research.md](docs/vertical-investment-research.md) | 投研垂直方案:ICP、痛点、体检产品、定价与 90 天验证 |
101
+ | [docs/pricing-experiment.md](docs/pricing-experiment.md) | 免费/付费切分与 ¥99/199/299 实验 |
102
+ | [docs/paid-trial-runbook.md](docs/paid-trial-runbook.md) | ¥1 付费试用技能的完整执行步骤、判据与分流 |
103
+ | [docs/pre-submit-review.md](docs/pre-submit-review.md) | 提审前评审:自动化检查、逐项合规、修复项与剩余风险 |
104
+ | [docs/submission-log.md](docs/submission-log.md) | 平台提交记录、审核状态与过审后的操作顺序 |
105
+ | [docs/repo-setup.md](docs/repo-setup.md) | 远程仓库、CI、分支保护与公开前的拆分计划 |
106
+ | [docs/developer-profile.md](docs/developer-profile.md) | 开发者主页:头像、昵称、简介与联系方式填写建议 |
107
+ | [docs/product-gap-assessment.md](docs/product-gap-assessment.md) | 产品缺口深度评估:数据接入、分析准确性、报告可信度与优先级 |
108
+
109
+ ## 已知限制
110
+
111
+ - **只读**:v0.3.0rc4 不提供 apply/undo,治理执行仍在服务侧。
112
+ - **抽取是可选的**:不跑 `kba extract` 时仍是元数据模式,报告标 C 级、不展示综合健康分;`pip install -e ".[extract]"` 是抽取前置。
113
+ - **旧版 Office 只计数**:`.ppt/.xls/.doc` 不做 LibreOffice 转换,显式标 unsupported;`.pptx/.xlsx` 已可抽取。
114
+ - **Excel 结构指纹只识别有公式/named range 的模型**:纯数值工作簿即使 sheet 同名也不会被当作“同模型不同版本”,避免误报。
115
+ - **页级重合不是整档裁决**:PDF/PPT 单页高重合会报告,但不进入重复簇、不自动折叠;版本差异目前是终稿/底稿信号 + 字数/相似度/增删字符摘要,未做逐段正文语义 diff。
116
+ - **PII 是正则初筛**:只覆盖邮箱/手机号/身份证/银行卡/密钥/合同编号模式,可能误报/漏报;报告只显示计数与文件名,不展示原文。
117
+ - **编码仍有边界**:UTF-8/GB18030/UTF-16(BOM) 可读;Big5/Shift-JIS 在 GB18030 可解码时仍可能是乱码(已报 `text_unreadable` 的只覆盖解码失败场景)。
118
+ - **正文缓存有 TTL 但不加密**:`job/text/` 默认保留 7 天;`kba extract` 默认在抽取前清理超过 7 天的 sidecar(`--ttl-days -1` 关闭);`kba clean --purge-text` / `--purge-extract` 可立即清理。金融/律所客户仍需显式说明缓存策略。
119
+ - **标签/领域推断是关键词规则**:frontmatter + 路径/文件名 + 内容关键词,单篇最多 6 个标签;可能误标/漏标,未接入行业词表校验。
120
+ - **负责人只读 frontmatter**:`owner/负责人` 字段;ACL、最后访问、文档系统 owner 未接。
121
+ - **断链只查 Markdown 本地相对链接**:跳过 http/https、`file:`/`javascript:` 等 URI scheme 和 `@ref` 占位符;不查外链、仓库外目标、非 Markdown 引用。
122
+ - **性能基线只是合成 5k**:`tools/bench_scan.py` 本机 5k 文本约 scan 9.6s / analyze 1.9s;50k 与真实 PDF/Office 语料未测。
123
+ - **时效判断不可靠**:文件系统 mtime 在复制/迁移后会重置;报告会提示“仅供参考”,后续接入文档系统 `updated`。
124
+ - **PDF/docx 标签不在文件内**:推断可补一部分,但二进制标签仍不如文本 frontmatter 可靠。
125
+ - **行业包未加载**:`packs/generic` 目前是配置草案,v0.4 起生效。
126
+ - **许可证暂用 MIT**:待 Q10 决策后可能切 Apache-2.0。
127
+
128
+ ## 下一步
129
+
130
+ 1. **P1 剩余准确性**:逐段正文语义 diff、ACL/文档系统 owner、标签误报治理与行业词表;用 3 个外部语料验收抽取 ≥90%、近重复召回 ≥80%。
131
+ 2. **报告可验收**:PDF 报告、报告哈希/签字页、执行摘要;默认脱敏分享。
132
+ 3. **工程门禁**:Windows CI 已在 `.gitcode/workflows/ci.yml` 增加;补 5 万文件性能基线、云占位/权限拒绝/Big5 fixtures。抽取并行与 sidecar TTL 已加。
133
+ 4. 按 [docs/paid-trial-runbook.md](docs/paid-trial-runbook.md) 走路径 B:免费包 → 过审 → 收入变现 → ¥1 付费技能验证。
134
+ 5. 按 [docs/interview-guide.md](docs/interview-guide.md) 约 10 人访谈;3 个外部语料跑通前不卖“完整审阅”。
135
+ 6. 连接器提审;根据 ¥1 验证结果决定付费报告形态。
@@ -0,0 +1,22 @@
1
+ kba/__init__.py,sha256=03x9LGTV8b9fEQFcQMmZhL-fxVu9wIUe0dQqhd6_lGw,72
2
+ kba/__main__.py,sha256=4JMK66Wj4uLZTKbF-sT3LAxOsr6buig77PmOkJCRRxw,83
3
+ kba/analyze.py,sha256=E0u8iLFaw7KdthGEQ1pLELJJcDW85VzAbwY-xk1bhPE,42172
4
+ kba/cli.py,sha256=WlzI-SR7Huney9Mqp1Uywe9yVCt68PHZEH0SO3xqRxw,21925
5
+ kba/doctor.py,sha256=LTduI5k-0msoq1ghxoSjG__KwAp1g-eXR0SzSWg5lFQ,2591
6
+ kba/eval.py,sha256=07seiG3rDiAwoeep3HMHXHTaT3ekcqmJYE6mRBJAh9Y,13355
7
+ kba/extract.py,sha256=5hBciTJzFNQnDUPylSSqqjZrD7UMETW7loHdXKXXzII,18313
8
+ kba/fingerprint.py,sha256=1zB8mNNLio-9q2ZsNn5gknK30PQRxr2J9PkJ-83Ezxs,12963
9
+ kba/jobs.py,sha256=g3nNRNshYUdpgea4zTHpUuTQK16TmskCYrPwh6MouTM,1780
10
+ kba/links.py,sha256=23uqOZzEp07YjCweyZe7UUA6Hzw4pkJHYuWj_m-iMKQ,1435
11
+ kba/pii.py,sha256=ch7Fk26dq2Rkevh3K8BBuswMGKO0brJiQGMv_D2wTm4,1348
12
+ kba/report.py,sha256=CFmdHyNFCrs3hRP_o58RKi8Js3K9iLJzZtHKMekwYz0,32286
13
+ kba/summary.py,sha256=Ap5HU7IoLrtbr5Jvm-pfjpdZUoEclaRvwPlgoJcp4lg,2245
14
+ kba/tags.py,sha256=SsYf1BmR0XWDSQ5ocSOKAMx8yGM4j910Y8C0IiUxwHQ,5200
15
+ kba/telemetry.py,sha256=L5zllit89CiwCFwBgCa8e6K0wBQOeM7euGqRXr3q1Ig,2121
16
+ kba/util.py,sha256=wFrqY_RWlZSmTJz_KSDOP4Qti2omyzrJwU0AblrqsXM,6485
17
+ kba_cli-0.3.0rc4.dist-info/licenses/LICENSE,sha256=u8T5zOdeAeV1yV7GwyCHuclCloDGM2XbUNebaUPDntA,1060
18
+ kba_cli-0.3.0rc4.dist-info/METADATA,sha256=Z0mdb2_0w3EvsMMkTSYNLuAGPUN9ZYWj7CYP9AHSk7k,11346
19
+ kba_cli-0.3.0rc4.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
20
+ kba_cli-0.3.0rc4.dist-info/entry_points.txt,sha256=n_iS4p6qLR1TPczTuiCs9knjV8Fwsp2dgG1i056JlKg,37
21
+ kba_cli-0.3.0rc4.dist-info/top_level.txt,sha256=dQToisLm3VfZPs8NeWqpwJnLQUUNsyVvm0Q1E_sNB3U,4
22
+ kba_cli-0.3.0rc4.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ kba = kba.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 KBA
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ kba