@liuhange/dsh-gov-data-inspector 3.1.5 → 3.1.6

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 CHANGED
@@ -1,9 +1,20 @@
1
1
  # Cordis Framework Patch Declaration for @liuhange/dsh-gov-data-inspector
2
2
  # This file declares the plugin's runtime patch for the Cordis framework.
3
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
4
+ - target: loaders.web
5
+ action: insert
6
+ entry:
7
+ id: "@liuhange/dsh-gov-data-inspector"
8
+ name: "@liuhange/dsh-gov-data-inspector"
9
+ version: "3.1.6"
10
+ - target: loaders.headless
11
+ action: insert
12
+ entry:
13
+ id: "@liuhange/dsh-gov-data-inspector"
14
+ name: "@liuhange/dsh-gov-data-inspector"
15
+ version: "3.1.6"
16
+ - target: tools
17
+ action: register
18
+ description: Register plugin tools into Cordis runtime
19
+ plugin: "@liuhange/dsh-gov-data-inspector"
20
+ version: "3.1.6"
@@ -0,0 +1,40 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { readFileSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ const patchPath = resolve(__dirname, '..', '..', 'cordis.patch.yml');
5
+ const patchContent = readFileSync(patchPath, 'utf-8');
6
+ describe('cordis.patch.yml 安装阻塞修复', () => {
7
+ it('P0-1: plugin字段值用双引号包裹', () => {
8
+ expect(patchContent).toContain('plugin: "@liuhange/dsh-gov-data-inspector"');
9
+ });
10
+ it('P0-1: version字段用双引号包裹', () => {
11
+ expect(patchContent).toContain('version: "3.1.6"');
12
+ });
13
+ it('P0-2: 顶层数组结构(以- target开头)', () => {
14
+ const lines = patchContent.split('\n').filter((l) => l.trim() && !l.trim().startsWith('#'));
15
+ expect(lines[0].trim().startsWith('- target:')).toBe(true);
16
+ });
17
+ it('P0-2: 含loaders.web的insert声明', () => {
18
+ expect(patchContent).toContain('target: loaders.web');
19
+ expect(patchContent).toContain('action: insert');
20
+ });
21
+ it('P0-2: 含loaders.headless的insert声明', () => {
22
+ expect(patchContent).toContain('target: loaders.headless');
23
+ });
24
+ it('P0-2: 含tools.register声明', () => {
25
+ expect(patchContent).toContain('target: tools');
26
+ expect(patchContent).toContain('action: register');
27
+ });
28
+ it('P0-2: entry含id和name指向插件', () => {
29
+ expect(patchContent).toContain('id: "@liuhange/dsh-gov-data-inspector"');
30
+ expect(patchContent).toContain('name: "@liuhange/dsh-gov-data-inspector"');
31
+ });
32
+ it('P0-2: 所有@开头值均用双引号包裹', () => {
33
+ const atLines = patchContent.split('\n').filter((l) => l.includes('@liuhange') && !l.trim().startsWith('#'));
34
+ expect(atLines.length).toBeGreaterThan(0);
35
+ for (const line of atLines) {
36
+ expect(line).toContain('"@liuhange');
37
+ }
38
+ });
39
+ });
40
+ //# sourceMappingURL=cordisPatch.test.js.map
@@ -60,5 +60,25 @@ describe('FormatValidator', () => {
60
60
  expect(timeLimitIssue).toBeDefined();
61
61
  expect(timeLimitIssue?.issue).toContain('格式要求');
62
62
  });
63
+ it('FV-06: 监督电话格式不正确 → 输出格式问题', () => {
64
+ const rules = [...formatRules, { field: '监督电话', pattern: '^(\\d{3,4}-)?\\d{7,8}$', suggestionTemplate: '监督电话应使用"区号-号码"格式' }];
65
+ const issues = FormatValidator.validate({ 监督电话: '请联系前台' }, 'g1', rules);
66
+ expect(issues.some((i) => i.field === '监督电话')).toBe(true);
67
+ });
68
+ it('FV-07: 监督电话格式正确 → 无格式问题', () => {
69
+ const rules = [...formatRules, { field: '监督电话', pattern: '^(\\d{3,4}-)?\\d{7,8}$', suggestionTemplate: '监督电话应使用"区号-号码"格式' }];
70
+ const issues = FormatValidator.validate({ 监督电话: '010-12345678' }, 'g1', rules);
71
+ expect(issues.some((i) => i.field === '监督电话')).toBe(false);
72
+ });
73
+ it('FV-08: 办理时间缺少必要关键词 → 输出格式问题', () => {
74
+ const rules = [...formatRules, { field: '办理时间', requiredKeywords: ['工作日', '上午', '下午', '周一至周五'], suggestionTemplate: '办理时间应注明工作日及上下午时段' }];
75
+ const issues = FormatValidator.validate({ 办理时间: '随时可办理' }, 'g1', rules);
76
+ expect(issues.some((i) => i.field === '办理时间')).toBe(true);
77
+ });
78
+ it('FV-09: 办理时间含正确关键词 → 无格式问题', () => {
79
+ const rules = [...formatRules, { field: '办理时间', requiredKeywords: ['工作日', '上午', '下午', '周一至周五'], suggestionTemplate: '办理时间应注明工作日及上下午时段' }];
80
+ const issues = FormatValidator.validate({ 办理时间: '周一至周五 上午9:00-12:00 下午13:30-17:00' }, 'g1', rules);
81
+ expect(issues.some((i) => i.field === '办理时间')).toBe(false);
82
+ });
63
83
  });
