dsh-data-quality 0.1.3 → 0.3.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/CHANGELOG.md +33 -0
- package/README.es.md +7 -1
- package/README.hi.md +7 -1
- package/README.md +22 -9
- package/README.pt.md +7 -1
- package/README.zh.md +7 -1
- package/cordis.patch.yml +11 -0
- package/lib/index.js +1609 -145
- package/lib/types/config.d.ts +12 -0
- package/lib/types/config.d.ts.map +1 -1
- package/lib/types/config.js +35 -0
- package/lib/types/config.js.map +1 -1
- package/lib/types/contract.d.ts +83 -0
- package/lib/types/contract.d.ts.map +1 -0
- package/lib/types/contract.js +144 -0
- package/lib/types/contract.js.map +1 -0
- package/lib/types/dataset.d.ts +50 -3
- package/lib/types/dataset.d.ts.map +1 -1
- package/lib/types/dataset.js +76 -21
- package/lib/types/dataset.js.map +1 -1
- package/lib/types/events.d.ts +2 -0
- package/lib/types/events.d.ts.map +1 -1
- package/lib/types/events.js.map +1 -1
- package/lib/types/index.d.ts +13 -8
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +17 -6
- package/lib/types/index.js.map +1 -1
- package/lib/types/presets.d.ts +28 -0
- package/lib/types/presets.d.ts.map +1 -0
- package/lib/types/presets.js +134 -0
- package/lib/types/presets.js.map +1 -0
- package/lib/types/profile.d.ts +40 -1
- package/lib/types/profile.d.ts.map +1 -1
- package/lib/types/profile.js +66 -10
- package/lib/types/profile.js.map +1 -1
- package/lib/types/provider-local.d.ts +7 -1
- package/lib/types/provider-local.d.ts.map +1 -1
- package/lib/types/provider-local.js +72 -2
- package/lib/types/provider-local.js.map +1 -1
- package/lib/types/report-html.d.ts +41 -0
- package/lib/types/report-html.d.ts.map +1 -0
- package/lib/types/report-html.js +192 -0
- package/lib/types/report-html.js.map +1 -0
- package/lib/types/scorecard.d.ts +50 -0
- package/lib/types/scorecard.d.ts.map +1 -0
- package/lib/types/scorecard.js +186 -0
- package/lib/types/scorecard.js.map +1 -0
- package/lib/types/service.d.ts +32 -2
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js.map +1 -1
- package/lib/types/store.d.ts +21 -1
- package/lib/types/store.d.ts.map +1 -1
- package/lib/types/store.js +13 -1
- package/lib/types/store.js.map +1 -1
- package/lib/types/tools/clean.d.ts.map +1 -1
- package/lib/types/tools/clean.js +76 -1
- package/lib/types/tools/clean.js.map +1 -1
- package/lib/types/tools/profile-report-schema.d.ts +319 -0
- package/lib/types/tools/profile-report-schema.d.ts.map +1 -0
- package/lib/types/tools/profile-report-schema.js +96 -0
- package/lib/types/tools/profile-report-schema.js.map +1 -0
- package/lib/types/tools/profile.d.ts.map +1 -1
- package/lib/types/tools/profile.js +6 -53
- package/lib/types/tools/profile.js.map +1 -1
- package/lib/types/tools/report.d.ts +14 -0
- package/lib/types/tools/report.d.ts.map +1 -0
- package/lib/types/tools/report.js +112 -0
- package/lib/types/tools/report.js.map +1 -0
- package/lib/types/tools/shared.d.ts.map +1 -1
- package/lib/types/tools/shared.js +22 -2
- package/lib/types/tools/shared.js.map +1 -1
- package/lib/types/tools/verify.d.ts.map +1 -1
- package/lib/types/tools/verify.js +36 -1
- package/lib/types/tools/verify.js.map +1 -1
- package/lib/types/verify.d.ts +49 -3
- package/lib/types/verify.d.ts.map +1 -1
- package/lib/types/verify.js +111 -4
- package/lib/types/verify.js.map +1 -1
- package/lib/types/version.d.ts +8 -1
- package/lib/types/version.d.ts.map +1 -1
- package/lib/types/version.js +8 -1
- package/lib/types/version.js.map +1 -1
- package/package.json +1 -1
- package/src/config.ts +52 -0
- package/src/contract.ts +190 -0
- package/src/dataset.ts +101 -21
- package/src/events.ts +2 -0
- package/src/index.ts +27 -8
- package/src/presets.ts +146 -0
- package/src/profile.ts +105 -11
- package/src/provider-local.ts +77 -3
- package/src/report-html.ts +208 -0
- package/src/scorecard.ts +244 -0
- package/src/service.ts +34 -2
- package/src/store.ts +28 -2
- package/src/tools/clean.ts +76 -1
- package/src/tools/profile-report-schema.ts +98 -0
- package/src/tools/profile.ts +8 -56
- package/src/tools/report.ts +137 -0
- package/src/tools/shared.ts +21 -2
- package/src/tools/verify.ts +40 -2
- package/src/verify.ts +151 -5
- package/src/version.ts +9 -1
package/src/verify.ts
CHANGED
|
@@ -41,6 +41,45 @@ export class VerifyRuleError extends Error {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/** The metrics a verification expectation can reconcile. */
|
|
45
|
+
export type VerifyMetric = 'rowCount' | 'columnSum' | 'columnMean' | 'uniqueCount' | 'nullCount'
|
|
46
|
+
|
|
47
|
+
/** One metric expectation: an expected value plus an optional relative tolerance. */
|
|
48
|
+
export interface VerifyExpectation {
|
|
49
|
+
readonly metric: VerifyMetric
|
|
50
|
+
/** Required for every metric except `rowCount`. */
|
|
51
|
+
readonly column?: string
|
|
52
|
+
readonly expected: number
|
|
53
|
+
/** Optional relative tolerance in [0, 1]; falls back to the configured `defaultTolerance`. */
|
|
54
|
+
readonly tolerance?: number
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** One expectation's reconciliation outcome. */
|
|
58
|
+
export interface VerifyExpectationResult {
|
|
59
|
+
readonly metric: VerifyMetric
|
|
60
|
+
/** Absent for `rowCount`. */
|
|
61
|
+
readonly column?: string
|
|
62
|
+
readonly expected: number
|
|
63
|
+
readonly actual: number
|
|
64
|
+
readonly tolerance: number
|
|
65
|
+
readonly passed: boolean
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Raised for invalid expectations; `message` names the expectation index and reason. */
|
|
69
|
+
export class VerifyExpectationError extends Error {
|
|
70
|
+
/**
|
|
71
|
+
* @param expectationIndex - index of the offending expectation in the request array.
|
|
72
|
+
* @param message - actionable human-readable detail.
|
|
73
|
+
*/
|
|
74
|
+
constructor(
|
|
75
|
+
readonly expectationIndex: number,
|
|
76
|
+
message: string,
|
|
77
|
+
) {
|
|
78
|
+
super(message)
|
|
79
|
+
this.name = 'VerifyExpectationError'
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
44
83
|
/** One failing row's evidence (capped per rule by `evidenceRowLimit`). */
|
|
45
84
|
export interface VerifyEvidenceRow {
|
|
46
85
|
/** 0-based data row index. */
|
|
@@ -68,6 +107,8 @@ export interface VerifyReport {
|
|
|
68
107
|
readonly passed: boolean
|
|
69
108
|
readonly rowCount: number
|
|
70
109
|
readonly rules: VerifyRuleResult[]
|
|
110
|
+
/** Metric-reconciliation outcomes (empty when no expectations were given). */
|
|
111
|
+
readonly expectations: VerifyExpectationResult[]
|
|
71
112
|
/** Storage-domain key of the persisted report, when persistence is on (set by the provider). */
|
|
72
113
|
readonly reportKey?: string
|
|
73
114
|
/** Injected generation timestamp (epoch ms). */
|
|
@@ -123,19 +164,118 @@ function evaluate(
|
|
|
123
164
|
|
|
124
165
|
const CROSS_OPS = ['<', '<=', '==', '!=', '>=', '>'] as const
|
|
125
166
|
|
|
167
|
+
/** The metric ids, for validation and diagnostics. */
|
|
168
|
+
const VERIFY_METRICS = ['rowCount', 'columnSum', 'columnMean', 'uniqueCount', 'nullCount'] as const
|
|
169
|
+
|
|
170
|
+
/** Throw unless the expectation is well-formed (metric/column/tolerance). */
|
|
171
|
+
function validateExpectation(table: Table, expectation: VerifyExpectation, index: number): void {
|
|
172
|
+
if (!VERIFY_METRICS.includes(expectation.metric)) {
|
|
173
|
+
throw new VerifyExpectationError(index, `expectation ${index}: unknown metric ${JSON.stringify(expectation.metric)} (expected one of ${VERIFY_METRICS.join(', ')})`)
|
|
174
|
+
}
|
|
175
|
+
if (expectation.metric !== 'rowCount') {
|
|
176
|
+
if (expectation.column === undefined || expectation.column === '') {
|
|
177
|
+
throw new VerifyExpectationError(index, `expectation ${index}: metric ${expectation.metric} requires a column`)
|
|
178
|
+
}
|
|
179
|
+
if (!table.columns.includes(expectation.column)) {
|
|
180
|
+
throw new VerifyExpectationError(index, `expectation ${index}: unknown column ${JSON.stringify(expectation.column)} (columns: ${table.columns.join(', ')})`)
|
|
181
|
+
}
|
|
182
|
+
} else if (expectation.column !== undefined) {
|
|
183
|
+
throw new VerifyExpectationError(index, `expectation ${index}: metric rowCount takes no column, got ${JSON.stringify(expectation.column)}`)
|
|
184
|
+
}
|
|
185
|
+
if (expectation.tolerance !== undefined && (typeof expectation.tolerance !== 'number' || !Number.isFinite(expectation.tolerance) || expectation.tolerance < 0 || expectation.tolerance > 1)) {
|
|
186
|
+
throw new VerifyExpectationError(index, `expectation ${index}: tolerance must be a finite number in [0, 1], got ${String(expectation.tolerance)}`)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Compute the deterministic actual value of one expectation's metric. */
|
|
191
|
+
function metricValueOf(table: Table, expectation: VerifyExpectation): number {
|
|
192
|
+
switch (expectation.metric) {
|
|
193
|
+
case 'rowCount':
|
|
194
|
+
return table.rows.length
|
|
195
|
+
case 'nullCount': {
|
|
196
|
+
let count = 0
|
|
197
|
+
for (const row of table.rows) {
|
|
198
|
+
if (isMissing(row[expectation.column as string])) count += 1
|
|
199
|
+
}
|
|
200
|
+
return count
|
|
201
|
+
}
|
|
202
|
+
case 'uniqueCount': {
|
|
203
|
+
const distinct = new Set<string>()
|
|
204
|
+
for (const row of table.rows) {
|
|
205
|
+
const cell = row[expectation.column as string]
|
|
206
|
+
if (isMissing(cell)) continue
|
|
207
|
+
distinct.add(typeof cell === 'string' ? cell : JSON.stringify(cell))
|
|
208
|
+
}
|
|
209
|
+
return distinct.size
|
|
210
|
+
}
|
|
211
|
+
case 'columnSum':
|
|
212
|
+
case 'columnMean': {
|
|
213
|
+
let sum = 0
|
|
214
|
+
let count = 0
|
|
215
|
+
for (const row of table.rows) {
|
|
216
|
+
const value = parseNumeric(row[expectation.column as string])
|
|
217
|
+
if (value === undefined) continue
|
|
218
|
+
sum += value
|
|
219
|
+
count += 1
|
|
220
|
+
}
|
|
221
|
+
return expectation.metric === 'columnSum' ? sum : count === 0 ? 0 : sum / count
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Reconcile each expectation against its deterministic actual value. A
|
|
228
|
+
* mismatch is a normal `passed: false` result, never a thrown error; invalid
|
|
229
|
+
* metrics, columns, and tolerances fail loud.
|
|
230
|
+
* @param table - the parsed dataset.
|
|
231
|
+
* @param expectations - the expectations to reconcile.
|
|
232
|
+
* @param defaultTolerance - configured fallback relative tolerance.
|
|
233
|
+
* @param signal - optional abort signal.
|
|
234
|
+
* @returns one outcome per expectation.
|
|
235
|
+
*/
|
|
236
|
+
export function verifyExpectations(
|
|
237
|
+
table: Table,
|
|
238
|
+
expectations: readonly VerifyExpectation[],
|
|
239
|
+
defaultTolerance: number,
|
|
240
|
+
signal?: AbortSignal,
|
|
241
|
+
): VerifyExpectationResult[] {
|
|
242
|
+
return expectations.map((expectation, index) => {
|
|
243
|
+
throwIfAborted(signal)
|
|
244
|
+
validateExpectation(table, expectation, index)
|
|
245
|
+
const actual = metricValueOf(table, expectation)
|
|
246
|
+
const tolerance = expectation.tolerance ?? defaultTolerance
|
|
247
|
+
const passed = numericClose(actual, expectation.expected, tolerance)
|
|
248
|
+
return {
|
|
249
|
+
metric: expectation.metric,
|
|
250
|
+
...(expectation.column !== undefined ? { column: expectation.column } : {}),
|
|
251
|
+
expected: expectation.expected,
|
|
252
|
+
actual,
|
|
253
|
+
tolerance,
|
|
254
|
+
passed,
|
|
255
|
+
}
|
|
256
|
+
})
|
|
257
|
+
}
|
|
258
|
+
|
|
126
259
|
/**
|
|
127
260
|
* Apply verification rules over a parsed table. A missing cell fails every
|
|
128
|
-
* rule that reads it. The overall `passed` is the conjunction of rule passes
|
|
129
|
-
* a failing dataset is a normal result, never a
|
|
261
|
+
* rule that reads it. The overall `passed` is the conjunction of rule passes
|
|
262
|
+
* and expectation passes; a failing dataset is a normal result, never a
|
|
263
|
+
* thrown error.
|
|
130
264
|
* @param table - the parsed dataset.
|
|
131
265
|
* @param rules - non-empty rule list.
|
|
132
|
-
* @param options - evidence cap, injected clock for `freshness`, abort signal.
|
|
266
|
+
* @param options - evidence cap, injected clock for `freshness`, optional expectations, default tolerance, abort signal.
|
|
133
267
|
* @returns the verify report (without the dataset label; the caller adds it).
|
|
134
268
|
*/
|
|
135
269
|
export function verifyTable(
|
|
136
270
|
table: Table,
|
|
137
271
|
rules: readonly VerifyRule[],
|
|
138
|
-
options: {
|
|
272
|
+
options: {
|
|
273
|
+
evidenceRowLimit: number
|
|
274
|
+
now: () => number
|
|
275
|
+
signal?: AbortSignal | undefined
|
|
276
|
+
expectations?: readonly VerifyExpectation[] | undefined
|
|
277
|
+
defaultTolerance?: number | undefined
|
|
278
|
+
},
|
|
139
279
|
): Omit<VerifyReport, 'dataset'> {
|
|
140
280
|
if (rules.length === 0) {
|
|
141
281
|
throw new VerifyRuleError(0, 'rules must be a non-empty array')
|
|
@@ -332,10 +472,12 @@ export function verifyTable(
|
|
|
332
472
|
}
|
|
333
473
|
results.push(result)
|
|
334
474
|
}
|
|
475
|
+
const expectations = verifyExpectations(table, options.expectations ?? [], options.defaultTolerance ?? 1e-9, options.signal)
|
|
335
476
|
return {
|
|
336
|
-
passed: results.every((result) => result.passed),
|
|
477
|
+
passed: results.every((result) => result.passed) && expectations.every((expectation) => expectation.passed),
|
|
337
478
|
rowCount: table.rows.length,
|
|
338
479
|
rules: results,
|
|
480
|
+
expectations,
|
|
339
481
|
generatedAt: options.now(),
|
|
340
482
|
}
|
|
341
483
|
}
|
|
@@ -354,6 +496,10 @@ export function renderVerifyText(report: VerifyReport): string {
|
|
|
354
496
|
lines.push(` … and ${rule.failedCount - rule.evidence.length} more failing row(s)`)
|
|
355
497
|
}
|
|
356
498
|
}
|
|
499
|
+
for (const expectation of report.expectations) {
|
|
500
|
+
const target = expectation.column !== undefined ? `${expectation.metric}(${expectation.column})` : expectation.metric
|
|
501
|
+
lines.push(`- [${expectation.passed ? 'pass' : 'FAIL'}] expectation ${target}: actual ${expectation.actual} vs expected ${expectation.expected} (tolerance ${expectation.tolerance})`)
|
|
502
|
+
}
|
|
357
503
|
return lines.join('\n')
|
|
358
504
|
}
|
|
359
505
|
|
package/src/version.ts
CHANGED
|
@@ -5,4 +5,12 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/** The package version reported in persisted reports. */
|
|
8
|
-
export const VERSION = '0.
|
|
8
|
+
export const VERSION = '0.3.0'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Version of the persisted report schema. Bump it whenever a report's
|
|
12
|
+
* canonical shape changes in a way old consumers cannot read (the durable
|
|
13
|
+
* `data_quality` records keep their own `schemaVersion` so a future reader
|
|
14
|
+
* can detect and reject an incompatible record instead of misreading it).
|
|
15
|
+
*/
|
|
16
|
+
export const REPORT_SCHEMA_VERSION = 1
|