@liuhange/dsh-data-asset-quality-score 3.0.0
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/lib/__tests__/qualityScore.test.js +18 -0
- package/lib/index.js +191 -0
- package/package.json +28 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
describe('data-asset-quality-score', () => {
|
|
3
|
+
it('导出name/inject/apply', async () => {
|
|
4
|
+
const mod = await import('../index.js');
|
|
5
|
+
expect(mod.name).toBe('@liuhange/dsh-data-asset-quality-score');
|
|
6
|
+
expect(mod.inject).toEqual(['tools']);
|
|
7
|
+
expect(typeof mod.apply).toBe('function');
|
|
8
|
+
});
|
|
9
|
+
it('apply注册score_data_quality工具', async () => {
|
|
10
|
+
const mod = await import('../index.js');
|
|
11
|
+
const tools = [];
|
|
12
|
+
mod.apply({ tools: { register: (t) => tools.push(t) } });
|
|
13
|
+
const tool = tools[0];
|
|
14
|
+
expect(tool.name).toBe('score_data_quality');
|
|
15
|
+
expect(tool.description).toContain('六维');
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
//# sourceMappingURL=qualityScore.test.js.map
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
export const name = '@liuhange/dsh-data-asset-quality-score';
|
|
2
|
+
export const inject = ['tools'];
|
|
3
|
+
export function apply(ctx) {
|
|
4
|
+
ctx.tools.register({
|
|
5
|
+
name: 'score_data_quality',
|
|
6
|
+
description: '数据质量量化评分:六维(规范性+完整性+准确性+一致性+时效性+可访问性)0-100分',
|
|
7
|
+
parameters: {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: {
|
|
10
|
+
dataSource: { type: 'string', description: '数据文件路径或JSON数据' },
|
|
11
|
+
schema: { type: 'string', description: '数据schema声明(可选)' },
|
|
12
|
+
},
|
|
13
|
+
required: ['dataSource'],
|
|
14
|
+
},
|
|
15
|
+
async execute(args) {
|
|
16
|
+
try {
|
|
17
|
+
if (!args?.dataSource || typeof args.dataSource !== 'string') {
|
|
18
|
+
return JSON.stringify({ error: 'INVALID_ARGS', message: 'dataSource is required' });
|
|
19
|
+
}
|
|
20
|
+
const { readFileSync } = await import('node:fs');
|
|
21
|
+
let rulesConfig;
|
|
22
|
+
try {
|
|
23
|
+
rulesConfig = JSON.parse(readFileSync(process.env.BUSINESS_RULES_PATH || 'config/business-rules.json', 'utf-8'));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return JSON.stringify({ error: 'QUALITY_RULES_MISSING', message: '无法加载business-rules.json' });
|
|
27
|
+
}
|
|
28
|
+
const qsConfig = rulesConfig.qualityScoringExtended;
|
|
29
|
+
if (!qsConfig) {
|
|
30
|
+
return JSON.stringify({ error: 'QUALITY_RULES_MISSING', message: 'qualityScoringExtended配置段缺失' });
|
|
31
|
+
}
|
|
32
|
+
let policyDocuments = [];
|
|
33
|
+
try {
|
|
34
|
+
const policyConfig = JSON.parse(readFileSync(process.env.POLICY_REFS_PATH || 'config/policy-references.json', 'utf-8'));
|
|
35
|
+
const stage = policyConfig.policyReferences?.find((s) => s.stage === 'QUALITY_SCORE');
|
|
36
|
+
policyDocuments = stage?.documents ?? [];
|
|
37
|
+
}
|
|
38
|
+
catch { /* empty */ }
|
|
39
|
+
const weights = qsConfig.weights;
|
|
40
|
+
const weightSum = Object.values(weights).reduce((a, b) => a + b, 0);
|
|
41
|
+
if (Math.abs(weightSum - 1) > 0.001) {
|
|
42
|
+
return JSON.stringify({ error: 'WEIGHT_SUM_INVALID', message: `权重和=${weightSum},必须为1` });
|
|
43
|
+
}
|
|
44
|
+
let data;
|
|
45
|
+
try {
|
|
46
|
+
const raw = readFileSync(args.dataSource, 'utf-8');
|
|
47
|
+
data = JSON.parse(raw);
|
|
48
|
+
if (!Array.isArray(data))
|
|
49
|
+
data = [data];
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return JSON.stringify({ error: 'DATA_LOAD_FAILED', message: `无法加载数据: ${args.dataSource}` });
|
|
53
|
+
}
|
|
54
|
+
const dimensions = [];
|
|
55
|
+
const normalityConfig = qsConfig.normality;
|
|
56
|
+
const normalityScore = scoreNormality(data, normalityConfig);
|
|
57
|
+
dimensions.push({ dimension: 'normality', score: normalityScore.score, weight: weights.normality ?? 0, issues: normalityScore.issues });
|
|
58
|
+
const completenessConfig = rulesConfig.qualityScoring?.completeness;
|
|
59
|
+
const completenessScore = scoreCompleteness(data, completenessConfig);
|
|
60
|
+
dimensions.push({ dimension: 'completeness', score: completenessScore.score, weight: weights.completeness ?? 0, issues: completenessScore.issues });
|
|
61
|
+
const accuracyScore = scoreAccuracy(data);
|
|
62
|
+
dimensions.push({ dimension: 'accuracy', score: accuracyScore.score, weight: weights.accuracy ?? 0, issues: accuracyScore.issues });
|
|
63
|
+
const consistencyScore = scoreConsistency(data);
|
|
64
|
+
dimensions.push({ dimension: 'consistency', score: consistencyScore.score, weight: weights.consistency ?? 0, issues: consistencyScore.issues });
|
|
65
|
+
const timelinessScore = scoreTimeliness(data);
|
|
66
|
+
dimensions.push({ dimension: 'timeliness', score: timelinessScore.score, weight: weights.timeliness ?? 0, issues: timelinessScore.issues });
|
|
67
|
+
const accessibilityConfig = qsConfig.accessibility;
|
|
68
|
+
const accessibilityScore = scoreAccessibility(data, accessibilityConfig);
|
|
69
|
+
dimensions.push({ dimension: 'accessibility', score: accessibilityScore.score, weight: weights.accessibility ?? 0, issues: accessibilityScore.issues });
|
|
70
|
+
const totalScore = dimensions.reduce((sum, d) => sum + d.score * d.weight, 0);
|
|
71
|
+
const report = {
|
|
72
|
+
dimensions,
|
|
73
|
+
totalScore: Math.round(totalScore * 10) / 10,
|
|
74
|
+
policyReferences: policyDocuments,
|
|
75
|
+
timestamp: new Date().toISOString(),
|
|
76
|
+
};
|
|
77
|
+
return JSON.stringify(report, null, 2);
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
return JSON.stringify({ error: 'UNKNOWN_ERROR', message: e.message });
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function scoreNormality(data, config) {
|
|
86
|
+
const issues = [];
|
|
87
|
+
const pattern = new RegExp(config.fieldNamePattern);
|
|
88
|
+
let fieldCount = 0, validCount = 0;
|
|
89
|
+
for (const row of data) {
|
|
90
|
+
if (typeof row === 'object' && row !== null) {
|
|
91
|
+
for (const key of Object.keys(row)) {
|
|
92
|
+
fieldCount++;
|
|
93
|
+
if (pattern.test(key))
|
|
94
|
+
validCount++;
|
|
95
|
+
else
|
|
96
|
+
issues.push(`字段名不规范: ${key}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
for (const req of config.requiredFields) {
|
|
101
|
+
const hasField = data.some(row => typeof row === 'object' && row !== null && req in row);
|
|
102
|
+
if (!hasField)
|
|
103
|
+
issues.push(`必填字段缺失: ${req}`);
|
|
104
|
+
}
|
|
105
|
+
const score = fieldCount > 0 ? (validCount / fieldCount) * 100 : 50;
|
|
106
|
+
return { score: Math.round(score), issues };
|
|
107
|
+
}
|
|
108
|
+
function scoreCompleteness(data, config) {
|
|
109
|
+
const issues = [];
|
|
110
|
+
let totalFields = 0, missingFields = 0;
|
|
111
|
+
for (const row of data) {
|
|
112
|
+
if (typeof row === 'object' && row !== null) {
|
|
113
|
+
for (const [key, value] of Object.entries(row)) {
|
|
114
|
+
totalFields++;
|
|
115
|
+
if (value === null || value === undefined || config.missingMarkers.includes(String(value))) {
|
|
116
|
+
missingFields++;
|
|
117
|
+
issues.push(`字段缺失: ${key}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const score = totalFields > 0 ? ((totalFields - missingFields) / totalFields) * 100 : 50;
|
|
123
|
+
return { score: Math.round(score), issues };
|
|
124
|
+
}
|
|
125
|
+
function scoreAccuracy(data) {
|
|
126
|
+
const issues = [];
|
|
127
|
+
let validCount = 0;
|
|
128
|
+
for (const row of data) {
|
|
129
|
+
if (typeof row === 'object' && row !== null) {
|
|
130
|
+
const obj = row;
|
|
131
|
+
if (obj.age !== undefined) {
|
|
132
|
+
const age = Number(obj.age);
|
|
133
|
+
if (age < 0 || age > 150)
|
|
134
|
+
issues.push(`年龄异常: ${obj.age}`);
|
|
135
|
+
else
|
|
136
|
+
validCount++;
|
|
137
|
+
}
|
|
138
|
+
if (obj.phone !== undefined) {
|
|
139
|
+
if (!/^1[3-9]\d{9}$/.test(String(obj.phone)))
|
|
140
|
+
issues.push(`手机号格式错误: ${obj.phone}`);
|
|
141
|
+
else
|
|
142
|
+
validCount++;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return { score: Math.min(100, 60 + validCount * 5), issues };
|
|
147
|
+
}
|
|
148
|
+
function scoreConsistency(data) {
|
|
149
|
+
const issues = [];
|
|
150
|
+
let consistentCount = 0;
|
|
151
|
+
for (const row of data) {
|
|
152
|
+
if (typeof row === 'object' && row !== null) {
|
|
153
|
+
const obj = row;
|
|
154
|
+
if (obj.startDate && obj.endDate) {
|
|
155
|
+
if (new Date(obj.startDate) <= new Date(obj.endDate))
|
|
156
|
+
consistentCount++;
|
|
157
|
+
else
|
|
158
|
+
issues.push(`日期不一致: startDate=${obj.startDate} > endDate=${obj.endDate}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return { score: Math.min(100, 60 + consistentCount * 5), issues };
|
|
163
|
+
}
|
|
164
|
+
function scoreTimeliness(data) {
|
|
165
|
+
const issues = [];
|
|
166
|
+
const now = Date.now();
|
|
167
|
+
const thresholdMs = 720 * 60 * 60 * 1000;
|
|
168
|
+
let freshCount = 0;
|
|
169
|
+
for (const row of data) {
|
|
170
|
+
if (typeof row === 'object' && row !== null) {
|
|
171
|
+
const obj = row;
|
|
172
|
+
if (obj.updatedAt) {
|
|
173
|
+
const updated = new Date(obj.updatedAt).getTime();
|
|
174
|
+
if (now - updated < thresholdMs)
|
|
175
|
+
freshCount++;
|
|
176
|
+
else
|
|
177
|
+
issues.push(`数据过期: updatedAt=${obj.updatedAt}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return { score: Math.min(100, 50 + freshCount * 10), issues };
|
|
182
|
+
}
|
|
183
|
+
function scoreAccessibility(data, config) {
|
|
184
|
+
const issues = [];
|
|
185
|
+
const accessibleRate = data.length > 0 ? 1 : 0;
|
|
186
|
+
if (accessibleRate < config.minAccessibleRate) {
|
|
187
|
+
issues.push(`可访问率${accessibleRate} < ${config.minAccessibleRate}`);
|
|
188
|
+
}
|
|
189
|
+
return { score: accessibleRate >= config.minAccessibleRate ? 100 : 50, issues };
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=index.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@liuhange/dsh-data-asset-quality-score",
|
|
3
|
+
"description": "Data quality scoring plugin: six-dimension quantitative scoring (normality + completeness + accuracy + consistency + timeliness + accessibility)",
|
|
4
|
+
"version": "3.0.0",
|
|
5
|
+
"publishConfig": { "access": "public" },
|
|
6
|
+
"repository": { "type": "git", "url": "git+https://github.com/liuhange789/data-asset-inspector.git", "directory": "packages/data-asset/data-asset-quality-score" },
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "lib/index.js",
|
|
9
|
+
"types": "lib/types/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": { "types": "./lib/types/index.d.ts", "default": "./lib/index.js" },
|
|
12
|
+
"./src/*": "./src/*",
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": ["lib/**/*.js", "lib/types/**/*.d.ts"],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"scripts": {
|
|
18
|
+
"typecheck": "tsc --noEmit",
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"build": "tsc"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"@deepseek-ai/cordis": "^4.0.0",
|
|
24
|
+
"@deepseek-ai/dsh-tools": "0.0.1-rc.1",
|
|
25
|
+
"@liuhange/dsh-data-asset-shared": "^3.0.0"
|
|
26
|
+
},
|
|
27
|
+
"dsh": { "bundle": { "patch": true } }
|
|
28
|
+
}
|