64
84
  //# sourceMappingURL=formatValidator.test.js.map
@@ -22,5 +22,34 @@ describe('gov-data-inspector 回归测试', () => {
22
22
  expect(tool.parameters.properties.dataSource).toBeDefined();
23
23
  expect(tool.parameters.properties.inspectionMode).toBeDefined();
24
24
  });
25
+ it('P0-3: apply注册的工具含output字段', async () => {
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
+ expect(tool.output).toBeDefined();
31
+ expect(tool.output.schema).toBeDefined();
32
+ expect(tool.output.render).toBeDefined();
33
+ });
34
+ it('P0-3: output.schema描述巡检报告顶层字段', async () => {
35
+ const mod = await import('../index.js');
36
+ const tools = [];
37
+ mod.apply({ tools: { register: (t) => tools.push(t) } });
38
+ const tool = tools[0];
39
+ const props = tool.output.schema.properties;
40
+ expect(props.dataSource).toBeDefined();
41
+ expect(props.inspectionMode).toBeDefined();
42
+ expect(props.timestamp).toBeDefined();
43
+ expect(props.policyBasis).toBeDefined();
44
+ expect(props.guideInspection).toBeDefined();
45
+ expect(props.classification).toBeDefined();
46
+ });
47
+ it('P0-3: output.render.type为json', async () => {
48
+ const mod = await import('../index.js');
49
+ const tools = [];
50
+ mod.apply({ tools: { register: (t) => tools.push(t) } });
51
+ const tool = tools[0];
52
+ expect(tool.output.render.type).toBe('json');
53
+ });
25
54
  });
26
55
  //# sourceMappingURL=govData.test.js.map
@@ -73,5 +73,20 @@ describe('LogicRuleEngine', () => {
73
73
  expect(ageProxyDetail).toBeDefined();
74
74
  expect(ageProxyDetail?.suggestion).toContain('未成年人由监护人代办');
75
75
  });
76
+ it('E26: 办理条件含"年满18周岁"+办理流程含"监护人代办" → 检出逻辑矛盾', () => {
77
+ const guide = { 事项名称: '身份证补领', 办理条件: '申请人须年满18周岁', 办理流程: '受理→监护人代办→发证', 办理时限: '20个工作日' };
78
+ const details = LogicRuleEngine.detect(guide, 'g1', rules);
79
+ const ageProxyDetail = details.find((d) => d.description.includes('年满18周岁'));
80
+ expect(ageProxyDetail).toBeDefined();
81
+ expect(ageProxyDetail.errorType).toBe('logical');
82
+ expect(ageProxyDetail.field).toBe('办理流程');
83
+ });
84
+ it('E35: 办理条件含"须为成年人"+办理流程含"未成年人代办" → 检出逻辑矛盾', () => {
85
+ const guide = { 事项名称: '户口迁移', 办理条件: '须为成年人', 办理流程: '可由未成年人代办' };
86
+ const details = LogicRuleEngine.detect(guide, 'g1', rules);
87
+ const ageProxyDetail = details.find((d) => d.description.includes('年满18周岁'));
88
+ expect(ageProxyDetail).toBeDefined();
89
+ expect(ageProxyDetail.errorType).toBe('logical');
90
+ });
76
91
  });
77
92
  //# sourceMappingURL=logicRuleEngine.test.js.map
