@futux/dqi 0.0.0-stage → 0.1.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/src/engine.ts ADDED
@@ -0,0 +1,79 @@
1
+ import { Schema } from "./schema.js";
2
+ import { DQIReport } from "./report.js";
3
+ import { EvaluationReport, IssueItem } from "./types.js";
4
+ import { scoreCompleteness } from "./dimensions/completeness.js";
5
+ import { scoreValidity } from "./dimensions/validity.js";
6
+ import { scoreUniqueness } from "./dimensions/uniqueness.js";
7
+ import { scoreOutliers } from "./dimensions/outliers.js";
8
+ import Papa from "papaparse";
9
+
10
+ export class DQIEngine {
11
+ public schema: Schema | null;
12
+
13
+ constructor(schema: Schema | null = null) {
14
+ this.schema = schema;
15
+ }
16
+
17
+ evaluate(csvInput: string | Record<string, string>[]): DQIReport {
18
+ let rows: Record<string, string>[];
19
+
20
+ if (typeof csvInput === "string") {
21
+ const parsed = Papa.parse<Record<string, string>>(csvInput, {
22
+ header: true,
23
+ skipEmptyLines: true,
24
+ });
25
+ rows = parsed.data;
26
+ if (!this.schema) {
27
+ this.schema = Schema.infer(csvInput);
28
+ }
29
+ } else {
30
+ rows = csvInput;
31
+ if (!this.schema) {
32
+ throw new Error("Schema must be provided when evaluating raw row arrays.");
33
+ }
34
+ }
35
+
36
+ const completeness = scoreCompleteness(rows, this.schema);
37
+ const validity = scoreValidity(rows, this.schema);
38
+ const uniqueness = scoreUniqueness(rows, this.schema);
39
+ const outliers = scoreOutliers(rows, this.schema);
40
+
41
+ const w = this.schema.weights;
42
+ const overall =
43
+ completeness.score * w.completeness +
44
+ validity.score * w.validity +
45
+ uniqueness.score * w.uniqueness +
46
+ outliers.score * w.outliers;
47
+
48
+ const roundOverall = Number(overall.toFixed(2));
49
+ let grade: "A" | "B" | "C" | "D" | "F" = "F";
50
+ if (roundOverall >= 90) grade = "A";
51
+ else if (roundOverall >= 75) grade = "B";
52
+ else if (roundOverall >= 60) grade = "C";
53
+ else if (roundOverall >= 45) grade = "D";
54
+
55
+ const allIssues: IssueItem[] = [
56
+ ...completeness.issues,
57
+ ...validity.issues,
58
+ ...uniqueness.issues,
59
+ ...outliers.issues,
60
+ ];
61
+
62
+ const reportData: EvaluationReport = {
63
+ overallScore: roundOverall,
64
+ grade,
65
+ totalRecords: rows.length,
66
+ dimensions: {
67
+ completeness,
68
+ validity,
69
+ uniqueness,
70
+ outliers,
71
+ },
72
+ weights: w,
73
+ allIssues,
74
+ generatedAt: new Date().toISOString(),
75
+ };
76
+
77
+ return new DQIReport(reportData);
78
+ }
79
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export { DQIEngine } from "./engine.js";
2
+ export { Schema } from "./schema.js";
3
+ export { DQIReport } from "./report.js";
4
+ export * from "./types.js";
5
+ export { jaroWinkler } from "./dimensions/uniqueness.js";
package/src/report.ts ADDED
@@ -0,0 +1,100 @@
1
+ import { EvaluationReport } from "./types.js";
2
+
3
+ export class DQIReport {
4
+ public data: EvaluationReport;
5
+
6
+ constructor(data: EvaluationReport) {
7
+ this.data = data;
8
+ }
9
+
10
+ get overallScore(): number {
11
+ return this.data.overallScore;
12
+ }
13
+
14
+ get grade(): string {
15
+ return this.data.grade;
16
+ }
17
+
18
+ toJSON(): string {
19
+ return JSON.stringify(this.data, null, 2);
20
+ }
21
+
22
+ toHTML(): string {
23
+ const { overallScore, grade, totalRecords, dimensions, allIssues, generatedAt } = this.data;
24
+
25
+ return `<!DOCTYPE html>
26
+ <html lang="en">
27
+ <head>
28
+ <meta charset="UTF-8">
29
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
30
+ <title>DQI Report</title>
31
+ <style>
32
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #f4f6f9; color: #2d3748; padding: 2rem; margin: 0; }
33
+ .header { margin-bottom: 2rem; }
34
+ .card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
35
+ .card { background: #fff; border-radius: 10px; padding: 1.5rem; text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
36
+ .score-val { font-size: 2.2rem; font-weight: 800; color: #2b6cb0; }
37
+ .badge { display: inline-block; padding: 0.25rem 0.5rem; border-radius: 4px; font-weight: bold; }
38
+ .grade-A { background: #c6f6d5; color: #22543d; }
39
+ .grade-B { background: #bee3f8; color: #2b6cb0; }
40
+ .grade-C { background: #feebc8; color: #7b341e; }
41
+ .grade-D { background: #fed7d7; color: #742a2a; }
42
+ .grade-F { background: #e2e8f0; color: #1a202c; }
43
+ table { width: 100%; border-collapse: collapse; background: #fff; border-radius: 8px; overflow: hidden; margin-top: 1rem; }
44
+ th, td { padding: 0.75rem 1rem; text-align: left; border-bottom: 1px solid #edf2f7; font-size: 0.85rem; }
45
+ th { background: #edf2f7; color: #4a5568; }
46
+ </style>
47
+ </head>
48
+ <body>
49
+ <div class="header">
50
+ <h1>Data Quality Index Report</h1>
51
+ <p>Total records: ${totalRecords} | Generated: ${generatedAt}</p>
52
+ </div>
53
+
54
+ <div class="card-grid">
55
+ <div class="card">
56
+ <div>Overall DQI</div>
57
+ <div class="score-val">${overallScore}</div>
58
+ <span class="badge grade-${grade}">Grade ${grade}</span>
59
+ </div>
60
+ <div class="card">
61
+ <div>Completeness</div>
62
+ <div class="score-val">${dimensions.completeness.score}</div>
63
+ </div>
64
+ <div class="card">
65
+ <div>Validity</div>
66
+ <div class="score-val">${dimensions.validity.score}</div>
67
+ </div>
68
+ <div class="card">
69
+ <div>Uniqueness</div>
70
+ <div class="score-val">${dimensions.uniqueness.score}</div>
71
+ </div>
72
+ <div class="card">
73
+ <div>Outliers</div>
74
+ <div class="score-val">${dimensions.outliers.score}</div>
75
+ </div>
76
+ </div>
77
+
78
+ <h2>Audit Issues (${allIssues.length})</h2>
79
+ ${
80
+ allIssues.length === 0
81
+ ? "<p>No issues detected.</p>"
82
+ : `<table>
83
+ <thead>
84
+ <tr><th>Row</th><th>Field</th><th>Dimension</th><th>Reason</th></tr>
85
+ </thead>
86
+ <tbody>
87
+ ${allIssues
88
+ .slice(0, 500)
89
+ .map(
90
+ (i) =>
91
+ `<tr><td>${i.rowIndex}</td><td>${i.field}</td><td>${i.dimension}</td><td>${i.reason}</td></tr>`
92
+ )
93
+ .join("")}
94
+ </tbody>
95
+ </table>`
96
+ }
97
+ </body>
98
+ </html>`;
99
+ }
100
+ }
package/src/schema.ts ADDED
@@ -0,0 +1,109 @@
1
+ import { FieldType, FieldDefinition, DimensionWeights } from "./types.js";
2
+ import yaml from "js-yaml";
3
+ import Papa from "papaparse";
4
+
5
+ export class Schema {
6
+ public fields: Record<string, FieldDefinition>;
7
+ public weights: DimensionWeights;
8
+ public entityLabel: string;
9
+
10
+ constructor(
11
+ fields: Record<string, FieldDefinition>,
12
+ weights: DimensionWeights = { completeness: 0.25, validity: 0.35, uniqueness: 0.25, outliers: 0.15 },
13
+ entityLabel: string = "Record"
14
+ ) {
15
+ this.fields = fields;
16
+ this.weights = weights;
17
+ this.entityLabel = entityLabel;
18
+ }
19
+
20
+ static fromYAML(yamlString: string): Schema {
21
+ const data = yaml.load(yamlString) as {
22
+ entity_label?: string;
23
+ dimensions?: { weights?: Partial<DimensionWeights> };
24
+ fields?: Record<string, Record<string, unknown>>;
25
+ };
26
+
27
+ const entityLabel = data?.entity_label ?? "Record";
28
+ const weightsCfg = data?.dimensions?.weights ?? {};
29
+ const weights: DimensionWeights = {
30
+ completeness: weightsCfg.completeness ?? 0.25,
31
+ validity: weightsCfg.validity ?? 0.35,
32
+ uniqueness: weightsCfg.uniqueness ?? 0.25,
33
+ outliers: weightsCfg.outliers ?? 0.15,
34
+ };
35
+
36
+ const fields: Record<string, FieldDefinition> = {};
37
+ if (data?.fields) {
38
+ for (const [name, cfg] of Object.entries(data.fields)) {
39
+ fields[name] = {
40
+ name,
41
+ type: (cfg["type"] as FieldType) || FieldType.TEXT,
42
+ required: Boolean(cfg["required"]),
43
+ uniqueKey: Boolean(cfg["unique_key"]),
44
+ fuzzyKey: Boolean(cfg["fuzzy_key"]),
45
+ min: cfg["min"] as number | string | undefined,
46
+ max: cfg["max"] as number | string | undefined,
47
+ pattern: cfg["pattern"] as string | undefined,
48
+ allowedValues: cfg["allowed_values"] as string[] | undefined,
49
+ };
50
+ }
51
+ }
52
+
53
+ return new Schema(fields, weights, entityLabel);
54
+ }
55
+
56
+ static infer(csvContent: string): Schema {
57
+ const parsed = Papa.parse<Record<string, string>>(csvContent, {
58
+ header: true,
59
+ skipEmptyLines: true,
60
+ preview: 500,
61
+ });
62
+
63
+ const rows = parsed.data;
64
+ const fields: Record<string, FieldDefinition> = {};
65
+ const columns = parsed.meta.fields ?? [];
66
+
67
+ for (const col of columns) {
68
+ const lower = col.toLowerCase();
69
+ let detectedType = FieldType.TEXT;
70
+
71
+ if (lower.includes("phone") || lower.includes("tel") || lower.includes("mobile")) {
72
+ detectedType = FieldType.PHONE;
73
+ } else if (lower.includes("email")) {
74
+ detectedType = FieldType.EMAIL;
75
+ } else if (lower.includes("lat") || lower.includes("latitude")) {
76
+ detectedType = FieldType.LATITUDE;
77
+ } else if (lower.includes("lon") || lower.includes("lng") || lower.includes("longitude")) {
78
+ detectedType = FieldType.LONGITUDE;
79
+ } else if (lower.includes("date") || lower.includes("dob") || lower.includes("birth")) {
80
+ detectedType = FieldType.DATE;
81
+ } else if (lower.includes("id") || lower.includes("nin") || lower.includes("uuid") || lower.includes("ref")) {
82
+ detectedType = FieldType.ID;
83
+ } else {
84
+ // Sample inspection
85
+ const sampleVals = rows.map((r) => r[col]).filter((v) => v !== undefined && v !== null && v.trim() !== "");
86
+ const allNumeric = sampleVals.length > 0 && sampleVals.every((v) => !isNaN(Number(v)));
87
+ if (allNumeric) {
88
+ const allInts = sampleVals.every((v) => Number.isInteger(Number(v)));
89
+ detectedType = allInts ? FieldType.INTEGER : FieldType.FLOAT;
90
+ }
91
+ }
92
+
93
+ const nonNullCount = rows.filter((r) => r[col] !== undefined && r[col] !== null && r[col].trim() !== "").length;
94
+ const isRequired = rows.length > 0 && nonNullCount / rows.length > 0.95;
95
+ const isUniqueKey = detectedType === FieldType.ID || lower.includes("id");
96
+
97
+ fields[col] = {
98
+ name: col,
99
+ type: detectedType,
100
+ required: isRequired,
101
+ uniqueKey: isUniqueKey,
102
+ fuzzyKey: detectedType === FieldType.TEXT && (lower.includes("name") || lower.includes("title")),
103
+ autoInferred: true,
104
+ };
105
+ }
106
+
107
+ return new Schema(fields);
108
+ }
109
+ }
package/src/types.ts ADDED
@@ -0,0 +1,63 @@
1
+ export enum FieldType {
2
+ TEXT = "text",
3
+ INTEGER = "integer",
4
+ FLOAT = "float",
5
+ DATE = "date",
6
+ PHONE = "phone",
7
+ EMAIL = "email",
8
+ LATITUDE = "latitude",
9
+ LONGITUDE = "longitude",
10
+ ENUM = "enum",
11
+ ID = "id",
12
+ BOOLEAN = "boolean",
13
+ }
14
+
15
+ export interface FieldDefinition {
16
+ name: string;
17
+ type: FieldType;
18
+ required?: boolean;
19
+ uniqueKey?: boolean;
20
+ fuzzyKey?: boolean;
21
+ min?: number | string;
22
+ max?: number | string;
23
+ pattern?: string;
24
+ allowedValues?: string[];
25
+ autoInferred?: boolean;
26
+ }
27
+
28
+ export interface DimensionWeights {
29
+ completeness: number;
30
+ validity: number;
31
+ uniqueness: number;
32
+ outliers: number;
33
+ }
34
+
35
+ export interface IssueItem {
36
+ rowIndex: number;
37
+ field: string;
38
+ dimension: "completeness" | "validity" | "uniqueness" | "outliers";
39
+ reason: string;
40
+ value?: string | number | null;
41
+ duplicateOf?: number;
42
+ }
43
+
44
+ export interface DimensionResult {
45
+ score: number;
46
+ issues: IssueItem[];
47
+ [key: string]: unknown;
48
+ }
49
+
50
+ export interface EvaluationReport {
51
+ overallScore: number;
52
+ grade: "A" | "B" | "C" | "D" | "F";
53
+ totalRecords: number;
54
+ dimensions: {
55
+ completeness: DimensionResult;
56
+ validity: DimensionResult;
57
+ uniqueness: DimensionResult;
58
+ outliers: DimensionResult;
59
+ };
60
+ weights: DimensionWeights;
61
+ allIssues: IssueItem[];
62
+ generatedAt: string;
63
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "declaration": true,
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "noImplicitAny": true,
11
+ "esModuleInterop": true,
12
+ "skipLibCheck": true,
13
+ "forceConsistentCasingInFileNames": true
14
+ },
15
+ "include": ["src/**/*"]
16
+ }