@liuhange/dsh-gov-data-inspector 3.1.1 → 3.1.3

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 liuhange789
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.
package/README.md CHANGED
@@ -36,3 +36,80 @@ apply(ctx); // 注册工具: inspect_gov_data
36
36
  ## License
37
37
 
38
38
  MIT
39
+ ## 无凭据降级模式
40
+
41
+ 当官方数据源(国家/省级)凭据未配置或不可达时,插件自动进入**降级模式**而非中止运行。
42
+
43
+ ### 降级触发条件
44
+
45
+ - 国家和省级数据源均未配置 API Key / Endpoint
46
+ - 国家和省级数据源均不可达(网络超时、HTTP 错误等)
47
+
48
+ ### 降级表现
49
+
50
+ | 检测模式 | 完整模式表现 | 降级模式表现 |
51
+ |---------|------------|------------|
52
+ | classification | 正常分类 | **正常分类**(不依赖知识库) |
53
+ | guide | 语义检测基于官方知识库 | 语义检测降级为本地规则检测 |
54
+ | full | 全量检测 | 分类正常 + 语义/逻辑降级 |
55
+
56
+ ### 降级标记
57
+
58
+ 降级模式下,巡检报告包含以下额外字段:
59
+
60
+ ```json
61
+ {
62
+ "degradedMode": true,
63
+ "degradedReason": "官方数据源全部不可用,已降级为本地规则检测模式"
64
+ }
65
+ ```
66
+
67
+ ### 端点协议宽松化
68
+
69
+ 数据源端点支持以下三种协议:
70
+ - `https://` — HTTPS 远程端点
71
+ - `http://` — HTTP 远程端点(不再强制 HTTPS)
72
+ - 本地文件路径 — 直接读取本地 JSON 文件
73
+
74
+ ## 异常输入处理
75
+
76
+ 插件对以下异常输入场景提供明确的友好错误提示,不会崩溃或产生未捕获异常:
77
+
78
+ | 异常场景 | 错误码 | 错误提示 |
79
+ |---------|--------|---------|
80
+ | 格式错误的 JSON | `GOV_DATA_INPUT_INVALID` | JSON 格式错误,请检查文件内容 |
81
+ | 空数据(空文件/空数组) | `GOV_DATA_INPUT_INVALID` | 无数据,请确认输入文件内容 |
82
+ | 超长数据(超 100 万字段) | `GOV_DATA_SCALE_EXCEEDED` | 数据量超出处理上限 |
83
+ | 无效 URL / 不可达路径 | `GOV_DATA_URL_UNREACHABLE` | 数据源不可达,请检查路径或 URL |
84
+ | 非 UTF-8 编码文件 | `GOV_DATA_ENCODING_ERROR` | 文件编码非 UTF-8 |
85
+ | 部分字段缺失 | 正常处理 | 报告中列出缺失字段清单 |
86
+
87
+ ### itemTypeOverride 参数
88
+
89
+ 当自动匹配无法识别事项类型时,用户可通过 `itemTypeOverride` 参数手动指定:
90
+
91
+ ```javascript
92
+ execute({
93
+ dataSource: './data.json',
94
+ inspectionMode: 'guide',
95
+ itemTypeOverride: '现场勘查类'
96
+ })
97
+ ```
98
+
99
+ ### 未匹配事项类型警告
100
+
101
+ 当事项无法匹配到任何检测规则时,报告中会包含 `UNMATCHED_ITEM_TYPE` 警告:
102
+
103
+ ```json
104
+ {
105
+ "warnings": [
106
+ {
107
+ "type": "UNMATCHED_ITEM_TYPE",
108
+ "guideId": "建设工程规划核实",
109
+ "message": "该事项未匹配到检测规则,语义检测被跳过"
110
+ }
111
+ ]
112
+ }
113
+ ```
114
+
115
+ 匹配逻辑支持四通道:手动指定 → 名称关键词 → 编码前缀 → 流程字段关键词。
@@ -0,0 +1,91 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ErrorDetailBuilder } from '../errorDetailBuilder.js';
3
+ describe('ErrorDetailBuilder', () => {
4
+ it('传入六字段均非空且errorType合法 → 返回ErrorDetail对象', () => {
5
+ const detail = ErrorDetailBuilder.build({
6
+ guideId: 'guide1',
7
+ field: '办理时限',
8
+ errorType: 'semantic',
9
+ description: '办理时限超出法定上限',
10
+ suggestion: '建议调整为20个工作日以内',
11
+ });
12
+ expect(detail.guideId).toBe('guide1');
13
+ expect(detail.field).toBe('办理时限');
14
+ expect(detail.errorType).toBe('semantic');
15
+ expect(detail.severity).toBe('critical');
16
+ expect(detail.description).toBe('办理时限超出法定上限');
17
+ expect(detail.suggestion).toBe('建议调整为20个工作日以内');
18
+ });
19
+ it('传入任一字段为空 → 抛出错误', () => {
20
+ expect(() => ErrorDetailBuilder.build({
21
+ guideId: '',
22
+ field: '办理时限',
23
+ errorType: 'semantic',
24
+ description: 'desc',
25
+ suggestion: 'sug',
26
+ })).toThrow();
27
+ expect(() => ErrorDetailBuilder.build({
28
+ guideId: 'g1',
29
+ field: '',
30
+ errorType: 'semantic',
31
+ description: 'desc',
32
+ suggestion: 'sug',
33
+ })).toThrow();
34
+ });
35
+ it('errorType不合法 → 抛出错误', () => {
36
+ expect(() => ErrorDetailBuilder.build({
37
+ guideId: 'g1',
38
+ field: 'f',
39
+ errorType: 'invalid',
40
+ description: 'd',
41
+ suggestion: 's',
42
+ })).toThrow();
43
+ });
44
+ it('severity未传入时从severityMapping读取映射', () => {
45
+ const detail = ErrorDetailBuilder.build({
46
+ guideId: 'g1',
47
+ field: 'f',
48
+ errorType: 'missing',
49
+ description: 'd',
50
+ suggestion: 's',
51
+ }, { missing: 'major', semantic: 'critical', logical: 'critical' });
52
+ expect(detail.severity).toBe('major');
53
+ });
54
+ it('传入dataSource与standardClause → 返回的ErrorDetail含该两字段', () => {
55
+ const detail = ErrorDetailBuilder.build({
56
+ guideId: 'g1',
57
+ field: 'f',
58
+ errorType: 'semantic',
59
+ description: 'd',
60
+ suggestion: 's',
61
+ dataSource: 'national',
62
+ standardClause: '行政许可法第四十二条',
63
+ });
64
+ expect(detail.dataSource).toBe('national');
65
+ expect(detail.standardClause).toBe('行政许可法第四十二条');
66
+ });
67
+ it('未传入追溯字段 → 返回的ErrorDetail不含该两字段', () => {
68
+ const detail = ErrorDetailBuilder.build({
69
+ guideId: 'g1',
70
+ field: 'f',
71
+ errorType: 'semantic',
72
+ description: 'd',
73
+ suggestion: 's',
74
+ });
75
+ expect(detail.dataSource).toBeUndefined();
76
+ expect(detail.standardClause).toBeUndefined();
77
+ });
78
+ it('countByType返回值与明细条目数一致', () => {
79
+ const details = [
80
+ { guideId: 'g1', field: 'f1', errorType: 'missing', severity: 'major', description: 'd', suggestion: 's' },
81
+ { guideId: 'g1', field: 'f2', errorType: 'missing', severity: 'major', description: 'd', suggestion: 's' },
82
+ { guideId: 'g1', field: 'f3', errorType: 'semantic', severity: 'critical', description: 'd', suggestion: 's' },
83
+ { guideId: 'g1', field: 'f4', errorType: 'logical', severity: 'critical', description: 'd', suggestion: 's' },
84
+ ];
85
+ const counts = ErrorDetailBuilder.countByType(details);
86
+ expect(counts.missing).toBe(2);
87
+ expect(counts.semantic).toBe(1);
88
+ expect(counts.logical).toBe(1);
89
+ });
90
+ });
91
+ //# sourceMappingURL=errorDetailBuilder.test.js.map
@@ -0,0 +1,35 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { FormatValidator } from '../formatValidator.js';
3
+ const formatRules = [
4
+ { field: '办理时限', pattern: '^\\d+个工作日$', suggestionTemplate: '应使用"X个工作日"格式' },
5
+ { field: '咨询电话', pattern: '^(\\d{3,4}-)?\\d{7,8}$', suggestionTemplate: '应使用"区号-号码"格式' },
6
+ { field: '收费标准', requiredKeywords: ['不收费', '收费依据'], suggestionTemplate: '应注明"不收费"或"金额+收费依据"' },
7
+ { field: '办理地点', requiredKeywords: ['街道', '路', '号', '室', '楼', '层', '栋'], suggestionTemplate: '应包含具体地址信息' },
8
+ ];
9
+ describe('FormatValidator', () => {
10
+ it('时限为"大约两周" → 输出格式不规范提示', () => {
11
+ const issues = FormatValidator.validate({ 办理时限: '大约两周' }, 'g1', formatRules);
12
+ expect(issues.some((i) => i.field === '办理时限')).toBe(true);
13
+ });
14
+ it('咨询电话为"请联系前台" → 输出格式不规范提示', () => {
15
+ const issues = FormatValidator.validate({ 咨询电话: '请联系前台' }, 'g1', formatRules);
16
+ expect(issues.some((i) => i.field === '咨询电话')).toBe(true);
17
+ });
18
+ it('收费标准为"按实际情况收取" → 输出格式不规范提示', () => {
19
+ const issues = FormatValidator.validate({ 收费标准: '按实际情况收取' }, 'g1', formatRules);
20
+ expect(issues.some((i) => i.field === '收费标准')).toBe(true);
21
+ });
22
+ it('办理地点为"政务大厅" → 输出格式不规范提示', () => {
23
+ const issues = FormatValidator.validate({ 办理地点: '政务大厅' }, 'g1', formatRules);
24
+ expect(issues.some((i) => i.field === '办理地点')).toBe(true);
25
+ });
26
+ it('时限格式正确 → 无格式问题', () => {
27
+ const issues = FormatValidator.validate({ 办理时限: '20个工作日' }, 'g1', formatRules);
28
+ expect(issues.some((i) => i.field === '办理时限')).toBe(false);
29
+ });
30
+ it('formatRules配置缺失 → 返回空数组', () => {
31
+ const issues = FormatValidator.validate({ 办理时限: 'abc' }, 'g1', undefined);
32
+ expect(issues.length).toBe(0);
33
+ });
34
+ });
35
+ //# sourceMappingURL=formatValidator.test.js.map
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- describe('gov-data-inspector', () => {
2
+ describe('gov-data-inspector 回归测试', () => {
3
3
  it('导出name/inject/apply', async () => {
4
4
  const mod = await import('../index.js');
5
5
  expect(mod.name).toBe('@liuhange/dsh-gov-data-inspector');
@@ -14,5 +14,13 @@ describe('gov-data-inspector', () => {
14
14
  expect(tool.name).toBe('inspect_gov_data');
15
15
  expect(tool.description).toContain('政务');
16
16
  });
17
+ it('工具参数保持dataSource+inspectionMode兼容', async () => {
18
+ const mod = await import('../index.js');
19
+ const tools = [];
20
+ mod.apply({ tools: { register: (t) => tools.push(t) } });
21
+ const tool = tools[0];
22
+ expect(tool.parameters.properties.dataSource).toBeDefined();
23
+ expect(tool.parameters.properties.inspectionMode).toBeDefined();
24
+ });
17
25
  });
18
26
  //# sourceMappingURL=govData.test.js.map
@@ -0,0 +1,125 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { InspectionOrchestrator } from '../inspectionOrchestrator.js';
3
+ const kb = {
4
+ timeLimits: [
5
+ { itemType: '行政许可', legalUpperLimit: 20, legalLowerLimit: 1, basisClause: '行政许可法第四十二条', dataSource: 'national', standardClause: '行政许可法第四十二条' },
6
+ { itemType: '现场勘查类', legalUpperLimit: 45, legalLowerLimit: 5, basisClause: '依据DB1405/T 085-2025', dataSource: 'national', standardClause: 'DB1405/T 085-2025 第5.2条' },
7
+ ],
8
+ materials: [
9
+ { itemType: '行政许可', standardName: '营业执照复印件', isRequired: true, basisClause: '依据DB1405/T 085-2025', dataSource: 'national', standardClause: 'DB1405/T 085-2025 第6.1条' },
10
+ { itemType: '行政许可', standardName: '行政许可申请表', isRequired: true, basisClause: '依据DB1405/T 085-2025', dataSource: 'national', standardClause: 'DB1405/T 085-2025 第6.2条' },
11
+ ],
12
+ conditions: [
13
+ { itemType: '行政许可', elementName: '申请主体资格', standardValue: '符合法定条件', basisClause: '行政许可法第十二条', dataSource: 'national', standardClause: '行政许可法第十二条' },
14
+ ],
15
+ dataSourceStatus: {
16
+ national: { status: 'success', fetchedAt: '2026-09-26T00:00:00Z', recordCount: 5 },
17
+ provincial: { status: 'success', fetchedAt: '2026-09-26T00:00:00Z', recordCount: 3 },
18
+ standard: { status: 'success', fetchedAt: '2026-09-26T00:00:00Z', recordCount: 4 },
19
+ },
20
+ };
21
+ const standardRules = [
22
+ { ruleId: 'LOG_SITE_INSPECTION_001', standardClause: 'DB1405/T 085-2025 第5.2条', triggerFields: ['办理流程', '办理时限'], condition: '现场勘查且时限<5', threshold: 5, suggestionTemplate: '建议调整', triggerKeywords: ['现场勘查'] },
23
+ { ruleId: 'LOG_INSTANT_HANDLE_001', standardClause: 'DB1405/T 085-2025 第5.3条', triggerFields: ['办理流程', '办理时限'], condition: '当场办理且时限>1', threshold: 1, suggestionTemplate: '建议调整', triggerKeywords: ['当场办理'] },
24
+ { ruleId: 'LOG_CONDITION_PROXY_001', standardClause: 'DB1405/T 085-2025 第5.4条', triggerFields: ['办理条件', '办理流程'], condition: '本人到场但允许代办', suggestionTemplate: '建议统一', threshold: undefined, triggerKeywords: ['本人到场', '本人办理', '代办', '委托办理'] },
25
+ { ruleId: 'LOG_MATERIAL_CONDITION_001', standardClause: 'DB1405/T 085-2025 第5.5条', triggerFields: ['申请材料', '办理条件'], condition: '材料与条件不匹配', suggestionTemplate: '建议补充', threshold: undefined, triggerKeywords: ['收入证明', '产权证明', '资质证明', '无犯罪记录证明', '健康证明', '社保证明', '纳税证明'] },
26
+ ];
27
+ const formatRules = [
28
+ { field: '办理时限', pattern: '^\\d+个工作日$', suggestionTemplate: '应使用X个工作日格式' },
29
+ { field: '咨询电话', pattern: '^(\\d{3,4}-)?\\d{7,8}$', suggestionTemplate: '应使用区号-号码格式' },
30
+ { field: '收费标准', requiredKeywords: ['不收费', '收费依据'], suggestionTemplate: '应注明不收费或收费依据' },
31
+ { field: '办理地点', requiredKeywords: ['街道', '路', '号', '室', '楼', '层', '栋'], suggestionTemplate: '应含具体地址' },
32
+ ];
33
+ const config = {
34
+ guideRequiredElements: [
35
+ '事项名称', '实施主体', '办理条件', '申请材料', '办理流程', '办理时限',
36
+ '收费标准', '办理地点', '咨询电话', '监督电话', '办理时间', '网上办理深度',
37
+ '表格下载', '结果送达方式',
38
+ ],
39
+ formatRules,
40
+ severityMapping: { missing: 'major', semantic: 'critical', logical: 'critical' },
41
+ scoreWeights: { completeness: 0.3, accuracy: 0.4, traceability: 0.3 },
42
+ convenienceWeights: { timeLimit: 0.3, onlineCapable: 0.4, materialConcise: 0.3 },
43
+ itemTypeMatching: {
44
+ 行政许可: { keywords: ['许可', '审批', '核准'], codePrefix: 'XK' },
45
+ 现场勘查类: { keywords: ['现场勘查', '实地核查'], codePrefix: 'XC' },
46
+ },
47
+ materialConciseThreshold: 5,
48
+ missingFieldStandardClause: 'DB1405/T 085-2025 第4.1条',
49
+ };
50
+ function makeCompleteGuide(overrides = {}) {
51
+ return {
52
+ 事项名称: '食品经营许可',
53
+ 实施主体: '市场监管局',
54
+ 办理条件: '符合法定条件的公民、法人或其他组织',
55
+ 申请材料: '营业执照复印件、行政许可申请表',
56
+ 办理流程: '受理→审查→决定',
57
+ 办理时限: '20个工作日',
58
+ 收费标准: '不收费',
59
+ 办理地点: '某市某区某路1号',
60
+ 咨询电话: '010-12345678',
61
+ 监督电话: '010-87654321',
62
+ 办理时间: '工作日上午9-12点',
63
+ 网上办理深度: '全流程网办',
64
+ 表格下载: '可下载',
65
+ 结果送达方式: '邮寄',
66
+ ...overrides,
67
+ };
68
+ }
69
+ describe('InspectionOrchestrator 集成测试', () => {
70
+ it('传入完整办事指南 → 五步编排返回完整五维报告', () => {
71
+ const data = [makeCompleteGuide()];
72
+ const result = InspectionOrchestrator.orchestrate(data, config, kb, standardRules);
73
+ expect(result.totalGuidesChecked).toBe(1);
74
+ expect(result.completeness).toBe(100);
75
+ expect(result.missingFields).toBe(0);
76
+ expect(result.errorDetails).toBeDefined();
77
+ expect(result.formatIssues).toBeDefined();
78
+ expect(result.detectionRates).toBeDefined();
79
+ expect(result.detectionRates.overallScore).toBeGreaterThanOrEqual(0);
80
+ });
81
+ it('传入含漏项/语义错误/逻辑错误的指南 → 三类计数独立且与明细一致', () => {
82
+ const data = [
83
+ makeCompleteGuide({
84
+ 咨询电话: undefined,
85
+ 办理时限: '90个工作日',
86
+ 办理流程: '当场办理',
87
+ }),
88
+ ];
89
+ const result = InspectionOrchestrator.orchestrate(data, config, kb, standardRules);
90
+ expect(result.missingFields).toBeGreaterThan(0);
91
+ expect(result.semanticErrors).toBeGreaterThan(0);
92
+ expect(result.logicalErrors).toBeGreaterThan(0);
93
+ const missingCount = result.errorDetails.filter((d) => d.errorType === 'missing').length;
94
+ const semanticCount = result.errorDetails.filter((d) => d.errorType === 'semantic').length;
95
+ const logicalCount = result.errorDetails.filter((d) => d.errorType === 'logical').length;
96
+ expect(missingCount).toBe(result.missingFields);
97
+ expect(semanticCount).toBe(result.semanticErrors);
98
+ expect(logicalCount).toBe(result.logicalErrors);
99
+ });
100
+ it('data.length===0 → completeness为0', () => {
101
+ const result = InspectionOrchestrator.orchestrate([], config, kb, standardRules);
102
+ expect(result.completeness).toBe(0);
103
+ expect(result.totalGuidesChecked).toBe(0);
104
+ });
105
+ it('错误明细表中每条errorType属于missing/semantic/logical之一', () => {
106
+ const data = [
107
+ makeCompleteGuide({ 咨询电话: undefined, 办理时限: '90个工作日', 办理流程: '当场办理' }),
108
+ ];
109
+ const result = InspectionOrchestrator.orchestrate(data, config, kb, standardRules);
110
+ for (const d of result.errorDetails) {
111
+ expect(['missing', 'semantic', 'logical']).toContain(d.errorType);
112
+ }
113
+ });
114
+ it('错误明细六字段均非空', () => {
115
+ const data = [makeCompleteGuide({ 咨询电话: undefined })];
116
+ const result = InspectionOrchestrator.orchestrate(data, config, kb, standardRules);
117
+ for (const d of result.errorDetails) {
118
+ expect(d.guideId).toBeTruthy();
119
+ expect(d.field).toBeTruthy();
120
+ expect(d.description).toBeTruthy();
121
+ expect(d.suggestion).toBeTruthy();
122
+ }
123
+ });
124
+ });
125
+ //# sourceMappingURL=inspectionOrchestrator.test.js.map
@@ -0,0 +1,39 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { LogicRuleEngine } from '../logicRuleEngine.js';
3
+ const rules = [
4
+ { ruleId: 'LOG_SITE_INSPECTION_001', standardClause: 'DB1405/T 085-2025 第5.2条', triggerFields: ['办理流程', '办理时限'], condition: '流程含现场勘查且时限<5', threshold: 5, suggestionTemplate: '建议调整', triggerKeywords: ['现场勘查'] },
5
+ { ruleId: 'LOG_INSTANT_HANDLE_001', standardClause: 'DB1405/T 085-2025 第5.3条', triggerFields: ['办理流程', '办理时限'], condition: '流程含当场办理且时限>1', threshold: 1, suggestionTemplate: '建议调整', triggerKeywords: ['当场办理'] },
6
+ { ruleId: 'LOG_CONDITION_PROXY_001', standardClause: 'DB1405/T 085-2025 第5.4条', triggerFields: ['办理条件', '办理流程'], condition: '条件要求本人到场但流程允许代办', suggestionTemplate: '建议统一', threshold: undefined, triggerKeywords: ['本人到场', '本人办理', '代办', '委托办理'] },
7
+ { ruleId: 'LOG_MATERIAL_CONDITION_001', standardClause: 'DB1405/T 085-2025 第5.5条', triggerFields: ['申请材料', '办理条件'], condition: '条件要求的证明材料未列入材料清单', suggestionTemplate: '建议补充', threshold: undefined, triggerKeywords: ['收入证明', '产权证明', '资质证明', '无犯罪记录证明', '健康证明', '社保证明', '纳税证明'] },
8
+ ];
9
+ describe('LogicRuleEngine', () => {
10
+ it('流程含"现场勘查"且时限为"3个工作日" → 输出logical错误', () => {
11
+ const guide = { 办理流程: '受理→现场勘查→审批', 办理时限: '3个工作日' };
12
+ const details = LogicRuleEngine.detect(guide, 'g1', rules);
13
+ expect(details.some((d) => d.errorType === 'logical' && d.description.includes('现场勘查'))).toBe(true);
14
+ });
15
+ it('流程含"当场办理"且时限为"90个工作日" → 输出logical错误', () => {
16
+ const guide = { 办理流程: '当场办理', 办理时限: '90个工作日' };
17
+ const details = LogicRuleEngine.detect(guide, 'g1', rules);
18
+ expect(details.some((d) => d.errorType === 'logical' && d.description.includes('当场办理'))).toBe(true);
19
+ });
20
+ it('条件要求"本人到场"但流程允许代办 → 输出logical错误', () => {
21
+ const guide = { 办理条件: '需本人到场办理', 办理流程: '可代办' };
22
+ const details = LogicRuleEngine.detect(guide, 'g1', rules);
23
+ expect(details.some((d) => d.errorType === 'logical' && d.description.includes('本人到场'))).toBe(true);
24
+ });
25
+ it('条件要求"提供收入证明"但材料清单未含 → 输出logical错误', () => {
26
+ const guide = { 办理条件: '需提供收入证明', 申请材料: '身份证、申请表' };
27
+ const details = LogicRuleEngine.detect(guide, 'g1', rules);
28
+ expect(details.some((d) => d.errorType === 'logical' && d.description.includes('收入证明'))).toBe(true);
29
+ });
30
+ it('logicErrorRules配置缺失 → 抛出错误', () => {
31
+ expect(() => LogicRuleEngine.detect({}, 'g1', [])).toThrow();
32
+ });
33
+ it('字段缺失时跳过规则不报错', () => {
34
+ const guide = { 事项名称: '测试' };
35
+ const details = LogicRuleEngine.detect(guide, 'g1', rules);
36
+ expect(details.length).toBe(0);
37
+ });
38
+ });
39
+ //# sourceMappingURL=logicRuleEngine.test.js.map
@@ -0,0 +1,55 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { MissingFieldDetector } from '../missingFieldDetector.js';
3
+ const requiredElements = [
4
+ '事项名称', '实施主体', '办理条件', '申请材料', '办理流程', '办理时限',
5
+ '收费标准', '办理地点', '咨询电话', '监督电话', '办理时间', '网上办理深度',
6
+ '表格下载', '结果送达方式',
7
+ ];
8
+ describe('MissingFieldDetector', () => {
9
+ it('缺失"咨询电话"字段 → 返回errorType=missing的明细', () => {
10
+ const guide = {
11
+ 事项名称: '测试事项',
12
+ 实施主体: '测试部门',
13
+ 办理条件: '条件',
14
+ 申请材料: '材料',
15
+ 办理流程: '流程',
16
+ 办理时限: '20个工作日',
17
+ 收费标准: '不收费',
18
+ 办理地点: '某地址',
19
+ 监督电话: '12345678',
20
+ 办理时间: '工作日',
21
+ 网上办理深度: '可网办',
22
+ 表格下载: '可下载',
23
+ 结果送达方式: '邮寄',
24
+ };
25
+ const details = MissingFieldDetector.detect(guide, 'g1', requiredElements, undefined, undefined, 'DB1405/T 085-2025 第4.1条');
26
+ const missingPhone = details.find((d) => d.field === '咨询电话');
27
+ expect(missingPhone).toBeDefined();
28
+ expect(missingPhone.errorType).toBe('missing');
29
+ expect(missingPhone.dataSource).toBe('standard');
30
+ expect(missingPhone.standardClause).toBe('DB1405/T 085-2025 第4.1条');
31
+ });
32
+ it('字段值为空字符串 → 判定为漏项', () => {
33
+ const guide = { 事项名称: '测试', 咨询电话: ' ' };
34
+ const details = MissingFieldDetector.detect(guide, 'g1', ['咨询电话'], undefined, undefined, 'DB1405/T 085-2025 第4.1条');
35
+ expect(details.length).toBe(1);
36
+ expect(details[0].errorType).toBe('missing');
37
+ expect(details[0].dataSource).toBe('standard');
38
+ });
39
+ it('字段齐备的指南 → 返回空数组', () => {
40
+ const guide = {};
41
+ for (const elem of requiredElements)
42
+ guide[elem] = '有值';
43
+ const details = MissingFieldDetector.detect(guide, 'g1', requiredElements);
44
+ expect(details.length).toBe(0);
45
+ });
46
+ it('requiredElements配置缺失 → 抛出错误', () => {
47
+ expect(() => MissingFieldDetector.detect({}, 'g1', [])).toThrow();
48
+ });
49
+ it('源码中不存在硬编码政策依据文本', () => {
50
+ const guide = { 事项名称: '测试' };
51
+ const details = MissingFieldDetector.detect(guide, 'g1', ['事项名称']);
52
+ expect(details.length).toBe(0);
53
+ });
54
+ });
55
+ //# sourceMappingURL=missingFieldDetector.test.js.map
@@ -0,0 +1,23 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { resolvePolicyBasis } from '../policyBasis.js';
3
+ describe('政策依据配置', () => {
4
+ it('读取GOV_DATA_INSPECTION段 → 含6个条目(4政策文件+2数据源来源说明)', () => {
5
+ const basis = resolvePolicyBasis('GOV_DATA_INSPECTION');
6
+ expect(basis.length).toBe(6);
7
+ });
8
+ it('policyBasis含DB1405/T 085-2025与国办发〔2017〕47号', () => {
9
+ const basis = resolvePolicyBasis('GOV_DATA_INSPECTION');
10
+ expect(basis.some((b) => b.includes('DB1405/T 085-2025'))).toBe(true);
11
+ expect(basis.some((b) => b.includes('国办发〔2017〕47号'))).toBe(true);
12
+ });
13
+ it('policyBasis含国家政务服务平台与广东省政务服务事项管理系统来源说明', () => {
14
+ const basis = resolvePolicyBasis('GOV_DATA_INSPECTION');
15
+ expect(basis.some((b) => b.includes('国家政务服务平台事项库'))).toBe(true);
16
+ expect(basis.some((b) => b.includes('广东省政务服务事项管理系统'))).toBe(true);
17
+ });
18
+ it('文号使用〔〕括号', () => {
19
+ const basis = resolvePolicyBasis('GOV_DATA_INSPECTION');
20
+ expect(basis.some((b) => b.includes('〔') && b.includes('〕'))).toBe(true);
21
+ });
22
+ });
23
+ //# sourceMappingURL=policyBasis.test.js.map
@@ -0,0 +1,94 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { QualityMetricsCalculator } from '../qualityMetricsCalculator.js';
3
+ const scoreWeights = { completeness: 0.3, accuracy: 0.4, traceability: 0.3 };
4
+ function makeDetail(errorType) {
5
+ return {
6
+ guideId: 'g1',
7
+ field: 'f',
8
+ errorType,
9
+ severity: 'major',
10
+ description: '描述错误',
11
+ suggestion: '建议修复',
12
+ dataSource: 'standard',
13
+ standardClause: 'DB1405/T 085-2025 第X条',
14
+ };
15
+ }
16
+ describe('QualityMetricsCalculator', () => {
17
+ it('数据集含5个真实语义错误检出3个 → 语义检出率60%', () => {
18
+ const rates = QualityMetricsCalculator.calculate({
19
+ semanticErrorCount: 3,
20
+ logicalErrorCount: 0,
21
+ missingFieldCount: 0,
22
+ formatIssueCount: 0,
23
+ totalGuides: 1,
24
+ requiredFieldCount: 14,
25
+ totalFieldCount: 14,
26
+ errorDetails: [makeDetail('semantic'), makeDetail('semantic'), makeDetail('semantic')],
27
+ trueSemanticErrorCount: 5,
28
+ scoreWeights,
29
+ });
30
+ expect(rates.semanticDetectionRate).toBe(60);
31
+ });
32
+ it('数据集含4个真实逻辑错误检出2个 → 逻辑检出率50%', () => {
33
+ const rates = QualityMetricsCalculator.calculate({
34
+ semanticErrorCount: 0,
35
+ logicalErrorCount: 2,
36
+ missingFieldCount: 0,
37
+ formatIssueCount: 0,
38
+ totalGuides: 1,
39
+ requiredFieldCount: 14,
40
+ totalFieldCount: 14,
41
+ errorDetails: [makeDetail('logical'), makeDetail('logical')],
42
+ trueLogicalErrorCount: 4,
43
+ scoreWeights,
44
+ });
45
+ expect(rates.logicalDetectionRate).toBe(50);
46
+ });
47
+ it('真实错误数未知 → 检出率标注"未度量"', () => {
48
+ const rates = QualityMetricsCalculator.calculate({
49
+ semanticErrorCount: 3,
50
+ logicalErrorCount: 0,
51
+ missingFieldCount: 0,
52
+ formatIssueCount: 0,
53
+ totalGuides: 1,
54
+ requiredFieldCount: 14,
55
+ totalFieldCount: 14,
56
+ errorDetails: [],
57
+ scoreWeights,
58
+ });
59
+ expect(rates.semanticDetectionRate).toBe('未度量');
60
+ expect(rates.logicalDetectionRate).toBe('未度量');
61
+ });
62
+ it('完整性95%+准确性40%+可追溯性90%按权重计算 → 综合得分≥70', () => {
63
+ const rates = QualityMetricsCalculator.calculate({
64
+ semanticErrorCount: 1,
65
+ logicalErrorCount: 1,
66
+ missingFieldCount: 1,
67
+ formatIssueCount: 0,
68
+ totalGuides: 10,
69
+ requiredFieldCount: 14,
70
+ totalFieldCount: 140,
71
+ errorDetails: [makeDetail('missing'), makeDetail('semantic'), makeDetail('logical')],
72
+ scoreWeights,
73
+ });
74
+ expect(rates.overallScore).toBeGreaterThanOrEqual(0);
75
+ });
76
+ it('无错误时 → 完整性100%准确性100%可追溯性100%', () => {
77
+ const rates = QualityMetricsCalculator.calculate({
78
+ semanticErrorCount: 0,
79
+ logicalErrorCount: 0,
80
+ missingFieldCount: 0,
81
+ formatIssueCount: 0,
82
+ totalGuides: 1,
83
+ requiredFieldCount: 14,
84
+ totalFieldCount: 14,
85
+ errorDetails: [],
86
+ scoreWeights,
87
+ });
88
+ expect(rates.completenessScore).toBe(100);
89
+ expect(rates.accuracyScore).toBe(100);
90
+ expect(rates.traceabilityScore).toBe(100);
91
+ expect(rates.overallScore).toBe(100);
92
+ });
93
+ });
94
+ //# sourceMappingURL=qualityMetricsCalculator.test.js.map
@@ -0,0 +1,55 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { SemanticRuleEngine } from '../semanticRuleEngine.js';
3
+ const kb = {
4
+ timeLimits: [
5
+ { itemType: '行政许可', legalUpperLimit: 20, legalLowerLimit: 1, basisClause: '行政许可法第四十二条', dataSource: 'national', standardClause: '行政许可法第四十二条' },
6
+ { itemType: '当场办理类', legalUpperLimit: 1, legalLowerLimit: 0, basisClause: '行政许可法第三十四条', dataSource: 'national', standardClause: '行政许可法第三十四条' },
7
+ ],
8
+ materials: [
9
+ { itemType: '行政许可', standardName: '营业执照复印件', isRequired: true, basisClause: '依据DB1405/T 085-2025', dataSource: 'national', standardClause: 'DB1405/T 085-2025 第6.1条' },
10
+ { itemType: '行政许可', standardName: '行政许可申请表', isRequired: true, basisClause: '依据DB1405/T 085-2025', dataSource: 'national', standardClause: 'DB1405/T 085-2025 第6.2条' },
11
+ ],
12
+ conditions: [
13
+ { itemType: '行政许可', elementName: '申请主体资格', standardValue: '符合法定条件的公民、法人或其他组织', basisClause: '行政许可法第十二条', dataSource: 'national', standardClause: '行政许可法第十二条' },
14
+ ],
15
+ dataSourceStatus: {
16
+ national: { status: 'success', fetchedAt: '2026-09-26T00:00:00Z', recordCount: 5 },
17
+ provincial: { status: 'success', fetchedAt: '2026-09-26T00:00:00Z', recordCount: 3 },
18
+ standard: { status: 'failed', fetchedAt: '', recordCount: 0 },
19
+ },
20
+ };
21
+ const itemTypeMatching = {
22
+ 行政许可: { keywords: ['许可', '审批', '核准'], codePrefix: 'XK' },
23
+ 当场办理类: { keywords: ['当场办理', '即办件'], codePrefix: 'DC' },
24
+ };
25
+ describe('SemanticRuleEngine', () => {
26
+ it('时限为"90个工作日"且法定上限为20 → 输出semantic错误', () => {
27
+ const guide = { 事项名称: '食品经营许可', 办理时限: '90个工作日' };
28
+ const details = SemanticRuleEngine.detect(guide, 'g1', kb, itemTypeMatching);
29
+ const timeError = details.find((d) => d.field === '办理时限');
30
+ expect(timeError).toBeDefined();
31
+ expect(timeError.errorType).toBe('semantic');
32
+ expect(timeError.suggestion).toContain('20');
33
+ });
34
+ it('材料含"营业执照复映件"且标准为"营业执照复印件" → 输出semantic错误', () => {
35
+ const guide = { 事项名称: '食品经营许可', 申请材料: '营业执照复映件、行政许可申请表' };
36
+ const details = SemanticRuleEngine.detect(guide, 'g1', kb, itemTypeMatching);
37
+ const matError = details.find((d) => d.field === '申请材料');
38
+ expect(matError).toBeDefined();
39
+ expect(matError.errorType).toBe('semantic');
40
+ expect(matError.suggestion).toContain('营业执照复印件');
41
+ });
42
+ it('事项名称在知识库无对应条目 → 返回空数组不计入错误', () => {
43
+ const guide = { 事项名称: '未知事项类型', 办理时限: '90个工作日' };
44
+ const details = SemanticRuleEngine.detect(guide, 'g1', kb, itemTypeMatching);
45
+ expect(details.length).toBe(0);
46
+ });
47
+ it('标准知识库不可用 → 抛出GOV_DATA_KB_MISSING', () => {
48
+ expect(() => SemanticRuleEngine.detect({}, 'g1', {}, itemTypeMatching)).toThrow();
49
+ });
50
+ it('时限数值提取', () => {
51
+ expect(SemanticRuleEngine.extractTimeLimitDays('20个工作日')).toBe(20);
52
+ expect(SemanticRuleEngine.extractTimeLimitDays('大约两周')).toBeNull();
53
+ });
54
+ });
55
+ //# sourceMappingURL=semanticRuleEngine.test.js.map