@@ -5,9 +5,9 @@ describe('政策依据配置', () => {
5
5
  const basis = resolvePolicyBasis('GOV_DATA_INSPECTION');
6
6
  expect(basis.length).toBe(6);
7
7
  });
8
- it('policyBasis含DB1405/T 085-2025与国办发〔2017〕47号', () => {
8
+ it('policyBasis含国办发〔2015〕46号与国办发〔2017〕47号', () => {
9
9
  const basis = resolvePolicyBasis('GOV_DATA_INSPECTION');
10
- expect(basis.some((b) => b.includes('DB1405/T 085-2025'))).toBe(true);
10
+ expect(basis.some((b) => b.includes('国办发〔2015〕46号'))).toBe(true);
11
11
  expect(basis.some((b) => b.includes('国办发〔2017〕47号'))).toBe(true);
12
12
  });
13
13
  it('policyBasis含国家政务服务平台与广东省政务服务事项管理系统来源说明', () => {
@@ -19,5 +19,18 @@ describe('政策依据配置', () => {
19
19
  const basis = resolvePolicyBasis('GOV_DATA_INSPECTION');
20
20
  expect(basis.some((b) => b.includes('〔') && b.includes('〕'))).toBe(true);
21
21
  });
22
+ it('P2-2: policyBasis不含河北省地方标准DB1405/T 085-2025', () => {
23
+ const basis = resolvePolicyBasis('GOV_DATA_INSPECTION');
24
+ expect(basis.some((b) => b.includes('DB1405/T 085-2025'))).toBe(false);
25
+ });
26
+ it('P2-2: policyBasis含国家层面权威标准国办发〔2015〕46号', () => {
27
+ const basis = resolvePolicyBasis('GOV_DATA_INSPECTION');
28
+ expect(basis.some((b) => b.includes('国办发〔2015〕46号'))).toBe(true);
29
+ });
30
+ it('P2-2: 保留GB/T 47949-2026与GB/T 47950-2026不变', () => {
31
+ const basis = resolvePolicyBasis('GOV_DATA_INSPECTION');
32
+ expect(basis.some((b) => b.includes('GB/T 47949-2026'))).toBe(true);
33
+ expect(basis.some((b) => b.includes('GB/T 47950-2026'))).toBe(true);
34
+ });
22
35
  });
23
36
  //# sourceMappingURL=policyBasis.test.js.map
@@ -117,5 +117,30 @@ describe('SemanticRuleEngine', () => {
117
117
  const result = SemanticRuleEngine.detect(guide, 'g1', { timeLimits: [], materials: [], conditions: [] }, itemTypeMatching, undefined, { degradedMode: true });
118
118
  expect(result.details.some((d) => d.standardClause === '降级模式·本地词表校验')).toBe(true);
119
119
  });
120
+ it('SRE-07: 降级+空KB+材料完全无匹配(maxSim===0) → 输出"疑似错误"含"与本地词表无任何匹配"', () => {
121
+ const guide = { 事项名称: '食品经营许可', 申请材料: '$$$###' };
122
+ const result = SemanticRuleEngine.detect(guide, 'g1', { timeLimits: [], materials: [], conditions: [] }, itemTypeMatching, undefined, { degradedMode: true });
123
+ expect(result.details.some((d) => d.errorType === 'semantic' && d.description.includes('与本地词表无任何匹配') && d.description.includes('疑似错误'))).toBe(true);
124
+ });
125
+ it('SRE-08: 降级+空KB+办理条件含疑似笔误 → 输出semantic错误含"笔误"', () => {
126
+ const guide = { 事项名称: '食品经营许可', 办理条件: '年满周岁' };
127
+ const result = SemanticRuleEngine.detect(guide, 'g1', { timeLimits: [], materials: [], conditions: [] }, itemTypeMatching, undefined, { degradedMode: true });
128
+ expect(result.details.some((d) => d.field === '办理条件' && d.errorType === 'semantic' && d.description.includes('笔误'))).toBe(true);
129
+ });
130
+ it('SRE-09: 降级+空KB+办理条件完全无匹配 → 输出"疑似错误"含"与本地词表无任何匹配"', () => {
131
+ const guide = { 事项名称: '食品经营许可', 办理条件: '$$$###' };
132
+ const result = SemanticRuleEngine.detect(guide, 'g1', { timeLimits: [], materials: [], conditions: [] }, itemTypeMatching, undefined, { degradedMode: true });
133
+ expect(result.details.some((d) => d.field === '办理条件' && d.errorType === 'semantic' && d.description.includes('与本地词表无任何匹配'))).toBe(true);
134
+ });
135
+ it('SRE-10: 降级+空KB+localTerms加载失败 → 返回空不崩溃', () => {
136
+ const guide = { 事项名称: '食品经营许可', 申请材料: '身份证复印件', 办理条件: '年满18周岁' };
137
+ const result = SemanticRuleEngine.detect(guide, 'g1', { timeLimits: [], materials: [], conditions: [] }, itemTypeMatching, undefined, { degradedMode: true, localTermsPath: '/nonexistent/path/terms.json' });
138
+ expect(result.details).toEqual([]);
139
+ });
140
+ it('SRE-11: 降级+空KB+材料正确+条件正确 → 返回空details', () => {
141
+ const guide = { 事项名称: '食品经营许可', 申请材料: '身份证复印件', 办理条件: '年满18周岁' };
142
+ const result = SemanticRuleEngine.detect(guide, 'g1', { timeLimits: [], materials: [], conditions: [] }, itemTypeMatching, undefined, { degradedMode: true });
143
+ expect(result.details).toEqual([]);
144
+ });
120
145
  });
