dsh-data-cleaning-agent 0.3.0 → 0.4.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.
@@ -30,7 +30,7 @@
30
30
  | --- | --- | --- | --- | --- |
31
31
  | 一期 | 0.3.0 ✅ | 方案 A:模型中介式 Skill `enterprise-enrichment` | 核心工商 7 字段 + 风险标签 | `qcc-dsh-mcp-oauth` 已连接 |
32
32
  | 二期 | 0.4.0 | 方案 A 扩展 Skill:工商全景 + 股权穿透 | 工商域 16 工具 + 历史工商 | 同上 |
33
- | 三期 | 0.5.0 | 方案 A 扩展 Skill:风险/知产/经营 + 方案 B 批量后端 | 风险 38 + 知产 18 + 经营 35 | 同上 + Spike #7(`ctx.loader` 可调) |
33
+ | 三期 | 0.5.0 | 方案 A 扩展 Skill:风险/知产/经营 + 方案 B 批量后端 | 风险 38 + 知产 18 + 经营 35 | 同上;S7 PASS,G5-2 安全闭环已落地 |
34
34
  | 四期(可选) | 0.6.0 | 历史轨迹 + 董监高 + 招投标 | 历史 34 + 人员 44 + 招投标 6 | 企业认证账号(历史域) |
35
35
 
36
36
  每期之间不互相阻塞:二期工商全景、三期风险知产都可独立评审与合入。
@@ -93,6 +93,23 @@ v1 字段契约(方案 A):
93
93
  - 消歧规则不变(`get_company_by_query` 多候选必须询问用户)。
94
94
  - 金额/比例/计数逐字引用工具返回值,禁止自算、禁止臆测。
95
95
 
96
+ ### 3.4 实施状态(0.4.0 发布候选)
97
+
98
+ - ✅ 第一切片:`lib/qcc-phase2.js` 已固化本地 QCC MCP 一手源码核对过的
99
+ 16 个工商工具和 4 个历史工商工具;`enterprise-enrichment` 已扩展为按维度组调用。
100
+ - ✅ 安全规则:多候选人工确认、付费组按需调用、数值原样保留、来源工具标记、
101
+ 历史域无权显式降级。
102
+ - ✅ 验收自动化:`e2e:phase2` 默认关闭,检查 20 企业 / 每企业 ≥15 维、
103
+ 来源工具、原值一致性、主体消歧和历史账号门,拒绝合成证据替代真实 E2E。
104
+ - ✅ DSH 冒烟:当前工作树 tarball 已在隔离 `0.1.1-rc.2` 和 `0.1.2-alpha.2` Host
105
+ 完成加载,两者 seam 均返回 `enrichSkillRegistered:true`;测试 Host 已停止,生产端口未触碰。
106
+ - ✅ 预检状态冒烟:无 OAuth 插件时返回 `oauth-plugin-missing`;安装插件但未授权时
107
+ 返回 `not-connected-or-refreshing`。两种情况都不执行 QCC 工具,不产生付费调用。
108
+ - ✅ 真实发布门主路径:隔离 rc.2 Host 完成 OAuth、20 企业、400 次调用;20/20 主体解析,
109
+ 每企业当前最低 15 维、历史 4 维,严格验收通过。
110
+ - ✅ 2026-09-02 发布门收口:自然过期 token 真实刷新、16+4 动态工具恢复、续期后 1 行真实 enrich;
111
+ 401/429/配额耗尽通过 Web→Bridge→ToolRuntime 故障注入验证,无自动重试且审计脱敏。
112
+
96
113
  ---
97
114
 
98
115
  ## 4. 三期(0.5.0)· 风险 / 知产 / 经营 + 批量后端
@@ -174,9 +191,14 @@ v1 字段契约(方案 A):
174
191
  ### 4.4 三期并行 · 方案 B 批量后端
175
192
 
176
193
  一期/二期/三期均为模型中介式(模型逐个调 QCC 工具)。当名单规模进入百级/千级,模型逐调成本高,
