@liuhange/dsh-gov-data-inspector 3.1.3 → 3.1.5
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/cordis.patch.yml +9 -0
- package/lib/__tests__/exceptionInput.test.js +226 -0
- package/lib/__tests__/formatValidator.test.js +29 -0
- package/lib/__tests__/input-boundary-checker.test.js +30 -0
- package/lib/__tests__/local-terms-loader.test.js +38 -0
- package/lib/__tests__/logicRuleEngine.test.js +38 -0
- package/lib/__tests__/semanticRuleEngine.test.js +75 -9
- package/lib/dataScaleGuard.js +19 -0
- package/lib/degradedModeController.js +21 -0
- package/lib/encodingDetector.js +31 -0
- package/lib/endpointResolver.js +21 -0
- package/lib/formatValidator.js +12 -4
- package/lib/index.js +113 -25
- package/lib/input-boundary-checker.js +20 -0
- package/lib/inspectionOrchestrator.js +8 -3
- package/lib/knowledgeBaseLoader.js +9 -4
- package/lib/local-terms-loader.js +26 -0
- package/lib/logicRuleEngine.js +29 -0
- package/lib/nationalDataSource.js +34 -19
- package/lib/provincialDataSource.js +34 -19
- package/lib/semanticRuleEngine.js +74 -12
- package/lib/unmatchedWarningBuilder.js +10 -0
- package/lib/urlReachabilityChecker.js +28 -0
- package/package.json +10 -9
- package/LICENSE +0 -21
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Cordis Framework Patch Declaration for @liuhange/dsh-gov-data-inspector
|
|
2
|
+
# This file declares the plugin's runtime patch for the Cordis framework.
|
|
3
|
+
# Used by DSH plugin manager to apply framework-level patches during installation.
|
|
4
|
+
plugin: @liuhange/dsh-gov-data-inspector
|
|
5
|
+
version: 3.1.5
|
|
6
|
+
patch:
|
|
7
|
+
- target: tools
|
|
8
|
+
action: register
|
|
9
|
+
description: Register plugin tools into Cordis runtime
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { writeFileSync, mkdirSync, rmSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
const tmpDir = resolve(tmpdir(), 'gov-data-inspector-test');
|
|
6
|
+
const badJsonPath = resolve(tmpDir, 'bad.json');
|
|
7
|
+
const emptyPath = resolve(tmpDir, 'empty.json');
|
|
8
|
+
const emptyArrayPath = resolve(tmpDir, 'empty-array.json');
|
|
9
|
+
const nonUtf8Path = resolve(tmpDir, 'non-utf8.json');
|
|
10
|
+
const partialDataPath = resolve(tmpDir, 'partial.json');
|
|
11
|
+
const validDataPath = resolve(tmpDir, 'valid.json');
|
|
12
|
+
beforeAll(() => {
|
|
13
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
14
|
+
writeFileSync(badJsonPath, '{ "事项名称": "测试", invalid json }', 'utf-8');
|
|
15
|
+
writeFileSync(emptyPath, '', 'utf-8');
|
|
16
|
+
writeFileSync(emptyArrayPath, '[]', 'utf-8');
|
|
17
|
+
const gbkBuffer = Buffer.from([0xB2, 0xE2, 0xCA, 0xD4]);
|
|
18
|
+
writeFileSync(nonUtf8Path, gbkBuffer);
|
|
19
|
+
writeFileSync(partialDataPath, JSON.stringify([{ 事项名称: '测试事项' }]), 'utf-8');
|
|
20
|
+
writeFileSync(validDataPath, JSON.stringify([{ 事项名称: '食品经营许可', 办理时限: '20个工作日' }]), 'utf-8');
|
|
21
|
+
});
|
|
22
|
+
afterAll(() => {
|
|
23
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
24
|
+
});
|
|
25
|
+
async function getExecute() {
|
|
26
|
+
const mod = await import('../index.js');
|
|
27
|
+
const tools = [];
|
|
28
|
+
mod.apply({ tools: { register: (t) => tools.push(t) } });
|
|
29
|
+
const tool = tools[0];
|
|
30
|
+
return tool.execute;
|
|
31
|
+
}
|
|
32
|
+
describe('异常输入处理', () => {
|
|
33
|
+
it('格式错误的JSON → 返回GOV_DATA_INPUT_INVALID', async () => {
|
|
34
|
+
const execute = await getExecute();
|
|
35
|
+
const result = JSON.parse(await execute({ dataSource: badJsonPath }));
|
|
36
|
+
expect(result.error).toBe('GOV_DATA_INPUT_INVALID');
|
|
37
|
+
expect(result.message).toContain('JSON 格式错误');
|
|
38
|
+
});
|
|
39
|
+
it('空数据(空文件) → 返回无数据提示', async () => {
|
|
40
|
+
const execute = await getExecute();
|
|
41
|
+
const result = JSON.parse(await execute({ dataSource: emptyPath }));
|
|
42
|
+
expect(result.error).toBe('GOV_DATA_INPUT_INVALID');
|
|
43
|
+
expect(result.message).toContain('无数据');
|
|
44
|
+
});
|
|
45
|
+
it('空数据(空数组) → 返回无数据提示', async () => {
|
|
46
|
+
const execute = await getExecute();
|
|
47
|
+
const result = JSON.parse(await execute({ dataSource: emptyArrayPath }));
|
|
48
|
+
expect(result.error).toBe('GOV_DATA_INPUT_INVALID');
|
|
49
|
+
expect(result.message).toContain('无数据');
|
|
50
|
+
});
|
|
51
|
+
it('无效URL → 返回不可达提示而非崩溃', async () => {
|
|
52
|
+
const execute = await getExecute();
|
|
53
|
+
const result = JSON.parse(await execute({ dataSource: 'http://invalid.localhost.nonexistent:99999/data.json' }));
|
|
54
|
+
expect(result.error).toBeDefined();
|
|
55
|
+
expect(result.message).toBeDefined();
|
|
56
|
+
});
|
|
57
|
+
it('部分字段缺失的数据 → 正常处理并报告缺失字段', async () => {
|
|
58
|
+
const execute = await getExecute();
|
|
59
|
+
const result = JSON.parse(await execute({ dataSource: partialDataPath, inspectionMode: 'guide' }));
|
|
60
|
+
expect(result.error).toBeUndefined();
|
|
61
|
+
expect(result.guideInspection).toBeDefined();
|
|
62
|
+
expect(result.guideInspection.missingFields).toBeGreaterThan(0);
|
|
63
|
+
});
|
|
64
|
+
it('非UTF-8编码文件 → 返回编码错误提示', async () => {
|
|
65
|
+
const execute = await getExecute();
|
|
66
|
+
const result = JSON.parse(await execute({ dataSource: nonUtf8Path }));
|
|
67
|
+
expect(result.error).toBe('GOV_DATA_ENCODING_ERROR');
|
|
68
|
+
expect(result.message).toContain('UTF-8');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe('EncodingDetector', () => {
|
|
72
|
+
it('UTF-8文件正常读取', async () => {
|
|
73
|
+
const { EncodingDetector } = await import('../encodingDetector.js');
|
|
74
|
+
const result = EncodingDetector.detect(validDataPath);
|
|
75
|
+
expect(result.content).not.toBeNull();
|
|
76
|
+
expect(result.encodingError).toBeUndefined();
|
|
77
|
+
});
|
|
78
|
+
it('非UTF-8文件返回编码错误', async () => {
|
|
79
|
+
const { EncodingDetector } = await import('../encodingDetector.js');
|
|
80
|
+
const result = EncodingDetector.detect(nonUtf8Path);
|
|
81
|
+
expect(result.content).toBeNull();
|
|
82
|
+
expect(result.encodingError).toContain('UTF-8');
|
|
83
|
+
});
|
|
84
|
+
it('不存在的文件返回读取错误', async () => {
|
|
85
|
+
const { EncodingDetector } = await import('../encodingDetector.js');
|
|
86
|
+
const result = EncodingDetector.detect(resolve(tmpDir, 'nonexistent.json'));
|
|
87
|
+
expect(result.content).toBeNull();
|
|
88
|
+
expect(result.encodingError).toContain('无法读取');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
describe('DataScaleGuard', () => {
|
|
92
|
+
it('正常规模数据通过', async () => {
|
|
93
|
+
const { DataScaleGuard } = await import('../dataScaleGuard.js');
|
|
94
|
+
const data = [{ a: 1, b: 2 }, { c: 3 }];
|
|
95
|
+
const result = DataScaleGuard.check(data);
|
|
96
|
+
expect(result.exceeded).toBe(false);
|
|
97
|
+
expect(result.fieldCount).toBe(3);
|
|
98
|
+
});
|
|
99
|
+
it('超100万字段数据返回exceeded=true', async () => {
|
|
100
|
+
const { DataScaleGuard } = await import('../dataScaleGuard.js');
|
|
101
|
+
const data = [];
|
|
102
|
+
for (let i = 0; i < 100001; i++)
|
|
103
|
+
data.push({ f: i });
|
|
104
|
+
const result = DataScaleGuard.check(data, 100000);
|
|
105
|
+
expect(result.exceeded).toBe(true);
|
|
106
|
+
expect(result.fieldCount).toBeGreaterThan(100000);
|
|
107
|
+
});
|
|
108
|
+
it('自定义上限', async () => {
|
|
109
|
+
const { DataScaleGuard } = await import('../dataScaleGuard.js');
|
|
110
|
+
const result = DataScaleGuard.check([{ a: 1, b: 2, c: 3 }], 2);
|
|
111
|
+
expect(result.exceeded).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
describe('UrlReachabilityChecker', () => {
|
|
115
|
+
it('本地文件存在 → reachable=true', async () => {
|
|
116
|
+
const { UrlReachabilityChecker } = await import('../urlReachabilityChecker.js');
|
|
117
|
+
const result = await UrlReachabilityChecker.check(validDataPath);
|
|
118
|
+
expect(result.reachable).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
it('不存在的文件 → reachable=false', async () => {
|
|
121
|
+
const { UrlReachabilityChecker } = await import('../urlReachabilityChecker.js');
|
|
122
|
+
const result = await UrlReachabilityChecker.check(resolve(tmpDir, 'nonexistent.json'));
|
|
123
|
+
expect(result.reachable).toBe(false);
|
|
124
|
+
expect(result.error).toBeDefined();
|
|
125
|
+
});
|
|
126
|
+
it('空路径 → reachable=false', async () => {
|
|
127
|
+
const { UrlReachabilityChecker } = await import('../urlReachabilityChecker.js');
|
|
128
|
+
const result = await UrlReachabilityChecker.check('');
|
|
129
|
+
expect(result.reachable).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
describe('EndpointResolver', () => {
|
|
133
|
+
it('HTTPS URL → kind=https', async () => {
|
|
134
|
+
const { EndpointResolver } = await import('../endpointResolver.js');
|
|
135
|
+
const result = EndpointResolver.resolve('https://api.example.com/data');
|
|
136
|
+
expect(result?.kind).toBe('https');
|
|
137
|
+
});
|
|
138
|
+
it('HTTP URL → kind=http', async () => {
|
|
139
|
+
const { EndpointResolver } = await import('../endpointResolver.js');
|
|
140
|
+
const result = EndpointResolver.resolve('http://api.example.com/data');
|
|
141
|
+
expect(result?.kind).toBe('http');
|
|
142
|
+
});
|
|
143
|
+
it('本地文件路径 → kind=file', async () => {
|
|
144
|
+
const { EndpointResolver } = await import('../endpointResolver.js');
|
|
145
|
+
const result = EndpointResolver.resolve(validDataPath);
|
|
146
|
+
expect(result?.kind).toBe('file');
|
|
147
|
+
});
|
|
148
|
+
it('相对路径 → kind=file', async () => {
|
|
149
|
+
const { EndpointResolver } = await import('../endpointResolver.js');
|
|
150
|
+
const result = EndpointResolver.resolve('./data.json');
|
|
151
|
+
expect(result?.kind).toBe('file');
|
|
152
|
+
});
|
|
153
|
+
it('非法字符串 → null', async () => {
|
|
154
|
+
const { EndpointResolver } = await import('../endpointResolver.js');
|
|
155
|
+
const result = EndpointResolver.resolve('not-a-valid-endpoint');
|
|
156
|
+
expect(result).toBeNull();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
describe('DegradedModeController', () => {
|
|
160
|
+
it('activate返回degradedMode=true', async () => {
|
|
161
|
+
const { DegradedModeController } = await import('../degradedModeController.js');
|
|
162
|
+
const result = DegradedModeController.activate('测试降级原因');
|
|
163
|
+
expect(result.degradedMode).toBe(true);
|
|
164
|
+
expect(result.degradedReason).toBe('测试降级原因');
|
|
165
|
+
expect(result.localKb).toBeDefined();
|
|
166
|
+
expect(result.localKb.timeLimits).toEqual([]);
|
|
167
|
+
expect(result.localKb.materials).toEqual([]);
|
|
168
|
+
expect(result.localKb.conditions).toEqual([]);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
describe('UnmatchedWarningBuilder', () => {
|
|
172
|
+
it('build返回正确警告结构', async () => {
|
|
173
|
+
const { UnmatchedWarningBuilder } = await import('../unmatchedWarningBuilder.js');
|
|
174
|
+
const warning = UnmatchedWarningBuilder.build('guide-001');
|
|
175
|
+
expect(warning.type).toBe('UNMATCHED_ITEM_TYPE');
|
|
176
|
+
expect(warning.guideId).toBe('guide-001');
|
|
177
|
+
expect(warning.message).toContain('未匹配到检测规则');
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
describe('输入边界友好提示', () => {
|
|
181
|
+
const emptyObjectPath = resolve(tmpDir, 'empty-object.json');
|
|
182
|
+
const tooLargePath = resolve(tmpDir, 'too-large.json');
|
|
183
|
+
const atThresholdPath = resolve(tmpDir, 'at-threshold.json');
|
|
184
|
+
beforeAll(() => {
|
|
185
|
+
writeFileSync(emptyObjectPath, JSON.stringify({}), 'utf-8');
|
|
186
|
+
writeFileSync(tooLargePath, 'a'.repeat(100000), 'utf-8');
|
|
187
|
+
writeFileSync(atThresholdPath, JSON.stringify([{ 事项名称: '测试', 办理时限: '20个工作日' }]).padEnd(50000, ' '), 'utf-8');
|
|
188
|
+
});
|
|
189
|
+
it('IDX-01: 空对象{} → 返回GOV_DATA_NO_DATA', async () => {
|
|
190
|
+
const execute = await getExecute();
|
|
191
|
+
const result = JSON.parse(await execute({ dataSource: emptyObjectPath, inspectionMode: 'guide' }));
|
|
192
|
+
expect(result.error).toBe('GOV_DATA_NO_DATA');
|
|
193
|
+
expect(result.message).toContain('未检测到有效数据');
|
|
194
|
+
});
|
|
195
|
+
it('IDX-02: 10万字符 → 返回GOV_DATA_INPUT_TOO_LARGE', async () => {
|
|
196
|
+
const execute = await getExecute();
|
|
197
|
+
const result = JSON.parse(await execute({ dataSource: tooLargePath }));
|
|
198
|
+
expect(result.error).toBe('GOV_DATA_INPUT_TOO_LARGE');
|
|
199
|
+
expect(result.message).toContain('超过处理上限');
|
|
200
|
+
expect(result.message).toContain('分批提交');
|
|
201
|
+
});
|
|
202
|
+
it('IDX-03: 50000字符 → 正常处理无超长错误', async () => {
|
|
203
|
+
const execute = await getExecute();
|
|
204
|
+
const result = JSON.parse(await execute({ dataSource: atThresholdPath }));
|
|
205
|
+
expect(result.error).not.toBe('GOV_DATA_INPUT_TOO_LARGE');
|
|
206
|
+
});
|
|
207
|
+
it('IDX-04: 空对象不进入漏项检测不报告14个漏项', async () => {
|
|
208
|
+
const execute = await getExecute();
|
|
209
|
+
const result = JSON.parse(await execute({ dataSource: emptyObjectPath, inspectionMode: 'guide' }));
|
|
210
|
+
expect(result.guideInspection).toBeUndefined();
|
|
211
|
+
});
|
|
212
|
+
it('IDX-05: 空对象不报告missingFields', async () => {
|
|
213
|
+
const execute = await getExecute();
|
|
214
|
+
const result = JSON.parse(await execute({ dataSource: emptyObjectPath, inspectionMode: 'guide' }));
|
|
215
|
+
expect(result.missingFields).toBeUndefined();
|
|
216
|
+
});
|
|
217
|
+
it('IDX-06: 降级模式+含笔误数据 → 报告含degradedMode和保底校验结果', async () => {
|
|
218
|
+
const typoDataPath = resolve(tmpDir, 'typo-data.json');
|
|
219
|
+
writeFileSync(typoDataPath, JSON.stringify([{ 事项名称: '食品经营许可', 申请材料: '身份证复件', 办理时限: '20个工作日' }]), 'utf-8');
|
|
220
|
+
const execute = await getExecute();
|
|
221
|
+
const result = JSON.parse(await execute({ dataSource: typoDataPath, inspectionMode: 'guide' }));
|
|
222
|
+
expect(result.degradedMode).toBe(true);
|
|
223
|
+
expect(result.degradedReason).toBeDefined();
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
//# sourceMappingURL=exceptionInput.test.js.map
|
|
@@ -31,5 +31,34 @@ describe('FormatValidator', () => {
|
|
|
31
31
|
const issues = FormatValidator.validate({ 办理时限: 'abc' }, 'g1', undefined);
|
|
32
32
|
expect(issues.length).toBe(0);
|
|
33
33
|
});
|
|
34
|
+
it('FV-01: 时限值"7个自然日" formatIssues含semanticHint含"自然日而非工作日"', () => {
|
|
35
|
+
const issues = FormatValidator.validate({ 办理时限: '7个自然日' }, 'g1', formatRules);
|
|
36
|
+
const timeLimitIssue = issues.find((i) => i.field === '办理时限');
|
|
37
|
+
expect(timeLimitIssue).toBeDefined();
|
|
38
|
+
expect(timeLimitIssue?.semanticHint).toContain("自然日");
|
|
39
|
+
expect(timeLimitIssue?.semanticHint).toContain("工作日");
|
|
40
|
+
});
|
|
41
|
+
it('FV-02: 时限值"大约两周" semanticHint为undefined', () => {
|
|
42
|
+
const issues = FormatValidator.validate({ 办理时限: '大约两周' }, 'g1', formatRules);
|
|
43
|
+
const timeLimitIssue = issues.find((i) => i.field === '办理时限');
|
|
44
|
+
expect(timeLimitIssue).toBeDefined();
|
|
45
|
+
expect(timeLimitIssue?.semanticHint).toBeUndefined();
|
|
46
|
+
});
|
|
47
|
+
it('FV-03: 咨询电话"请联系前台" semanticHint为undefined', () => {
|
|
48
|
+
const issues = FormatValidator.validate({ 咨询电话: '请联系前台' }, 'g1', formatRules);
|
|
49
|
+
const phoneIssue = issues.find((i) => i.field === '咨询电话');
|
|
50
|
+
expect(phoneIssue).toBeDefined();
|
|
51
|
+
expect(phoneIssue?.semanticHint).toBeUndefined();
|
|
52
|
+
});
|
|
53
|
+
it('FV-04: 双标注主归类仍归属formatIssues不计入semanticErrors', () => {
|
|
54
|
+
const issues = FormatValidator.validate({ 办理时限: '7个自然日' }, 'g1', formatRules);
|
|
55
|
+
expect(issues.some((i) => i.field === '办理时限' && i.semanticHint !== undefined)).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
it('FV-05: 双标注policyBasis保留原有格式校验政策依据', () => {
|
|
58
|
+
const issues = FormatValidator.validate({ 办理时限: '7个自然日' }, 'g1', formatRules);
|
|
59
|
+
const timeLimitIssue = issues.find((i) => i.field === '办理时限');
|
|
60
|
+
expect(timeLimitIssue).toBeDefined();
|
|
61
|
+
expect(timeLimitIssue?.issue).toContain('格式要求');
|
|
62
|
+
});
|
|
34
63
|
});
|
|
35
64
|
//# sourceMappingURL=formatValidator.test.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { InputBoundaryChecker } from '../input-boundary-checker.js';
|
|
3
|
+
describe('InputBoundaryChecker', () => {
|
|
4
|
+
it('IBC-01: checkEmpty([{}]) 返回 isEmpty=true', () => {
|
|
5
|
+
const result = InputBoundaryChecker.checkEmpty([{}]);
|
|
6
|
+
expect(result.isEmpty).toBe(true);
|
|
7
|
+
});
|
|
8
|
+
it('IBC-02: checkEmpty([{事项名称:"测试"}]) 返回 isEmpty=false', () => {
|
|
9
|
+
const result = InputBoundaryChecker.checkEmpty([{ 事项名称: '测试' }]);
|
|
10
|
+
expect(result.isEmpty).toBe(false);
|
|
11
|
+
});
|
|
12
|
+
it('IBC-03: checkEmpty([{a:""},{b:null}]) 返回 isEmpty=true', () => {
|
|
13
|
+
const result = InputBoundaryChecker.checkEmpty([{ a: '' }, { b: null }]);
|
|
14
|
+
expect(result.isEmpty).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
it('IBC-04: checkTooLarge 50001字符 阈值50000 返回 isTooLarge=true', () => {
|
|
17
|
+
const result = InputBoundaryChecker.checkTooLarge('a'.repeat(50001), 50000);
|
|
18
|
+
expect(result.isTooLarge).toBe(true);
|
|
19
|
+
expect(result.length).toBe(50001);
|
|
20
|
+
});
|
|
21
|
+
it('IBC-05: checkTooLarge 50000字符 阈值50000 返回 isTooLarge=false', () => {
|
|
22
|
+
const result = InputBoundaryChecker.checkTooLarge('a'.repeat(50000), 50000);
|
|
23
|
+
expect(result.isTooLarge).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
it('IBC-06: checkTooLarge 100字符 阈值50000 返回 isTooLarge=false', () => {
|
|
26
|
+
const result = InputBoundaryChecker.checkTooLarge('a'.repeat(100), 50000);
|
|
27
|
+
expect(result.isTooLarge).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
//# sourceMappingURL=input-boundary-checker.test.js.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { LocalTermsLoader } from '../local-terms-loader.js';
|
|
3
|
+
import { writeFileSync, mkdirSync, existsSync, rmSync } from 'node:fs';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
describe('LocalTermsLoader', () => {
|
|
6
|
+
const tmpDir = resolve(process.cwd(), 'tmp-test-terms');
|
|
7
|
+
const tmpFile = resolve(tmpDir, 'test-terms.json');
|
|
8
|
+
it('LTL-01: 有效词表路径正常加载', () => {
|
|
9
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
10
|
+
writeFileSync(tmpFile, JSON.stringify({ materials: ['身份证原件', '申请表'], conditions: ['年满18周岁'] }), 'utf-8');
|
|
11
|
+
const result = LocalTermsLoader.load(tmpFile);
|
|
12
|
+
expect(result.materials).toContain('身份证原件');
|
|
13
|
+
expect(result.materials).toContain('申请表');
|
|
14
|
+
expect(result.conditions).toContain('年满18周岁');
|
|
15
|
+
});
|
|
16
|
+
it('LTL-02: 不存在路径返回空词表不抛错', () => {
|
|
17
|
+
const result = LocalTermsLoader.load('/nonexistent/path/terms.json');
|
|
18
|
+
expect(result.materials).toEqual([]);
|
|
19
|
+
expect(result.conditions).toEqual([]);
|
|
20
|
+
});
|
|
21
|
+
it('LTL-03: 空词表文件返回空词表不抛错', () => {
|
|
22
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
23
|
+
writeFileSync(tmpFile, JSON.stringify({ materials: [], conditions: [] }), 'utf-8');
|
|
24
|
+
const result = LocalTermsLoader.load(tmpFile);
|
|
25
|
+
expect(result.materials).toEqual([]);
|
|
26
|
+
expect(result.conditions).toEqual([]);
|
|
27
|
+
});
|
|
28
|
+
it('LTL-04: 未配置路径使用默认路径加载', () => {
|
|
29
|
+
const result = LocalTermsLoader.load();
|
|
30
|
+
expect(Array.isArray(result.materials)).toBe(true);
|
|
31
|
+
expect(Array.isArray(result.conditions)).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
it('cleanup', () => {
|
|
34
|
+
if (existsSync(tmpDir))
|
|
35
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
//# sourceMappingURL=local-terms-loader.test.js.map
|
|
@@ -5,6 +5,7 @@ const rules = [
|
|
|
5
5
|
{ ruleId: 'LOG_INSTANT_HANDLE_001', standardClause: 'DB1405/T 085-2025 第5.3条', triggerFields: ['办理流程', '办理时限'], condition: '流程含当场办理且时限>1', threshold: 1, suggestionTemplate: '建议调整', triggerKeywords: ['当场办理'] },
|
|
6
6
|
{ ruleId: 'LOG_CONDITION_PROXY_001', standardClause: 'DB1405/T 085-2025 第5.4条', triggerFields: ['办理条件', '办理流程'], condition: '条件要求本人到场但流程允许代办', suggestionTemplate: '建议统一', threshold: undefined, triggerKeywords: ['本人到场', '本人办理', '代办', '委托办理'] },
|
|
7
7
|
{ ruleId: 'LOG_MATERIAL_CONDITION_001', standardClause: 'DB1405/T 085-2025 第5.5条', triggerFields: ['申请材料', '办理条件'], condition: '条件要求的证明材料未列入材料清单', suggestionTemplate: '建议补充', threshold: undefined, triggerKeywords: ['收入证明', '产权证明', '资质证明', '无犯罪记录证明', '健康证明', '社保证明', '纳税证明'] },
|
|
8
|
+
{ ruleId: 'LOG_CONDITION_AGE_PROXY_001', standardClause: 'DB1405/T 085-2025 第5.6条', triggerFields: ['办理条件', '办理流程'], condition: '办理条件含年龄限制且办理流程含代办', suggestionTemplate: '建议明确:未成年人由监护人代办,或删除年龄限制', threshold: undefined, triggerKeywords: ['年满18周岁', '年满十八周岁', '须为成年人', '监护人代办', '未成年人代办', '法定代理人代办'] },
|
|
8
9
|
];
|
|
9
10
|
describe('LogicRuleEngine', () => {
|
|
10
11
|
it('流程含"现场勘查"且时限为"3个工作日" → 输出logical错误', () => {
|
|
@@ -35,5 +36,42 @@ describe('LogicRuleEngine', () => {
|
|
|
35
36
|
const details = LogicRuleEngine.detect(guide, 'g1', rules);
|
|
36
37
|
expect(details.length).toBe(0);
|
|
37
38
|
});
|
|
39
|
+
it('LRE-01: 条件含"年满18周岁"+流程含"监护人代办" → 检出矛盾', () => {
|
|
40
|
+
const guide = { 办理条件: '申请人须年满18周岁', 办理流程: '可由监护人代办' };
|
|
41
|
+
const details = LogicRuleEngine.detect(guide, 'g1', rules);
|
|
42
|
+
expect(details.some((d) => d.errorType === 'logical' && d.description.includes('年满18周岁'))).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
it('LRE-02: 条件含"年满十八周岁"+流程含"法定代理人代办" → 检出矛盾', () => {
|
|
45
|
+
const guide = { 办理条件: '申请人须年满十八周岁', 办理流程: '可由法定代理人代办' };
|
|
46
|
+
const details = LogicRuleEngine.detect(guide, 'g1', rules);
|
|
47
|
+
expect(details.some((d) => d.errorType === 'logical' && d.description.includes('年满18周岁'))).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
it('LRE-03: 仅年龄无代办 → 不报告', () => {
|
|
50
|
+
const guide = { 办理条件: '申请人须年满18周岁', 办理流程: '受理→审批→发证' };
|
|
51
|
+
const details = LogicRuleEngine.detect(guide, 'g1', rules);
|
|
52
|
+
expect(details.some((d) => d.description.includes('年满18周岁') && d.description.includes('代办'))).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
it('LRE-04: 仅代办无年龄 → 不报告', () => {
|
|
55
|
+
const guide = { 办理条件: '符合法定条件', 办理流程: '可由监护人代办' };
|
|
56
|
+
const details = LogicRuleEngine.detect(guide, 'g1', rules);
|
|
57
|
+
expect(details.some((d) => d.description.includes('年满18周岁'))).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
it('LRE-05: 现有LOG_CONDITION_PROXY_001(本人到场+代办)不回归', () => {
|
|
60
|
+
const guide = { 办理条件: '需本人到场办理', 办理流程: '可代办' };
|
|
61
|
+
const details = LogicRuleEngine.detect(guide, 'g1', rules);
|
|
62
|
+
expect(details.some((d) => d.description.includes('本人到场'))).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
it('LRE-06: 新规则standardClause含"DB1405/T 085-2025 第5.6条"', () => {
|
|
65
|
+
const guide = { 办理条件: '申请人须年满18周岁', 办理流程: '可由监护人代办' };
|
|
66
|
+
const details = LogicRuleEngine.detect(guide, 'g1', rules);
|
|
67
|
+
expect(details.some((d) => d.standardClause === 'DB1405/T 085-2025 第5.6条')).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
it('LRE-07: 新规则suggestion含"未成年人由监护人代办"', () => {
|
|
70
|
+
const guide = { 办理条件: '申请人须年满18周岁', 办理流程: '可由监护人代办' };
|
|
71
|
+
const details = LogicRuleEngine.detect(guide, 'g1', rules);
|
|
72
|
+
const ageProxyDetail = details.find((d) => d.description.includes('年满18周岁'));
|
|
73
|
+
expect(ageProxyDetail).toBeDefined();
|
|
74
|
+
expect(ageProxyDetail?.suggestion).toContain('未成年人由监护人代办');
|
|
75
|
+
});
|
|
38
76
|
});
|
|
39
77
|
//# sourceMappingURL=logicRuleEngine.test.js.map
|
|
@@ -25,31 +25,97 @@ const itemTypeMatching = {
|
|
|
25
25
|
describe('SemanticRuleEngine', () => {
|
|
26
26
|
it('时限为"90个工作日"且法定上限为20 → 输出semantic错误', () => {
|
|
27
27
|
const guide = { 事项名称: '食品经营许可', 办理时限: '90个工作日' };
|
|
28
|
-
const
|
|
29
|
-
const timeError = details.find((d) => d.field === '办理时限');
|
|
28
|
+
const result = SemanticRuleEngine.detect(guide, 'g1', kb, itemTypeMatching);
|
|
29
|
+
const timeError = result.details.find((d) => d.field === '办理时限');
|
|
30
30
|
expect(timeError).toBeDefined();
|
|
31
31
|
expect(timeError.errorType).toBe('semantic');
|
|
32
32
|
expect(timeError.suggestion).toContain('20');
|
|
33
33
|
});
|
|
34
34
|
it('材料含"营业执照复映件"且标准为"营业执照复印件" → 输出semantic错误', () => {
|
|
35
35
|
const guide = { 事项名称: '食品经营许可', 申请材料: '营业执照复映件、行政许可申请表' };
|
|
36
|
-
const
|
|
37
|
-
const matError = details.find((d) => d.field === '申请材料');
|
|
36
|
+
const result = SemanticRuleEngine.detect(guide, 'g1', kb, itemTypeMatching);
|
|
37
|
+
const matError = result.details.find((d) => d.field === '申请材料');
|
|
38
38
|
expect(matError).toBeDefined();
|
|
39
39
|
expect(matError.errorType).toBe('semantic');
|
|
40
40
|
expect(matError.suggestion).toContain('营业执照复印件');
|
|
41
41
|
});
|
|
42
|
-
it('事项名称在知识库无对应条目 →
|
|
42
|
+
it('事项名称在知识库无对应条目 → 返回unmatched警告', () => {
|
|
43
43
|
const guide = { 事项名称: '未知事项类型', 办理时限: '90个工作日' };
|
|
44
|
-
const
|
|
45
|
-
expect(details.length).toBe(0);
|
|
44
|
+
const result = SemanticRuleEngine.detect(guide, 'g1', kb, itemTypeMatching);
|
|
45
|
+
expect(result.details.length).toBe(0);
|
|
46
|
+
expect(result.unmatched).toBe(true);
|
|
47
|
+
expect(result.warning).toBeDefined();
|
|
48
|
+
expect(result.warning.type).toBe('UNMATCHED_ITEM_TYPE');
|
|
46
49
|
});
|
|
47
|
-
it('
|
|
48
|
-
expect(() => SemanticRuleEngine.detect({}, 'g1', {}, itemTypeMatching)).toThrow();
|
|
50
|
+
it('标准知识库不可用且非降级 → 抛出GOV_DATA_KB_MISSING', () => {
|
|
51
|
+
expect(() => SemanticRuleEngine.detect({ 事项名称: '食品经营许可' }, 'g1', {}, itemTypeMatching)).toThrow();
|
|
52
|
+
});
|
|
53
|
+
it('标准知识库不可用且降级模式 → 返回空details不抛错', () => {
|
|
54
|
+
const result = SemanticRuleEngine.detect({ 事项名称: '食品经营许可' }, 'g1', {}, itemTypeMatching, undefined, { degradedMode: true });
|
|
55
|
+
expect(result.details).toEqual([]);
|
|
56
|
+
});
|
|
57
|
+
it('流程字段含关键词可匹配事项类型', () => {
|
|
58
|
+
const guide = { 事项名称: '建设工程规划核实', 办理流程: '受理→现场勘查→核实→出证', 办理时限: '90个工作日' };
|
|
59
|
+
const itemTypeMatchingWithFlow = {
|
|
60
|
+
现场勘查类: { keywords: ['现场勘查', '实地核查'], codePrefix: 'XC' },
|
|
61
|
+
};
|
|
62
|
+
const result = SemanticRuleEngine.detect(guide, 'g1', kb, itemTypeMatchingWithFlow);
|
|
63
|
+
expect(result.unmatched).toBeUndefined();
|
|
64
|
+
});
|
|
65
|
+
it('itemTypeOverride手动指定事项类型', () => {
|
|
66
|
+
const guide = { 事项名称: '建设工程规划核实', 办理时限: '90个工作日' };
|
|
67
|
+
const result = SemanticRuleEngine.detect(guide, 'g1', kb, itemTypeMatching, undefined, { itemTypeOverride: '行政许可' });
|
|
68
|
+
expect(result.unmatched).toBeUndefined();
|
|
69
|
+
const timeError = result.details.find((d) => d.field === '办理时限');
|
|
70
|
+
expect(timeError).toBeDefined();
|
|
71
|
+
});
|
|
72
|
+
it('matchItemType返回matchSource', () => {
|
|
73
|
+
const nameMatch = SemanticRuleEngine.matchItemType({ 事项名称: '食品经营许可' }, itemTypeMatching);
|
|
74
|
+
expect(nameMatch.matchSource).toBe('name');
|
|
75
|
+
const codeMatch = SemanticRuleEngine.matchItemType({ 事项编码: 'XK001' }, itemTypeMatching);
|
|
76
|
+
expect(codeMatch.matchSource).toBe('code');
|
|
77
|
+
const flowMatch = SemanticRuleEngine.matchItemType({ 事项名称: '测试', 办理流程: '现场勘查' }, { 现场勘查类: { keywords: ['现场勘查'], codePrefix: 'XC' } });
|
|
78
|
+
expect(flowMatch.matchSource).toBe('flow');
|
|
79
|
+
const manualMatch = SemanticRuleEngine.matchItemType({ 事项名称: '测试' }, itemTypeMatching, { itemTypeOverride: '行政许可' });
|
|
80
|
+
expect(manualMatch.matchSource).toBe('manual');
|
|
81
|
+
const noMatch = SemanticRuleEngine.matchItemType({ 事项名称: '测试' }, itemTypeMatching);
|
|
82
|
+
expect(noMatch.matchSource).toBeNull();
|
|
83
|
+
expect(noMatch.itemType).toBeNull();
|
|
49
84
|
});
|
|
50
85
|
it('时限数值提取', () => {
|
|
51
86
|
expect(SemanticRuleEngine.extractTimeLimitDays('20个工作日')).toBe(20);
|
|
52
87
|
expect(SemanticRuleEngine.extractTimeLimitDays('大约两周')).toBeNull();
|
|
53
88
|
});
|
|
89
|
+
it('SRE-01: 降级+空KB+本地词表含"身份证复印件"+材料含"身份证复" → 检出笔误', () => {
|
|
90
|
+
const guide = { 事项名称: '食品经营许可', 申请材料: '身份证复、申请表' };
|
|
91
|
+
const result = SemanticRuleEngine.detect(guide, 'g1', { timeLimits: [], materials: [], conditions: [] }, itemTypeMatching, undefined, { degradedMode: true });
|
|
92
|
+
expect(result.details.some((d) => d.errorType === 'semantic' && d.description.includes('笔误'))).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
it('SRE-02: 降级+空KB+本地词表含"营业执照复印件"+材料含"营业执照复" → 检出笔误', () => {
|
|
95
|
+
const guide = { 事项名称: '食品经营许可', 申请材料: '营业执照复' };
|
|
96
|
+
const result = SemanticRuleEngine.detect(guide, 'g1', { timeLimits: [], materials: [], conditions: [] }, itemTypeMatching, undefined, { degradedMode: true });
|
|
97
|
+
expect(result.details.some((d) => d.errorType === 'semantic' && d.description.includes('笔误'))).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
it('SRE-03: 降级+空KB+本地词表为空 → 返回空details不抛错', () => {
|
|
100
|
+
const guide = { 事项名称: '食品经营许可', 申请材料: '身份证复件' };
|
|
101
|
+
const result = SemanticRuleEngine.detect(guide, 'g1', { timeLimits: [], materials: [], conditions: [] }, itemTypeMatching, undefined, { degradedMode: true, localTermsPath: '/nonexistent/path/terms.json' });
|
|
102
|
+
expect(result.details).toEqual([]);
|
|
103
|
+
});
|
|
104
|
+
it('SRE-04: 降级+空KB+材料与本地词表完全匹配 → 返回空details', () => {
|
|
105
|
+
const guide = { 事项名称: '食品经营许可', 申请材料: '身份证复印件' };
|
|
106
|
+
const result = SemanticRuleEngine.detect(guide, 'g1', { timeLimits: [], materials: [], conditions: [] }, itemTypeMatching, undefined, { degradedMode: true });
|
|
107
|
+
expect(result.details).toEqual([]);
|
|
108
|
+
});
|
|
109
|
+
it('SRE-05: 降级+空KB+材料相似度=0.8 → 不报告(严格小于)', () => {
|
|
110
|
+
const guide = { 事项名称: '食品经营许可', 申请材料: '身份证复印件' };
|
|
111
|
+
const result = SemanticRuleEngine.detect(guide, 'g1', { timeLimits: [], materials: [], conditions: [] }, itemTypeMatching, undefined, { degradedMode: true, degradedSimilarityThreshold: 0.8 });
|
|
112
|
+
const hasPenalty = result.details.some((d) => d.description.includes('笔误'));
|
|
113
|
+
expect(hasPenalty).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
it('SRE-06: 降级保底错误明细standardClause含"降级模式·本地词表校验"', () => {
|
|
116
|
+
const guide = { 事项名称: '食品经营许可', 申请材料: '身份证复' };
|
|
117
|
+
const result = SemanticRuleEngine.detect(guide, 'g1', { timeLimits: [], materials: [], conditions: [] }, itemTypeMatching, undefined, { degradedMode: true });
|
|
118
|
+
expect(result.details.some((d) => d.standardClause === '降级模式·本地词表校验')).toBe(true);
|
|
119
|
+
});
|
|
54
120
|
});
|
|
55
121
|
//# sourceMappingURL=semanticRuleEngine.test.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const DataScaleGuard = {
|
|
2
|
+
check(data, maxFieldCount) {
|
|
3
|
+
const limit = maxFieldCount ?? 1000000;
|
|
4
|
+
let fieldCount = 0;
|
|
5
|
+
for (const row of data) {
|
|
6
|
+
if (typeof row === 'object' && row !== null) {
|
|
7
|
+
fieldCount += Object.keys(row).length;
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
fieldCount++;
|
|
11
|
+
}
|
|
12
|
+
if (fieldCount > limit) {
|
|
13
|
+
return { exceeded: true, fieldCount };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return { exceeded: false, fieldCount };
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=dataScaleGuard.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const DEGRADED_STATUS = { status: 'degraded', fetchedAt: '', recordCount: 0 };
|
|
2
|
+
const EMPTY_KB = {
|
|
3
|
+
timeLimits: [],
|
|
4
|
+
materials: [],
|
|
5
|
+
conditions: [],
|
|
6
|
+
dataSourceStatus: {
|
|
7
|
+
national: DEGRADED_STATUS,
|
|
8
|
+
provincial: DEGRADED_STATUS,
|
|
9
|
+
standard: DEGRADED_STATUS,
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
export const DegradedModeController = {
|
|
13
|
+
activate(reason) {
|
|
14
|
+
return {
|
|
15
|
+
degradedMode: true,
|
|
16
|
+
degradedReason: reason,
|
|
17
|
+
localKb: { ...EMPTY_KB, dataSourceStatus: { ...EMPTY_KB.dataSourceStatus } },
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=degradedModeController.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const EncodingDetector = {
|
|
2
|
+
detect(filePath) {
|
|
3
|
+
const { readFileSync } = require('node:fs');
|
|
4
|
+
let buffer;
|
|
5
|
+
try {
|
|
6
|
+
buffer = readFileSync(filePath);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return { content: null, encodingError: `无法读取文件: ${filePath}` };
|
|
10
|
+
}
|
|
11
|
+
if (buffer.length === 0) {
|
|
12
|
+
return { content: '', encodingError: '文件为空' };
|
|
13
|
+
}
|
|
14
|
+
const isUtf8 = this.isValidUtf8(buffer);
|
|
15
|
+
if (!isUtf8) {
|
|
16
|
+
return { content: null, encodingError: '文件编码非 UTF-8,请将文件转换为 UTF-8 编码后重试' };
|
|
17
|
+
}
|
|
18
|
+
return { content: buffer.toString('utf-8') };
|
|
19
|
+
},
|
|
20
|
+
isValidUtf8(buffer) {
|
|
21
|
+
try {
|
|
22
|
+
const str = buffer.toString('utf-8');
|
|
23
|
+
const reencoded = Buffer.from(str, 'utf-8');
|
|
24
|
+
return reencoded.equals(buffer);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=encodingDetector.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const EndpointResolver = {
|
|
2
|
+
resolve(endpoint) {
|
|
3
|
+
if (!endpoint || typeof endpoint !== 'string')
|
|
4
|
+
return null;
|
|
5
|
+
if (endpoint.startsWith('https://')) {
|
|
6
|
+
return { kind: 'https', target: endpoint };
|
|
7
|
+
}
|
|
8
|
+
if (endpoint.startsWith('http://')) {
|
|
9
|
+
return { kind: 'http', target: endpoint };
|
|
10
|
+
}
|
|
11
|
+
const { existsSync } = require('node:fs');
|
|
12
|
+
if (existsSync(endpoint)) {
|
|
13
|
+
return { kind: 'file', target: endpoint };
|
|
14
|
+
}
|
|
15
|
+
if (endpoint.startsWith('./') || endpoint.startsWith('../') || endpoint.startsWith('/') || /^[a-zA-Z]:[\\/]/.test(endpoint)) {
|
|
16
|
+
return { kind: 'file', target: endpoint };
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=endpointResolver.js.map
|
package/lib/formatValidator.js
CHANGED
|
@@ -13,23 +13,31 @@ export const FormatValidator = {
|
|
|
13
13
|
if (rule.pattern) {
|
|
14
14
|
const regex = new RegExp(rule.pattern);
|
|
15
15
|
if (!regex.test(strVal)) {
|
|
16
|
-
|
|
16
|
+
const issue = {
|
|
17
17
|
guideId,
|
|
18
18
|
field: rule.field,
|
|
19
19
|
issue: `字段"${rule.field}"值"${strVal}"不符合格式要求(正则: ${rule.pattern})`,
|
|
20
20
|
suggestion: rule.suggestionTemplate,
|
|
21
|
-
}
|
|
21
|
+
};
|
|
22
|
+
if (rule.field === '办理时限' && strVal.includes('自然日')) {
|
|
23
|
+
issue.semanticHint = "时限使用'自然日'而非'工作日',可能存在语义偏差";
|
|
24
|
+
}
|
|
25
|
+
issues.push(issue);
|
|
22
26
|
}
|
|
23
27
|
}
|
|
24
28
|
if (rule.requiredKeywords && rule.requiredKeywords.length > 0) {
|
|
25
29
|
const hasAnyKeyword = rule.requiredKeywords.some((kw) => strVal.includes(kw));
|
|
26
30
|
if (!hasAnyKeyword) {
|
|
27
|
-
|
|
31
|
+
const issue = {
|
|
28
32
|
guideId,
|
|
29
33
|
field: rule.field,
|
|
30
34
|
issue: `字段"${rule.field}"值"${strVal}"缺少必要关键词(需包含: ${rule.requiredKeywords.join('或')})`,
|
|
31
35
|
suggestion: rule.suggestionTemplate,
|
|
32
|
-
}
|
|
36
|
+
};
|
|
37
|
+
if (rule.field === '办理时限' && strVal.includes('自然日')) {
|
|
38
|
+
issue.semanticHint = "时限使用'自然日'而非'工作日',可能存在语义偏差";
|
|
39
|
+
}
|
|
40
|
+
issues.push(issue);
|
|
33
41
|
}
|
|
34
42
|
}
|
|
35
43
|
}
|