121
146
  //# sourceMappingURL=semanticRuleEngine.test.js.map
package/lib/index.js CHANGED
@@ -22,6 +22,25 @@ export function apply(ctx) {
22
22
  },
23
23
  required: ['dataSource'],
24
24
  },
25
+ output: {
26
+ schema: {
27
+ type: 'object',
28
+ properties: {
29
+ dataSource: { type: 'string', description: '巡检数据源路径' },
30
+ inspectionMode: { type: 'string', enum: ['guide', 'classification', 'full'], description: '巡检模式' },
31
+ timestamp: { type: 'string', description: '巡检时间戳(ISO8601)' },
32
+ policyBasis: { type: 'array', items: { type: 'string' }, description: '政策依据列表' },
33
+ dataSourceStatus: { type: 'object', description: '数据源获取状态' },
34
+ degradedMode: { type: 'boolean', description: '是否降级模式' },
35
+ degradedReason: { type: 'string', description: '降级原因' },
36
+ guideInspection: { type: 'object', description: '办事指南巡检结果(含completeness/missingFields/semanticErrors/logicalErrors/errorDetails/formatIssues等)' },
37
+ classification: { type: 'object', description: '公共数据资产分类结果(含dataAssetCode/categoryLevel/specificType)' },
38
+ warnings: { type: 'array', description: '未匹配事项类型等告警信息' },
39
+ },
40
+ required: ['dataSource', 'inspectionMode', 'timestamp', 'policyBasis'],
41
+ },
42
+ render: { type: 'json' },
43
+ },
25
44
  async execute(args) {
26
45
  try {
27
46
  if (!args?.dataSource || typeof args.dataSource !== 'string') {
@@ -177,35 +177,83 @@ export const SemanticRuleEngine = {
177
177
  const hasKbData = kb.timeLimits.length > 0 || kb.materials.length > 0 || kb.conditions.length > 0;
178
178
  if (!hasKbData && options?.degradedMode) {
179
179
  const localTerms = LocalTermsLoader.load(options.localTermsPath);
180
- if (localTerms.materials.length === 0) {
180
+ if (localTerms.materials.length === 0 && localTerms.conditions.length === 0) {
181
181
  return { details: [] };
182
182
  }
183
183
  const threshold = options.degradedSimilarityThreshold ?? 0.8;
184
- const materialStr = String(guide['申请材料'] ?? '');
185
- if (!materialStr)
186
- return { details: [] };
187
- const materials = materialStr.split(/[、,,;;\n]/).map((s) => s.trim()).filter(Boolean);
188
184
  const details = [];
189
- for (const mat of materials) {
190
- let maxSim = 0;
191
- let closestTerm = '';
192
- for (const term of localTerms.materials) {
193
- const sim = levenshteinSimilarity(mat, term);
194
- if (sim > maxSim) {
195
- maxSim = sim;
196
- closestTerm = term;
185
+ const materialStr = String(guide['申请材料'] ?? '');
186
+ if (materialStr && localTerms.materials.length > 0) {
187
+ const materials = materialStr.split(/[、,,;;\n]/).map((s) => s.trim()).filter(Boolean);
188
+ for (const mat of materials) {
189
+ let maxSim = 0;
190
+ let closestTerm = '';
191
+ for (const term of localTerms.materials) {
192
+ const sim = levenshteinSimilarity(mat, term);
193
+ if (sim > maxSim) {
194
+ maxSim = sim;
195
+ closestTerm = term;
196
+ }
197
+ }
198
+ if (maxSim === 0) {
199
+ details.push(ErrorDetailBuilder.build({
200
+ guideId,
201
+ field: '申请材料',
202
+ errorType: 'semantic',
203
+ description: `申请材料"${mat}"与本地词表无任何匹配,疑似错误。[降级模式·本地词表校验]`,
204
+ suggestion: `建议核实"${mat}"是否为标准申请材料名称`,
205
+ dataSource: 'standard',
206
+ standardClause: '降级模式·本地词表校验',
207
+ }, severityMapping));
208
+ }
209
+ else if (maxSim < threshold && maxSim > 0) {
210
+ details.push(ErrorDetailBuilder.build({
211
+ guideId,
212
+ field: '申请材料',
213
+ errorType: 'semantic',
214
+ description: `申请材料"${mat}"与标准词表"${closestTerm}"高度相似(相似度${maxSim.toFixed(2)}),可能存在笔误。[降级模式·本地词表校验]`,
215
+ suggestion: `建议将"${mat}"修改为标准名称"${closestTerm}"`,
216
+ dataSource: 'standard',
217
+ standardClause: '降级模式·本地词表校验',
218
+ }, severityMapping));
197
219
  }
198
220
  }
199
- if (maxSim < threshold && maxSim > 0) {
200
- details.push(ErrorDetailBuilder.build({
201
- guideId,
202
- field: '申请材料',
203
- errorType: 'semantic',
204
- description: `申请材料"${mat}"与标准词表"${closestTerm}"高度相似(相似度${maxSim.toFixed(2)}),可能存在笔误。[降级模式·本地词表校验]`,
205
- suggestion: `建议将"${mat}"修改为标准名称"${closestTerm}"`,
206
- dataSource: 'standard',
207
- standardClause: '降级模式·本地词表校验',
208
- }, severityMapping));
221
+ }
222
+ const conditionStr = String(guide['办理条件'] ?? '');
223
+ if (conditionStr && localTerms.conditions.length > 0) {
224
+ const condParts = conditionStr.split(/[、,,;;\n。]/).map((s) => s.trim()).filter(Boolean);
225
+ for (const cond of condParts) {
226
+ let maxSim = 0;
227
+ let closestTerm = '';
228
+ for (const term of localTerms.conditions) {
229
+ const sim = levenshteinSimilarity(cond, term);
230
+ if (sim > maxSim) {
231
+ maxSim = sim;
232
+ closestTerm = term;
233
+ }
234
+ }
235
+ if (maxSim === 0) {
236
+ details.push(ErrorDetailBuilder.build({
237
+ guideId,
238
+ field: '办理条件',
239
+ errorType: 'semantic',
240
+ description: `办理条件"${cond}"与本地词表无任何匹配,疑似错误。[降级模式·本地词表校验]`,
241
+ suggestion: `建议核实"${cond}"是否为标准办理条件表述`,
242
+ dataSource: 'standard',
243
+ standardClause: '降级模式·本地词表校验',
244
+ }, severityMapping));
245
+ }
246
+ else if (maxSim < threshold && maxSim > 0) {
247
+ details.push(ErrorDetailBuilder.build({
248
+ guideId,
249
+ field: '办理条件',
250
+ errorType: 'semantic',
251
+ description: `办理条件"${cond}"与标准词表"${closestTerm}"高度相似(相似度${maxSim.toFixed(2)}),可能存在笔误。[降级模式·本地词表校验]`,
252
+ suggestion: `建议将"${cond}"修改为标准表述"${closestTerm}"`,
253
+ dataSource: 'standard',
254
+ standardClause: '降级模式·本地词表校验',
255
+ }, severityMapping));
256
+ }
209
257
  }
210
258
  }
211
259
  return { details };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@liuhange/dsh-gov-data-inspector",
3
3
  "description": "Government data inspection plugin: service guide quality check and public data asset classification",
4
- "version": "3.1.5",
4
+ "version": "3.1.6",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@deepseek-ai/cordis": "^4.0.0",
40
- "@deepseek-ai/dsh-tools": "0.0.1-rc.1"
40
+ "@deepseek-ai/dsh-tools": "0.2.0-rc.2"
41
41
  },
42
42
  "dsh": {
43
43
  "bundle": {