177
- 启动方案 B(插件内 `lib/qcc.js` 直接经 `ctx.loader` 调用 mcp-client 条目):
178
- - 前置:Spike #7 验证「`ctx.loader` 拿到的 mcp-client 条目可在插件后端直接调用」。
179
- - 若验证失败,方案 B 边界封闭,退回方案 A 的分批模型中介。
194
+ 方案 B 已启动:插件内 `lib/qcc.js` 经公共 `ctx.tools.execute()` 调用 mcp-client 动态注册的工具;
195
+ 不直接访问 `ctx.loader` 条目或 mcp-client 私有 client。
196
+
197
+ - Spike #7:rc.2 / alpha.2 双基线 PASS。
198
+ - G5-2:在 G5-1 基础上完成默认关闭 E2E Runner、脱敏、请求幂等、多候选确认续跑、
199
+ retryable 失败人工重试、细分错误分类与安全审计,均已通过 Mock/Contract 测试。
200
+ - 已通过:真实 OAuth 首连、授权跨重启恢复、真实 QCC 主调用路径、token 自然到期刷新与续期后调用。
201
+ - 已通过:401/429/配额耗尽故障注入;仅 retryable 错误允许用户显式重试,非 retryable 配额错误在派发前阻断。
180
202
 
181
203
  ---
182
204
 
