@liuhange/dsh-gov-data-inspector 3.1.8 → 3.2.1
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/README.md +120 -1
- package/lib/__tests__/configPack.test.js +76 -0
- package/lib/__tests__/configPackLoader.test.js +90 -0
- package/lib/__tests__/configPackSchemaValidator.test.js +75 -0
- package/lib/__tests__/defaultConfigPack.test.js +27 -0
- package/lib/__tests__/govConfigPacks.test.js +59 -0
- package/lib/config-pack-schema.json +186 -0
- package/lib/configPack.js +38 -0
- package/lib/configPackLoader.js +126 -0
- package/lib/configPackSchemaValidator.js +196 -0
- package/lib/default-config-pack.json +854 -0
- package/lib/index.js +33 -61
- package/lib/inspectionOrchestrator.js +7 -5
- package/lib/local-terms-loader.js +7 -1
- package/lib/policyBasis.js +4 -1
- package/lib/semanticRuleEngine.js +1 -1
- package/package.json +5 -2
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const REQUIRED_FIELDS = [
|
|
2
|
+
'configPackId',
|
|
3
|
+
'region',
|
|
4
|
+
'configPackVersion',
|
|
5
|
+
'policyBasis',
|
|
6
|
+
'requiredFields',
|
|
7
|
+
'formatRules',
|
|
8
|
+
'localStandardTerms',
|
|
9
|
+
'logicErrorRules',
|
|
10
|
+
'scoringWeights',
|
|
11
|
+
'itemTypeMatching',
|
|
12
|
+
'severityMapping',
|
|
13
|
+
'gbtMapping',
|
|
14
|
+
'dataSourceCredentials',
|
|
15
|
+
'dataSourcePriority',
|
|
16
|
+
];
|
|
17
|
+
export function normalizeConfigPack(raw) {
|
|
18
|
+
const requiredFields = (raw.requiredFields ?? raw.guideRequiredElements);
|
|
19
|
+
const scoringWeights = (raw.scoringWeights ?? raw.scoreWeights);
|
|
20
|
+
const gbtMapping = (raw.gbtMapping ?? raw.gbt47949Mapping);
|
|
21
|
+
const partial = { ...raw };
|
|
22
|
+
if (requiredFields !== undefined)
|
|
23
|
+
partial.requiredFields = requiredFields;
|
|
24
|
+
if (scoringWeights !== undefined)
|
|
25
|
+
partial.scoringWeights = scoringWeights;
|
|
26
|
+
if (gbtMapping !== undefined)
|
|
27
|
+
partial.gbtMapping = gbtMapping;
|
|
28
|
+
delete partial.guideRequiredElements;
|
|
29
|
+
delete partial.scoreWeights;
|
|
30
|
+
delete partial.gbt47949Mapping;
|
|
31
|
+
for (const field of REQUIRED_FIELDS) {
|
|
32
|
+
if (partial[field] === undefined || partial[field] === null) {
|
|
33
|
+
throw new Error(`GOV_CONFIG_PACK_INVALID: 必填字段 "${field}" 缺失`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return partial;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=configPack.js.map
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { normalizeConfigPack } from './configPack.js';
|
|
4
|
+
import { ConfigPackSchemaValidator } from './configPackSchemaValidator.js';
|
|
5
|
+
import defaultConfigPack from './default-config-pack.json' with { type: 'json' };
|
|
6
|
+
function tryLoadFromFilePath(warnings) {
|
|
7
|
+
const envPath = process.env.GOV_CONFIG_PACK_PATH;
|
|
8
|
+
if (!envPath)
|
|
9
|
+
return null;
|
|
10
|
+
try {
|
|
11
|
+
const raw = readFileSync(envPath, 'utf-8');
|
|
12
|
+
try {
|
|
13
|
+
const parsed = JSON.parse(raw);
|
|
14
|
+
const normalized = normalizeConfigPack(parsed);
|
|
15
|
+
const validation = ConfigPackSchemaValidator.validate(normalized);
|
|
16
|
+
if (!validation.valid) {
|
|
17
|
+
warnings.push({
|
|
18
|
+
type: 'CONFIG_PACK_WARNING',
|
|
19
|
+
code: 'GOV_CONFIG_PACK_SCHEMA_INVALID',
|
|
20
|
+
message: `文件配置包 Schema 校验失败: ${validation.errors.map((e) => e.path).join(', ')}`,
|
|
21
|
+
});
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
return normalized;
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
warnings.push({
|
|
28
|
+
type: 'CONFIG_PACK_WARNING',
|
|
29
|
+
code: 'GOV_CONFIG_PACK_PARSE_ERROR',
|
|
30
|
+
message: `文件配置包 JSON 解析失败: ${e.message}`,
|
|
31
|
+
});
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
warnings.push({
|
|
37
|
+
type: 'CONFIG_PACK_WARNING',
|
|
38
|
+
code: 'GOV_CONFIG_PACK_NOT_FOUND',
|
|
39
|
+
message: `文件配置包加载失败: ${e.message}`,
|
|
40
|
+
});
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function tryLoadFromNpmPackage(warnings) {
|
|
45
|
+
const packageName = process.env.GOV_CONFIG_PACK_PACKAGE;
|
|
46
|
+
if (!packageName)
|
|
47
|
+
return null;
|
|
48
|
+
try {
|
|
49
|
+
const require = createRequire(import.meta.url);
|
|
50
|
+
const raw = require(packageName);
|
|
51
|
+
try {
|
|
52
|
+
const normalized = normalizeConfigPack(raw);
|
|
53
|
+
const validation = ConfigPackSchemaValidator.validate(normalized);
|
|
54
|
+
if (!validation.valid) {
|
|
55
|
+
warnings.push({
|
|
56
|
+
type: 'CONFIG_PACK_WARNING',
|
|
57
|
+
code: 'GOV_CONFIG_PACK_SCHEMA_INVALID',
|
|
58
|
+
message: `npm 包配置包 Schema 校验失败: ${validation.errors.map((e) => e.path).join(', ')}`,
|
|
59
|
+
});
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return normalized;
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
warnings.push({
|
|
66
|
+
type: 'CONFIG_PACK_WARNING',
|
|
67
|
+
code: 'GOV_CONFIG_PACK_PARSE_ERROR',
|
|
68
|
+
message: `npm 包配置包解析失败: ${e.message}`,
|
|
69
|
+
});
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
warnings.push({
|
|
75
|
+
type: 'CONFIG_PACK_WARNING',
|
|
76
|
+
code: 'GOV_CONFIG_PACK_NOT_FOUND',
|
|
77
|
+
message: `npm 包配置包加载失败: ${e.message}`,
|
|
78
|
+
});
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function tryLoadDefault(warnings) {
|
|
83
|
+
try {
|
|
84
|
+
const normalized = normalizeConfigPack(defaultConfigPack);
|
|
85
|
+
const validation = ConfigPackSchemaValidator.validate(normalized);
|
|
86
|
+
if (!validation.valid) {
|
|
87
|
+
warnings.push({
|
|
88
|
+
type: 'CONFIG_PACK_WARNING',
|
|
89
|
+
code: 'GOV_CONFIG_PACK_FALLBACK',
|
|
90
|
+
message: `缺省配置包 Schema 校验失败: ${validation.errors.map((e) => e.path).join(', ')}`,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return normalized;
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
warnings.push({
|
|
97
|
+
type: 'CONFIG_PACK_WARNING',
|
|
98
|
+
code: 'GOV_CONFIG_PACK_FALLBACK',
|
|
99
|
+
message: `缺省配置包加载失败: ${e.message}`,
|
|
100
|
+
});
|
|
101
|
+
return defaultConfigPack;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export const ConfigPackLoader = {
|
|
105
|
+
load() {
|
|
106
|
+
const warnings = [];
|
|
107
|
+
const fromFile = tryLoadFromFilePath(warnings);
|
|
108
|
+
if (fromFile) {
|
|
109
|
+
return { configPack: fromFile, loadSource: 'file', warnings };
|
|
110
|
+
}
|
|
111
|
+
const fromNpm = tryLoadFromNpmPackage(warnings);
|
|
112
|
+
if (fromNpm) {
|
|
113
|
+
return { configPack: fromNpm, loadSource: 'npm', warnings };
|
|
114
|
+
}
|
|
115
|
+
if (warnings.length > 0) {
|
|
116
|
+
warnings.push({
|
|
117
|
+
type: 'CONFIG_PACK_WARNING',
|
|
118
|
+
code: 'GOV_CONFIG_PACK_FALLBACK',
|
|
119
|
+
message: '配置包加载回退至缺省配置包',
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
const fromDefault = tryLoadDefault(warnings);
|
|
123
|
+
return { configPack: fromDefault, loadSource: 'default', warnings };
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
//# sourceMappingURL=configPackLoader.js.map
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
const REQUIRED_TOP_FIELDS = [
|
|
2
|
+
'configPackId',
|
|
3
|
+
'region',
|
|
4
|
+
'configPackVersion',
|
|
5
|
+
'policyBasis',
|
|
6
|
+
'requiredFields',
|
|
7
|
+
'formatRules',
|
|
8
|
+
'localStandardTerms',
|
|
9
|
+
'logicErrorRules',
|
|
10
|
+
'scoringWeights',
|
|
11
|
+
'itemTypeMatching',
|
|
12
|
+
'severityMapping',
|
|
13
|
+
'gbtMapping',
|
|
14
|
+
'dataSourceCredentials',
|
|
15
|
+
'dataSourcePriority',
|
|
16
|
+
];
|
|
17
|
+
function isObject(val) {
|
|
18
|
+
return typeof val === 'object' && val !== null && !Array.isArray(val);
|
|
19
|
+
}
|
|
20
|
+
function isString(val) {
|
|
21
|
+
return typeof val === 'string';
|
|
22
|
+
}
|
|
23
|
+
function isNumber(val) {
|
|
24
|
+
return typeof val === 'number';
|
|
25
|
+
}
|
|
26
|
+
function isArray(val) {
|
|
27
|
+
return Array.isArray(val);
|
|
28
|
+
}
|
|
29
|
+
function checkRegexSafety(pattern) {
|
|
30
|
+
if (/\([^)]*\+\)[+*]/.test(pattern))
|
|
31
|
+
return false;
|
|
32
|
+
if (/\([^)]*\*\)[+*]/.test(pattern))
|
|
33
|
+
return false;
|
|
34
|
+
if (/\(\?:[^)]*\)[+*]{2,}/.test(pattern))
|
|
35
|
+
return false;
|
|
36
|
+
if (/\[[^\]]*\*\]\[[^\]]*\*\]/.test(pattern))
|
|
37
|
+
return false;
|
|
38
|
+
if (/\[[^\]]*\+\]\[[^\]]*\+\]/.test(pattern))
|
|
39
|
+
return false;
|
|
40
|
+
try {
|
|
41
|
+
new RegExp(pattern);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
export const ConfigPackSchemaValidator = {
|
|
49
|
+
validate(pack) {
|
|
50
|
+
const errors = [];
|
|
51
|
+
if (!isObject(pack)) {
|
|
52
|
+
return { valid: false, errors: [{ path: '', message: '配置包应为对象' }] };
|
|
53
|
+
}
|
|
54
|
+
for (const field of REQUIRED_TOP_FIELDS) {
|
|
55
|
+
if (pack[field] === undefined || pack[field] === null) {
|
|
56
|
+
errors.push({ path: field, message: `必填字段 "${field}" 缺失` });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (isString(pack.configPackId)) {
|
|
60
|
+
if (!/^([a-z]{2}-[a-z]+-\d{4})|default$/.test(pack.configPackId)) {
|
|
61
|
+
errors.push({ path: 'configPackId', message: 'configPackId 应匹配 ^[a-z]{2}-[a-z]+-\\d{4}$ 或为 default' });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (isString(pack.region) && pack.region.length < 2) {
|
|
65
|
+
errors.push({ path: 'region', message: 'region 长度应不少于 2' });
|
|
66
|
+
}
|
|
67
|
+
if (isString(pack.configPackVersion)) {
|
|
68
|
+
if (!/^\d+\.\d+\.\d+$/.test(pack.configPackVersion)) {
|
|
69
|
+
errors.push({ path: 'configPackVersion', message: 'configPackVersion 应匹配 ^\\d+\\.\\d+\\.\\d+$' });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (isArray(pack.policyBasis)) {
|
|
73
|
+
if (pack.policyBasis.length < 1) {
|
|
74
|
+
errors.push({ path: 'policyBasis', message: 'policyBasis 应至少含 1 个条目' });
|
|
75
|
+
}
|
|
76
|
+
for (let i = 0; i < pack.policyBasis.length; i++) {
|
|
77
|
+
const doc = pack.policyBasis[i];
|
|
78
|
+
if (!isObject(doc)) {
|
|
79
|
+
errors.push({ path: `policyBasis[${i}]`, message: '应为对象' });
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
for (const f of ['name', 'docNumber', 'coreRequirement']) {
|
|
83
|
+
if (!isString(doc[f]) || doc[f].length < 1) {
|
|
84
|
+
errors.push({ path: `policyBasis[${i}].${f}`, message: `应为非空字符串` });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (isArray(pack.requiredFields) && pack.requiredFields.length < 1) {
|
|
90
|
+
errors.push({ path: 'requiredFields', message: 'requiredFields 应至少含 1 个条目' });
|
|
91
|
+
}
|
|
92
|
+
if (isArray(pack.formatRules)) {
|
|
93
|
+
for (let i = 0; i < pack.formatRules.length; i++) {
|
|
94
|
+
const rule = pack.formatRules[i];
|
|
95
|
+
if (!isObject(rule)) {
|
|
96
|
+
errors.push({ path: `formatRules[${i}]`, message: '应为对象' });
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (!isString(rule.field)) {
|
|
100
|
+
errors.push({ path: `formatRules[${i}].field`, message: '应为字符串' });
|
|
101
|
+
}
|
|
102
|
+
if (!isString(rule.suggestionTemplate)) {
|
|
103
|
+
errors.push({ path: `formatRules[${i}].suggestionTemplate`, message: '应为字符串' });
|
|
104
|
+
}
|
|
105
|
+
if (isString(rule.pattern)) {
|
|
106
|
+
if (!checkRegexSafety(rule.pattern)) {
|
|
107
|
+
errors.push({ path: `formatRules[${i}].pattern`, message: `正则 "${rule.pattern}" 不安全(可能引发 ReDoS)` });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (isObject(pack.localStandardTerms)) {
|
|
113
|
+
if (!isArray(pack.localStandardTerms.materials)) {
|
|
114
|
+
errors.push({ path: 'localStandardTerms.materials', message: '应为数组' });
|
|
115
|
+
}
|
|
116
|
+
if (!isArray(pack.localStandardTerms.conditions)) {
|
|
117
|
+
errors.push({ path: 'localStandardTerms.conditions', message: '应为数组' });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (isArray(pack.logicErrorRules)) {
|
|
121
|
+
for (let i = 0; i < pack.logicErrorRules.length; i++) {
|
|
122
|
+
const rule = pack.logicErrorRules[i];
|
|
123
|
+
if (!isObject(rule)) {
|
|
124
|
+
errors.push({ path: `logicErrorRules[${i}]`, message: '应为对象' });
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (!isString(rule.ruleId)) {
|
|
128
|
+
errors.push({ path: `logicErrorRules[${i}].ruleId`, message: '应为字符串' });
|
|
129
|
+
}
|
|
130
|
+
if (!isString(rule.standardClause)) {
|
|
131
|
+
errors.push({ path: `logicErrorRules[${i}].standardClause`, message: '应为字符串' });
|
|
132
|
+
}
|
|
133
|
+
if (!isArray(rule.triggerFields)) {
|
|
134
|
+
errors.push({ path: `logicErrorRules[${i}].triggerFields`, message: '应为数组' });
|
|
135
|
+
}
|
|
136
|
+
if (!isArray(rule.triggerKeywords)) {
|
|
137
|
+
errors.push({ path: `logicErrorRules[${i}].triggerKeywords`, message: '应为数组' });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (isObject(pack.scoringWeights)) {
|
|
142
|
+
const sw = pack.scoringWeights;
|
|
143
|
+
const completeness = isNumber(sw.completeness) ? sw.completeness : NaN;
|
|
144
|
+
const accuracy = isNumber(sw.accuracy) ? sw.accuracy : NaN;
|
|
145
|
+
const traceability = isNumber(sw.traceability) ? sw.traceability : NaN;
|
|
146
|
+
if (isNaN(completeness) || isNaN(accuracy) || isNaN(traceability)) {
|
|
147
|
+
errors.push({ path: 'scoringWeights', message: 'completeness/accuracy/traceability 应为数字' });
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
const sum = completeness + accuracy + traceability;
|
|
151
|
+
if (Math.abs(sum - 1) > 0.001) {
|
|
152
|
+
errors.push({ path: 'scoringWeights', message: `三权重之和应为 1,实际为 ${sum}` });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (isObject(pack.itemTypeMatching)) {
|
|
157
|
+
if (Object.keys(pack.itemTypeMatching).length < 1) {
|
|
158
|
+
errors.push({ path: 'itemTypeMatching', message: '应至少含 1 个条目' });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (isObject(pack.severityMapping)) {
|
|
162
|
+
const validValues = ['critical', 'major', 'minor'];
|
|
163
|
+
for (const [key, val] of Object.entries(pack.severityMapping)) {
|
|
164
|
+
if (!validValues.includes(val)) {
|
|
165
|
+
errors.push({ path: `severityMapping.${key}`, message: `应为 critical/major/minor,实际为 ${val}` });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (isObject(pack.gbtMapping) && Object.keys(pack.gbtMapping).length < 1) {
|
|
170
|
+
errors.push({ path: 'gbtMapping', message: '应至少含 1 个条目' });
|
|
171
|
+
}
|
|
172
|
+
if (isObject(pack.dataSourceCredentials)) {
|
|
173
|
+
const ds = pack.dataSourceCredentials;
|
|
174
|
+
for (const src of ['national', 'provincial', 'standard']) {
|
|
175
|
+
if (!isObject(ds[src])) {
|
|
176
|
+
errors.push({ path: `dataSourceCredentials.${src}`, message: '应为对象' });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (isArray(pack.dataSourcePriority)) {
|
|
181
|
+
if (pack.dataSourcePriority.length < 1) {
|
|
182
|
+
errors.push({ path: 'dataSourcePriority', message: '应至少含 1 个条目' });
|
|
183
|
+
}
|
|
184
|
+
for (let i = 0; i < pack.dataSourcePriority.length; i++) {
|
|
185
|
+
if (!['national', 'provincial'].includes(pack.dataSourcePriority[i])) {
|
|
186
|
+
errors.push({ path: `dataSourcePriority[${i}]`, message: '应为 national 或 provincial' });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return { valid: errors.length === 0, errors };
|
|
191
|
+
},
|
|
192
|
+
checkRegexSafety(pattern) {
|
|
193
|
+
return checkRegexSafety(pattern);
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
//# sourceMappingURL=configPackSchemaValidator.js.map
|