@liuhange/dsh-gov-data-inspector 3.1.0 → 3.1.2

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.
@@ -0,0 +1,160 @@
1
+ import { ErrorDetailBuilder } from './errorDetailBuilder.js';
2
+ function matchItemType(guide, itemTypeMatching) {
3
+ if (!itemTypeMatching)
4
+ return null;
5
+ const itemName = String(guide['事项名称'] ?? '');
6
+ const itemCode = String(guide['事项编码'] ?? '');
7
+ for (const [itemType, rule] of Object.entries(itemTypeMatching)) {
8
+ if (rule.keywords.some((kw) => itemName.includes(kw)))
9
+ return itemType;
10
+ if (itemCode && itemCode.startsWith(rule.codePrefix))
11
+ return itemType;
12
+ }
13
+ return null;
14
+ }
15
+ function extractTimeLimitDays(timeLimit) {
16
+ const str = String(timeLimit ?? '');
17
+ const match = str.match(/(\d+)\s*个?\s*工作日/);
18
+ if (!match || !match[1])
19
+ return null;
20
+ return parseInt(match[1], 10);
21
+ }
22
+ function detectTimeLimit(guide, guideId, itemType, kb, severityMapping) {
23
+ const timeLimitStr = String(guide['办理时限'] ?? '');
24
+ const days = extractTimeLimitDays(timeLimitStr);
25
+ if (days === null)
26
+ return [];
27
+ const stdEntry = kb.timeLimits.find((t) => t.itemType === itemType);
28
+ if (!stdEntry)
29
+ return [];
30
+ if (days > stdEntry.legalUpperLimit) {
31
+ return [
32
+ ErrorDetailBuilder.build({
33
+ guideId,
34
+ field: '办理时限',
35
+ errorType: 'semantic',
36
+ description: `办理时限"${timeLimitStr}"(${days}个工作日)超出法定上限${stdEntry.legalUpperLimit}个工作日。[数据源:${stdEntry.dataSource},条款:${stdEntry.standardClause}]`,
37
+ suggestion: `建议将办理时限调整为不超过${stdEntry.legalUpperLimit}个工作日,法定下限为${stdEntry.legalLowerLimit}个工作日`,
38
+ dataSource: stdEntry.dataSource,
39
+ standardClause: stdEntry.standardClause,
40
+ }, severityMapping),
41
+ ];
42
+ }
43
+ return [];
44
+ }
45
+ function detectMaterials(guide, guideId, itemType, kb, severityMapping) {
46
+ const materialStr = String(guide['申请材料'] ?? '');
47
+ if (!materialStr)
48
+ return [];
49
+ const materials = materialStr.split(/[、,,;;\n]/).map((s) => s.trim()).filter(Boolean);
50
+ const stdMaterials = kb.materials.filter((m) => m.itemType === itemType);
51
+ if (stdMaterials.length === 0)
52
+ return [];
53
+ const details = [];
54
+ for (const mat of materials) {
55
+ const matched = stdMaterials.some((std) => {
56
+ const stdName = std.standardName;
57
+ return mat === stdName || mat.includes(stdName) || stdName.includes(mat) || levenshteinSimilarity(mat, stdName) >= 0.95;
58
+ });
59
+ if (!matched) {
60
+ const closest = findClosestMaterial(mat, stdMaterials);
61
+ details.push(ErrorDetailBuilder.build({
62
+ guideId,
63
+ field: '申请材料',
64
+ errorType: 'semantic',
65
+ description: `申请材料"${mat}"与标准材料清单不匹配。标准材料清单中该事项类型应包含:${stdMaterials.map((m) => m.standardName).join('、')}。[数据源:${closest?.dataSource ?? 'unknown'},条款:${closest?.standardClause ?? 'unknown'}]`,
66
+ suggestion: closest
67
+ ? `建议将"${mat}"修改为标准名称"${closest.standardName}"。${closest.basisClause}`
68
+ : `建议核实"${mat}"是否为该事项标准材料,或补充至标准材料清单`,
69
+ dataSource: closest?.dataSource,
70
+ standardClause: closest?.standardClause,
71
+ }, severityMapping));
72
+ }
73
+ }
74
+ return details;
75
+ }
76
+ function levenshteinSimilarity(a, b) {
77
+ if (a.length === 0 && b.length === 0)
78
+ return 1;
79
+ const maxLen = Math.max(a.length, b.length);
80
+ if (maxLen === 0)
81
+ return 1;
82
+ const dist = levenshtein(a, b);
83
+ return 1 - dist / maxLen;
84
+ }
85
+ function levenshtein(a, b) {
86
+ const m = a.length;
87
+ const n = b.length;
88
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
89
+ for (let i = 0; i <= m; i++) {
90
+ const row = dp[i];
91
+ row[0] = i;
92
+ }
93
+ for (let j = 0; j <= n; j++) {
94
+ dp[0][j] = j;
95
+ }
96
+ for (let i = 1; i <= m; i++) {
97
+ for (let j = 1; j <= n; j++) {
98
+ const prevRow = dp[i - 1];
99
+ const currRow = dp[i];
100
+ if (a[i - 1] === b[j - 1])
101
+ currRow[j] = prevRow[j - 1];
102
+ else
103
+ currRow[j] = Math.min(prevRow[j], currRow[j - 1], prevRow[j - 1]) + 1;
104
+ }
105
+ }
106
+ return dp[m][n];
107
+ }
108
+ function findClosestMaterial(mat, stdMaterials) {
109
+ let best = null;
110
+ let bestSim = 0;
111
+ for (const std of stdMaterials) {
112
+ const sim = levenshteinSimilarity(mat, std.standardName);
113
+ if (sim > bestSim) {
114
+ bestSim = sim;
115
+ best = std;
116
+ }
117
+ }
118
+ return best;
119
+ }
120
+ function detectConditions(guide, guideId, itemType, kb, severityMapping) {
121
+ const conditionStr = String(guide['办理条件'] ?? '');
122
+ if (!conditionStr)
123
+ return [];
124
+ const stdConditions = kb.conditions.filter((c) => c.itemType === itemType);
125
+ if (stdConditions.length === 0)
126
+ return [];
127
+ const details = [];
128
+ for (const std of stdConditions) {
129
+ if (!conditionStr.includes(std.elementName) && !conditionStr.includes(std.standardValue)) {
130
+ details.push(ErrorDetailBuilder.build({
131
+ guideId,
132
+ field: '办理条件',
133
+ errorType: 'semantic',
134
+ description: `办理条件中缺失标准要素"${std.elementName}",标准值应为"${std.standardValue}"。[数据源:${std.dataSource},条款:${std.standardClause}]`,
135
+ suggestion: `建议在办理条件中补充"${std.elementName}"要求,标准值为"${std.standardValue}"`,
136
+ dataSource: std.dataSource,
137
+ standardClause: std.standardClause,
138
+ }, severityMapping));
139
+ }
140
+ }
141
+ return details;
142
+ }
143
+ export const SemanticRuleEngine = {
144
+ detect(guide, guideId, kb, itemTypeMatching, severityMapping) {
145
+ if (!kb || !kb.timeLimits || !kb.materials || !kb.conditions) {
146
+ throw new Error('GOV_DATA_KB_MISSING: 标准知识库不可用,语义错误检测已中止');
147
+ }
148
+ const itemType = matchItemType(guide, itemTypeMatching);
149
+ if (!itemType)
150
+ return [];
151
+ const details = [];
152
+ details.push(...detectTimeLimit(guide, guideId, itemType, kb, severityMapping));
153
+ details.push(...detectMaterials(guide, guideId, itemType, kb, severityMapping));
154
+ details.push(...detectConditions(guide, guideId, itemType, kb, severityMapping));
155
+ return details;
156
+ },
157
+ matchItemType,
158
+ extractTimeLimitDays,
159
+ };
160
+ //# sourceMappingURL=semanticRuleEngine.js.map
@@ -0,0 +1,62 @@
1
+ const FAILED_RESULT = {
2
+ rules: [],
3
+ status: { status: 'failed', fetchedAt: '', recordCount: 0 },
4
+ };
5
+ export const StandardRuleSource = {
6
+ async fetch(credentials) {
7
+ const docPath = credentials?.docPath ?? process.env.STANDARD_DOC_PATH;
8
+ const endpoint = credentials?.endpoint ?? process.env.STANDARD_RULES_ENDPOINT;
9
+ if (!docPath && !endpoint) {
10
+ return { ...FAILED_RESULT, status: { ...FAILED_RESULT.status } };
11
+ }
12
+ try {
13
+ if (endpoint && endpoint.startsWith('https://')) {
14
+ const response = await fetch(endpoint, {
15
+ method: 'GET',
16
+ headers: { 'Accept': 'application/json' },
17
+ signal: AbortSignal.timeout(10000),
18
+ });
19
+ if (!response.ok)
20
+ return { ...FAILED_RESULT, status: { ...FAILED_RESULT.status } };
21
+ const raw = await response.json();
22
+ if (!Array.isArray(raw))
23
+ return { ...FAILED_RESULT, status: { ...FAILED_RESULT.status } };
24
+ const rules = parseRules(raw);
25
+ return {
26
+ rules,
27
+ status: { status: 'success', fetchedAt: new Date().toISOString(), recordCount: rules.length },
28
+ };
29
+ }
30
+ if (docPath) {
31
+ const { readFileSync } = await import('node:fs');
32
+ const raw = JSON.parse(readFileSync(docPath, 'utf-8'));
33
+ if (!Array.isArray(raw))
34
+ return { ...FAILED_RESULT, status: { ...FAILED_RESULT.status } };
35
+ const rules = parseRules(raw);
36
+ return {
37
+ rules,
38
+ status: { status: 'success', fetchedAt: new Date().toISOString(), recordCount: rules.length },
39
+ };
40
+ }
41
+ return { ...FAILED_RESULT, status: { ...FAILED_RESULT.status } };
42
+ }
43
+ catch {
44
+ return { ...FAILED_RESULT, status: { ...FAILED_RESULT.status } };
45
+ }
46
+ },
47
+ };
48
+ function parseRules(raw) {
49
+ return raw
50
+ .filter((item) => item !== null && typeof item === 'object')
51
+ .map((item) => ({
52
+ ruleId: String(item.ruleId ?? ''),
53
+ standardClause: String(item.standardClause ?? ''),
54
+ triggerFields: Array.isArray(item.triggerFields) ? item.triggerFields : [],
55
+ condition: String(item.condition ?? ''),
56
+ threshold: item.threshold,
57
+ suggestionTemplate: String(item.suggestionTemplate ?? ''),
58
+ triggerKeywords: Array.isArray(item.triggerKeywords) ? item.triggerKeywords : [],
59
+ }))
60
+ .filter((r) => r.ruleId && r.standardClause && r.triggerFields.length > 0);
61
+ }
62
+ //# sourceMappingURL=standardRuleSource.js.map
package/lib/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
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.0",
4
+ "version": "3.1.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -31,10 +31,12 @@
31
31
  "test": "vitest run",
32
32
  "build": "tsc"
33
33
  },
34
+ "dependencies": {
35
+ "@liuhange/dsh-data-asset-shared": "^3.1.0"
36
+ },
34
37
  "peerDependencies": {
35
38
  "@deepseek-ai/cordis": "^4.0.0",
36
- "@deepseek-ai/dsh-tools": "0.0.1-rc.1",
37
- "@liuhange/dsh-data-asset-shared": "^3.0.0"
39
+ "@deepseek-ai/dsh-tools": "0.0.1-rc.1"
38
40
  },
39
41
  "dsh": {
40
42
  "bundle": {
@@ -57,4 +59,4 @@
57
59
  "办事指南"
58
60
  ],
59
61
  "homepage": "https://github.com/liuhange789/data-asset-inspector#readme"
60
- }
62
+ }