@@ -0,0 +1,57 @@
1
+ # 0.4.0 发布候选检查单
2
+
3
+ - 源码版本:`0.4.0`
4
+ - 状态:发布候选,尚未创建 `v0.4.0` tag、GitHub Release 或 npm 发布
5
+ - 基线日期:2026-09-02
6
+
7
+ ## 已纳入范围
8
+
9
+ - `enterprise-enrichment` 扩展为工商全景、股权、治理和历史工商维度组按需调用。
10
+ - 固化 16 个当前工商工具与 4 个历史工商工具契约,并兼容
11
+ `qcc-dsh-mcp-oauth@0.1.7` 实测 legacy serverName。
12
+ - 新增只读 `/data-cleaning/api/phase2/capabilities` 预检、真实证据验收器与默认关闭的本地 Runner。
13
+ - G5 Host Bridge 提供批量幂等、多候选人工续跑、retryable 失败人工重试、取消/超时、错误分类和脱敏审计。
14
+ - 发布 workflow 在发布前执行完整 `npm run check`,并强制 Git tag 与 `package.json` 版本一致。
15
+
16
+ ## 已通过门
17
+
18
+ - 单元、契约、Web 路由、Skill、脱敏和 Runner 自动测试。
19
+ - npm 打包白名单与 README/包版本一致性检查。
20
+ - DSH `0.1.1-rc.2` / `0.1.2-alpha.2` 隔离 Host 加载冒烟。
21
+ - rc.2 隔离 Host 真实 OAuth、授权跨重启恢复、20 家公开企业、400 次 QCC 调用:
22
+ 20/20 主体解析,每企业当前工商最低 15 维、历史工商 4 维。
23
+ - rc.2 隔离 Host 的旧 access token 已自然过期;重新启动后持久 grant 自动刷新、到期时间前移,
24
+ 16 个 company + 4 个 history 动态工具恢复,并以 1 行真实 enrich(1/1 成功、2 条安全审计)确认新 token 可用。
25
+ - Web→Bridge→Mock ToolRuntime 故障注入覆盖 401、429 与配额耗尽:除 `UNKNOWN_TOOL` 刷新竞态外
26
+ 均不自动重试;401/429 只能显式人工重试,配额耗尽在重新派发前阻断;审计不含参数、原始响应或秘密。
27
+ - `main` 提交 `0c8cb75` 的远端 CI `33569931224` 已通过 Linux Node 22、Linux Node 24 与 Windows Node 24 全矩阵。
28
+ - 真实证据与报告仅保存在 Git 忽略的本机目录,不进入仓库或 npm 包。
29
+
30
+ ## 发布阻断门(已通过)
31
+
32
+ 2026-09-02 已完成此前两个剩余门:
33
+
34
+ 1. ✅ access token 自然到期后的真实 refresh、持久 grant 更新、动态工具恢复与续期后最小真实调用。
35
+ 2. ✅ 401、429、配额不足的本地故障注入、稳定错误码、人工重试门及审计脱敏。
36
+
37
+ 代码审查、本机 `npm run check`、干净工作树和远端 CI 已完成;剩余只是创建/推送 tag 所触发的正式发布操作,
38
+ 不再有 0.4.0 功能实现缺口。
39
+
40
+ ## 发布命令(阻断门全部通过后)
41
+
42
+ ```bash
43
+ npm run check
44
+ git status --short
45
+ git tag -a v0.4.0 -m "Release v0.4.0"
46
+ git push origin v0.4.0
47
+ ```
48
+
49
+ `v0.4.0` tag 会触发 `.github/workflows/release.yml`:校验 tag/包版本、执行完整检查、
50
+ 通过 npm OIDC Trusted Publishing 发布并生成 GitHub Release。不要手工写入生产密钥。
51
+
52
+ ## 回滚
53
+
54
+ - tag 尚未推送:删除本地 tag,修复后重新检查。
55
+ - tag 已推送但 workflow 未发布 npm:停止 workflow,修复后使用新的补丁版本;不要复用已公开 tag。
56
+ - npm 已发布:npm 版本不可覆盖。立即在 GitHub/npm 标记受影响版本,必要时执行 `npm deprecate`,
57
+ 修复后发布 `0.4.1`;代码回滚使用普通 revert commit,不改写 `main` 历史。
@@ -43,17 +43,46 @@ bash <(curl -fsSL https://raw.githubusercontent.com/duhu2000/dsh-data-cleaning-a
43
43
 
44
44
  先用企查查 MCP 连接插件(`qcc-dsh-mcp-oauth`)完成授权,然后对对话说:
45
45
 
46
- > 帮我补全这份企业名单:统一社会信用代码、法人、注册资本、成立日期、登记状态、风险标签。
46
+ > 帮我补全这份企业名单:工商全景 + 股权穿透,不要历史工商。
47
47
 
48
48
  模型会按 `enterprise-enrichment` Skill 逐个企业调 `mcp__qcc-company__get_company_by_query`
49
- (消歧,多候选时停下询问)→ `get_company_registration_info`(工商详情)→
50
- `get_company_risk_scan`(风险标签),最后输出摘要 + Markdown 补全表。
49
+ (消歧,多候选时停下询问)→ `get_company_registration_info`(工商详情),
50
+ 然后只调用请求的维度组:
51
+
52
+ - `panorama`:企业画像、联系方式、开票、上市、财务;
53
+ - `ownership`:实控人、受益所有人、股东和对外投资;
54
+ - `governance`:分支机构、主要人员、变更记录和年报;
55
+ - `history`:历史股东、法人、高管和登记信息(需企业认证账号)。
56
+
57
+ 如果请求没有明确维度,Skill 会先让用户选择,不默认调用全部付费工具。
58
+ 历史域无权时只标记 `permission_required`,当前工商组仍继续。对话默认返回统计摘要和小量预览;
59
+ 完整明细只在 Host 确实提供同源下载/产物能力时交付。
60
+ 开发者可在真实调用前 GET `/data-cleaning/api/phase2/capabilities`,只读检查 16+4 工具面;
61
+ 该预检不发起 QCC 或付费调用,也不会把「工具已注册」误报为「历史账号已授权」。
51
62
 
52
63
  **前置条件**:先连接企查查 MCP(未连接时 Skill 会引导执行 `qcc_oauth_connect`)。
53
64
 
54
65
  **本阶段边界**(方案 A,模型中介式):
55
66
  - 不重造 OAuth;工具面来自 `qcc-dsh-mcp-oauth`。
56
- - 逐企业调用,适合中小名单(几十条以内);百级以上大名单的批量补全见路线图(方案 B)。
67
+ - 逐企业调用,适合中小名单(几十条以内)。
68
+ - 金额、比例、计数保留 QCC 原值,不自算股权链、不将缺失值写成「无」或 0。
69
+ - 0.4.0 尚未发布;20 企业、每企业至少 15 个当前维度并含 4 个历史维度的真实账号验收已通过。
70
+ access token 自然到期后的真实刷新、动态工具恢复以及限流/配额故障注入已在隔离环境验收。
71
+
72
+ ### 3.3 QCC 后台批量 Host Bridge(0.4.0 发布候选 / G5-2)
73
+
74
+ 源码 `main` 已提供 `/data-cleaning/api/g5/capabilities`(只读能力探测)和
75
+ `/data-cleaning/api/g5/enrich`(同源批量补全)基础层。它按企业名去重调用、只对唯一精确主体继续补全,
76
+ 多候选进入人工确认队列,模型不接触完整明细。
77
+
78
+ 这是 0.4.0 尚未发布的候选能力:真实 OAuth、授权跨重启恢复、QCC 主调用路径、token 自然到期刷新
79
+ 与 401 / 429 / 配额故障注入均已完成隔离验收;正式可用版本仍以 npm/GitHub Release 为准。
80
+ 调用批量端点必须由 UI 在用户确认后同时发送 `confirmPaidCalls:true` 和唯一 `idempotencyKey`;
81
+ 未确认或缺少幂等键时不会产生任何 QCC 调用。
82
+
83
+ 初次请求返回 `runId`:多候选进入 `awaiting-review`,只能通过 `/g5/resolve` 选择返回候选中的
84
+ 信用代码后续跑;retryable 失败只能由用户通过 `/g5/retry` 显式重试。run 仅在 Host 内存保留
85
+ 30 分钟,Host 重启后失效,不把原始企业行持久化落盘。
57
86
 
58
87
  ## 4. 数据边界
59
88
 
@@ -72,6 +101,9 @@ bash <(curl -fsSL https://raw.githubusercontent.com/duhu2000/dsh-data-cleaning-a
72
101
  - **Q:安装后工具不出现?** A:确认已完全重启 DSH;确认 `dsh plugin list`(或 profile 的
73
102
  `package.json` → `dsh.profile.bundles`)含 `dsh-data-cleaning-agent`。
74
103
  - **Q:XLSX 解析报 `XLSX_UNAVAILABLE`?** A:当前 DSH 组合未安装 `xlsx`;web 组合默认可用。
75
- - **Q:能接企查查补全企业信息吗?** A:可以。先安装并连接 `qcc-dsh-mcp-oauth`,再说"帮我补全这份企业名单",会自动走
104
+ - **Q:能接企查查补全企业信息吗?** A:可以。先安装并连接 `qcc-dsh-mcp-oauth`;rc.2 隔离 Profile
105
+ 需同时显式安装同版本 `@deepseek-ai/dsh-mcp-client`。再说"帮我补全这份企业名单",会自动走
76
106
  `enterprise-enrichment` Skill(方案 A 模型中介式)。字段契约与二期规划见
77
107
  [QCC-ENRICHMENT-DESIGN.md](QCC-ENRICHMENT-DESIGN.md)。
108
+ - **Q:可以在后台批量补全吗?** A:`main` 已有 G5-2 Host Bridge 安全闭环,真实 OAuth/QCC 主路径已验收但尚未发布;
109
+ 生产使用前仍需通过 `docs/G5-HOST-BRIDGE.md` 的 token 到期刷新与计费错误门。
package/lib/index.js CHANGED
@@ -6,6 +6,7 @@
6
6
  * 2. ctx.skills —— 注册内嵌 Skill `data-cleaning`(正文指引模型调上述工具)
7
7
  * 3. webServer/webRuntime —— 挂载上传/解析/同步清洗补全/异步任务/UI 路由
8
8
  * 4. ctx.jobs + ctx.storageDomain —— 异步任务状态机(web 组合内可用)
9
+ * 5. ctx.tools.execute —— G5 QCC Host Bridge(程序化批量补全,web 组合内可用)
9
10
  *
10
11
  * headless 组合无 webServer/webRuntime:用 ctx.get() 存在性守卫跳过 web 半区,
11
12
  * 工具与 Skill 照常注册(端到端真实模型路径依赖它们)。
@@ -26,6 +27,7 @@ export function apply(ctx, config) {
26
27
  skillRegistered: false,
27
28
  webMounted: false,
28
29
  webSkipped: false,
30
+ qccBridgeMounted: false,
29
31
  };
30
32
  const disposers = [];
31
33
 
@@ -0,0 +1,191 @@
1
+ import { isDeepStrictEqual } from 'node:util';
2
+ import {
3
+ QCC_PHASE2_COMPANY_TOOLS,
4
+ QCC_PHASE2_HISTORY_TOOLS,
5
+ qccToolRuntimeCandidates,
6
+ } from './qcc-phase2.js';
7
+
8
+ export const QCC_PHASE2_ACCEPTANCE_SCHEMA_VERSION = 1;
9
+ export const QCC_PHASE2_EVIDENCE_KIND = 'qcc-phase2-real-tool-transcript';
10
+ export const QCC_PHASE2_ACCEPTANCE_FLOORS = Object.freeze({
11
+ minimumRecords: 20,
12
+ minimumCurrentDimensions: 15,
13
+ requiredHistoryDimensions: Object.keys(QCC_PHASE2_HISTORY_TOOLS).length,
14
+ });
15
+
16
+ const CURRENT_TOOLS = new Map(Object.entries(QCC_PHASE2_COMPANY_TOOLS));
17
+ const HISTORY_TOOLS = new Map(Object.entries(QCC_PHASE2_HISTORY_TOOLS));
18
+ const DELIVERED_STATUSES = new Set(['resolved', 'no_data']);
19
+ const ALLOWED_STATUSES = new Set([
20
+ ...DELIVERED_STATUSES,
21
+ 'permission_required',
22
+ 'not_available',
23
+ 'error',
24
+ ]);
25
+ const OPAQUE_REFERENCE = /^row-[0-9]{3,6}$/;
26
+
27
+ function hasOwn(value, key) {
28
+ return Object.prototype.hasOwnProperty.call(value, key);
29
+ }
30
+
31
+ function hasMeaningfulValue(value) {
32
+ if (value === null || value === undefined) return false;
33
+ if (typeof value === 'string') return value.trim().length > 0;
34
+ if (Array.isArray(value)) return value.length > 0;
35
+ if (typeof value === 'object') return Object.keys(value).length > 0;
36
+ return true;
37
+ }
38
+
39
+ function evaluateField(field) {
40
+ if (!field || typeof field !== 'object' || Array.isArray(field)) return false;
41
+ if (typeof field.key !== 'string' || field.key.length === 0) return false;
42
+ if (!hasOwn(field, 'value') || !hasOwn(field, 'sourceValue')) return false;
43
+ if (!hasMeaningfulValue(field.value) || !hasMeaningfulValue(field.sourceValue)) return false;
44
+ return isDeepStrictEqual(field.value, field.sourceValue);
45
+ }
46
+
47
+ function evaluateDimension(dimension, knownTools) {
48
+ const failures = [];
49
+ if (!dimension || typeof dimension !== 'object' || Array.isArray(dimension)) {
50
+ return { id: null, delivered: false, failures: ['DIMENSION_INVALID'] };
51
+ }
52
+ const id = typeof dimension.id === 'string' ? dimension.id : null;
53
+ const expectedTool = id ? knownTools.get(id) : null;
54
+ if (!expectedTool) failures.push('DIMENSION_UNKNOWN');
55
+ if (!qccToolRuntimeCandidates(expectedTool ?? '').includes(dimension.sourceTool)) {
56
+ failures.push('SOURCE_TOOL_MISMATCH');
57
+ }
58
+ if (!ALLOWED_STATUSES.has(dimension.status)) failures.push('DIMENSION_STATUS_INVALID');
59
+
60
+ const fieldsAreArray = Array.isArray(dimension.fields);
61
+ const fields = fieldsAreArray ? dimension.fields : [];
62
+ if (!fieldsAreArray) failures.push('FIELDS_ARRAY_REQUIRED');
63
+ if (dimension.status === 'resolved') {
64
+ if (fields.length === 0) failures.push('RESOLVED_FIELDS_REQUIRED');
65
+ if (!fields.every(evaluateField)) failures.push('VALUE_NOT_VERBATIM');
66
+ }
67
+ if (dimension.status === 'no_data' && fields.length > 0) {
68
+ failures.push('NO_DATA_MUST_NOT_CARRY_VALUES');
69
+ }
70
+
71
+ return {
72
+ id,
73
+ delivered: failures.length === 0 && DELIVERED_STATUSES.has(dimension.status),
74
+ failures,
75
+ };
76
+ }
77
+
78
+ function evaluateRecord(record, index, { requireHistory }) {
79
+ const safeReference = OPAQUE_REFERENCE.test(record?.reference ?? '')
80
+ ? record.reference
81
+ : `row-${String(index + 1).padStart(3, '0')}`;
82
+ const failures = [];
83
+ if (!OPAQUE_REFERENCE.test(record?.reference ?? '')) failures.push('REFERENCE_NOT_OPAQUE');
84
+ if (record?.entityStatus !== 'resolved') failures.push('ENTITY_NOT_RESOLVED');
85
+
86
+ const dimensions = Array.isArray(record?.dimensions) ? record.dimensions : [];
87
+ if (!Array.isArray(record?.dimensions)) failures.push('DIMENSIONS_REQUIRED');
88
+
89
+ const seen = new Set();
90
+ const deliveredStatuses = new Map();
91
+ let currentDelivered = 0;
92
+ let historyDelivered = 0;
93
+ for (const dimension of dimensions) {
94
+ const domain = dimension?.domain;
95
+ const tools = domain === 'history' ? HISTORY_TOOLS : domain === 'company' ? CURRENT_TOOLS : null;
96
+ if (!tools) {
97
+ failures.push('DIMENSION_DOMAIN_INVALID');
98
+ continue;
99
+ }
100
+ const uniqueKey = `${domain}:${dimension?.id ?? ''}`;
101
+ if (seen.has(uniqueKey)) {
102
+ failures.push('DIMENSION_DUPLICATE');
103
+ continue;
104
+ }
105
+ seen.add(uniqueKey);
106
+ const result = evaluateDimension(dimension, tools);
107
+ failures.push(...result.failures);
108
+ if (result.delivered) deliveredStatuses.set(uniqueKey, dimension.status);
109
+ if (result.delivered && domain === 'company') currentDelivered += 1;
110
+ if (result.delivered && domain === 'history') historyDelivered += 1;
111
+ }
112
+
113
+ if (
114
+ deliveredStatuses.get('company:resolveEntity') !== 'resolved'
115
+ || deliveredStatuses.get('company:registration') !== 'resolved'
116
+ ) {
117
+ failures.push('IDENTITY_EVIDENCE_REQUIRED');
118
+ }
119
+ if (currentDelivered < QCC_PHASE2_ACCEPTANCE_FLOORS.minimumCurrentDimensions) {
120
+ failures.push('CURRENT_DIMENSION_FLOOR_NOT_MET');
121
+ }
122
+ if (requireHistory && historyDelivered < QCC_PHASE2_ACCEPTANCE_FLOORS.requiredHistoryDimensions) {
123
+ failures.push('HISTORY_DIMENSION_FLOOR_NOT_MET');
124
+ }
125
+
126
+ return {
127
+ reference: safeReference,
128
+ passed: failures.length === 0,
129
+ currentDelivered,
130
+ historyDelivered,
131
+ failures: [...new Set(failures)].sort(),
132
+ entityStatus: ['resolved', 'ambiguous', 'unresolved'].includes(record?.entityStatus)
133
+ ? record.entityStatus
134
+ : 'invalid',
135
+ };
136
+ }
137
+
138
+ /**
139
+ * 评估 0.4.0 真实 E2E 证据。返回值严格不携带企业名、信用代码或字段值。
140
+ */
141
+ export function evaluateQccPhase2Evidence(evidence, { requireHistory = false } = {}) {
142
+ const globalFailures = [];
143
+ if (!evidence || typeof evidence !== 'object' || Array.isArray(evidence)) {
144
+ globalFailures.push('EVIDENCE_INVALID');
145
+ }
146
+ if (evidence?.schemaVersion !== QCC_PHASE2_ACCEPTANCE_SCHEMA_VERSION) {
147
+ globalFailures.push('SCHEMA_VERSION_UNSUPPORTED');
148
+ }
149
+ if (evidence?.evidenceKind !== QCC_PHASE2_EVIDENCE_KIND) {
150
+ globalFailures.push('EVIDENCE_KIND_UNVERIFIED');
151
+ }
152
+ // 真实 E2E 证据必须显式声明 synthetic:false;缺省值也按未验证处理,保持 fail-closed。
153
+ if (evidence?.synthetic !== false) globalFailures.push('SYNTHETIC_EVIDENCE_REJECTED');
154
+ if (!Array.isArray(evidence?.records)) globalFailures.push('RECORDS_REQUIRED');
155
+ const records = Array.isArray(evidence?.records) ? evidence.records : [];
156
+ if (records.length < QCC_PHASE2_ACCEPTANCE_FLOORS.minimumRecords) {
157
+ globalFailures.push('RECORD_FLOOR_NOT_MET');
158
+ }
159
+ if (requireHistory && evidence?.historyAccess !== 'enterprise-certified') {
160
+ globalFailures.push('ENTERPRISE_HISTORY_ACCESS_NOT_VERIFIED');
161
+ }
162
+
163
+ const recordReports = records.map((record, index) => evaluateRecord(record, index, { requireHistory }));
164
+ const references = recordReports.map((record) => record.reference);
165
+ if (new Set(references).size !== references.length) globalFailures.push('REFERENCE_DUPLICATE');
166
+
167
+ const failedRecords = recordReports.filter((record) => !record.passed);
168
+ const summary = {
169
+ recordCount: recordReports.length,
170
+ passedRecords: recordReports.length - failedRecords.length,
171
+ failedRecords: failedRecords.length,
172
+ ambiguousRecords: recordReports.filter((record) => record.entityStatus === 'ambiguous').length,
173
+ unresolvedRecords: recordReports.filter((record) => record.entityStatus === 'unresolved').length,
174
+ minimumCurrentDimensions: recordReports.length === 0
175
+ ? 0
176
+ : Math.min(...recordReports.map((record) => record.currentDelivered)),
177
+ minimumHistoryDimensions: recordReports.length === 0
178
+ ? 0
179
+ : Math.min(...recordReports.map((record) => record.historyDelivered)),
180
+ };
181
+
182
+ return {
183
+ schemaVersion: QCC_PHASE2_ACCEPTANCE_SCHEMA_VERSION,
184
+ passed: globalFailures.length === 0 && failedRecords.length === 0,
185
+ requireHistory,
186
+ floors: QCC_PHASE2_ACCEPTANCE_FLOORS,
187
+ summary,
188
+ globalFailures: [...new Set(globalFailures)].sort(),
189
+ failures: failedRecords.map(({ reference, failures }) => ({ reference, codes: failures })),
190
+ };
191
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * QCC 0.4.0 二期工具契约。
3
+ *
4
+ * 这里只固化已经在本地 QCC MCP 一手源码注册表中核对过的工具名。
5
+ * 不固化上游响应字段:方案 A 由模型中介解读工具返回,并必须保留原值。
6
+ */
7
+
8
+ const companyTool = (name) => `mcp__qcc-company__${name}`;
9
+ const historyTool = (name) => `mcp__qcc-history__${name}`;
10
+
11
+ /**
12
+ * qcc-dsh-mcp-oauth 0.1.7 把 serverKey 直接作为 serverName,因而注册为
13
+ * `mcp__company__*` / `mcp__history__*`;修复版与手工配置使用文档约定的
14
+ * `mcp__qcc-company__*` / `mcp__qcc-history__*`。Bridge 同时兼容两者,
15
+ * 但始终把带 qcc- 前缀的名称作为规范契约。
16
+ */
17
+ export function qccToolRuntimeCandidates(canonicalName) {
18
+ const name = String(canonicalName ?? '');
19
+ const legacy = name.replace(/^mcp__qcc-(company|risk|ipr|operation|history|executive)__/, 'mcp__$1__');
20
+ return legacy === name ? Object.freeze([name]) : Object.freeze([name, legacy]);
21
+ }
22
+
23
+ export const QCC_PHASE2_COMPANY_TOOLS = Object.freeze({
24
+ resolveEntity: companyTool('get_company_by_query'),
25
+ registration: companyTool('get_company_registration_info'),
26
+ profile: companyTool('get_company_profile'),
27
+ verifyIdentity: companyTool('verify_company_accuracy'),
28
+ actualController: companyTool('get_actual_controller'),
29
+ beneficialOwners: companyTool('get_beneficial_owners'),
30
+ shareholders: companyTool('get_shareholder_info'),
31
+ externalInvestments: companyTool('get_external_investments'),
32
+ branches: companyTool('get_branches'),
33
+ keyPersonnel: companyTool('get_key_personnel'),
34
+ changes: companyTool('get_change_records'),
35
+ annualReports: companyTool('get_annual_reports'),
36
+ contact: companyTool('get_contact_info'),
37
+ taxInvoice: companyTool('get_tax_invoice_info'),
38
+ listing: companyTool('get_listing_info'),
39
+ financial: companyTool('get_financial_data'),
40
+ });
41
+
42
+ export const QCC_PHASE2_HISTORY_TOOLS = Object.freeze({
43
+ shareholders: historyTool('get_historical_shareholders'),
44
+ legalRepresentative: historyTool('get_historical_legal_rep'),
45
+ executives: historyTool('get_historical_executives'),
46
+ registration: historyTool('get_historical_registration'),
47
+ });
48
+
49
+ /**
50
+ * 用户可选的维度组。identity 是任何任务的必需步骤;其余组按用户意图调用。
51
+ * 字段缺失、无权或上游不可用时,应保留状态而不是补造值。
52
+ */
53
+ export const QCC_PHASE2_DIMENSION_GROUPS = Object.freeze({
54
+ identity: Object.freeze({
55
+ label: '主体锚定与核验',
56
+ access: 'basic',
57
+ tools: Object.freeze([
58
+ QCC_PHASE2_COMPANY_TOOLS.resolveEntity,
59
+ QCC_PHASE2_COMPANY_TOOLS.registration,
60
+ QCC_PHASE2_COMPANY_TOOLS.verifyIdentity,
61
+ ]),
62
+ }),
63
+ panorama: Object.freeze({
64
+ label: '企业全景',
65
+ access: 'basic',
66
+ tools: Object.freeze([
67
+ QCC_PHASE2_COMPANY_TOOLS.profile,
68
+ QCC_PHASE2_COMPANY_TOOLS.contact,
69
+ QCC_PHASE2_COMPANY_TOOLS.taxInvoice,
70
+ QCC_PHASE2_COMPANY_TOOLS.listing,
71
+ QCC_PHASE2_COMPANY_TOOLS.financial,
72
+ ]),
73
+ }),
74
+ ownership: Object.freeze({
75
+ label: '股权穿透',
76
+ access: 'basic',
77
+ tools: Object.freeze([
78
+ QCC_PHASE2_COMPANY_TOOLS.actualController,
79
+ QCC_PHASE2_COMPANY_TOOLS.beneficialOwners,
80
+ QCC_PHASE2_COMPANY_TOOLS.shareholders,
81
+ QCC_PHASE2_COMPANY_TOOLS.externalInvestments,
82
+ ]),
83
+ }),
84
+ governance: Object.freeze({
85
+ label: '组织与沿革',
86
+ access: 'basic',
87
+ tools: Object.freeze([
88
+ QCC_PHASE2_COMPANY_TOOLS.branches,
89
+ QCC_PHASE2_COMPANY_TOOLS.keyPersonnel,
90
+ QCC_PHASE2_COMPANY_TOOLS.changes,
91
+ QCC_PHASE2_COMPANY_TOOLS.annualReports,
92
+ ]),
93
+ }),
94
+ history: Object.freeze({
95
+ label: '历史工商',
96
+ access: 'enterprise-certified',
97
+ tools: Object.freeze(Object.values(QCC_PHASE2_HISTORY_TOOLS)),
98
+ }),
99
+ });