deaf-intelligence 1.0.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.
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Data validation pipeline — 5 rules applied to scraped data.
3
+ *
4
+ * Runs after scraping, before Excel generation. Flags issues but
5
+ * does not modify data — the scraper stores raw values, validation
6
+ * produces a report.
7
+ *
8
+ * Rules:
9
+ * 1. Future dates — no data point should be in the future
10
+ * 2. Negative values — streams/listeners/saves/adds can't be negative
11
+ * 3. Null/missing — required fields must be present
12
+ * 4. Spikes — value > 10× rolling average = flag (bot removal or viral)
13
+ * 5. Gaps — missing days in timeseries = flag
14
+ *
15
+ * SPEC-10 #5, P0.
16
+ */
17
+ export interface ValidationIssue {
18
+ rule: "future_date" | "negative" | "null_required" | "spike" | "gap";
19
+ severity: "error" | "warning";
20
+ track?: string;
21
+ metric?: string;
22
+ date?: string;
23
+ value?: number;
24
+ message: string;
25
+ }
26
+ export interface ValidationReport {
27
+ timestamp: string;
28
+ issues: ValidationIssue[];
29
+ trackCount: number;
30
+ metricCount: number;
31
+ dateRange: {
32
+ from: string;
33
+ to: string;
34
+ } | null;
35
+ }
36
+ /**
37
+ * Validate S4A scraped data before Excel generation.
38
+ */
39
+ export declare function validateS4AData(s4a: any): ValidationReport;
40
+ /**
41
+ * Print validation report to console.
42
+ */
43
+ export declare function printValidationReport(report: ValidationReport): void;
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Data validation pipeline — 5 rules applied to scraped data.
3
+ *
4
+ * Runs after scraping, before Excel generation. Flags issues but
5
+ * does not modify data — the scraper stores raw values, validation
6
+ * produces a report.
7
+ *
8
+ * Rules:
9
+ * 1. Future dates — no data point should be in the future
10
+ * 2. Negative values — streams/listeners/saves/adds can't be negative
11
+ * 3. Null/missing — required fields must be present
12
+ * 4. Spikes — value > 10× rolling average = flag (bot removal or viral)
13
+ * 5. Gaps — missing days in timeseries = flag
14
+ *
15
+ * SPEC-10 #5, P0.
16
+ */
17
+ const METRICS = ["streams", "listeners", "saves", "playlist_adds", "streams_per_listener"];
18
+ /**
19
+ * Validate S4A scraped data before Excel generation.
20
+ */
21
+ export function validateS4AData(s4a) {
22
+ const issues = [];
23
+ const today = new Date().toISOString().slice(0, 10);
24
+ let trackCount = 0;
25
+ let earliestDate = "9999-99-99";
26
+ let latestDate = "0000-00-00";
27
+ if (!s4a?.perSong) {
28
+ issues.push({ rule: "null_required", severity: "error", message: "s4a.perSong is missing — no per-song data" });
29
+ return { timestamp: new Date().toISOString(), issues, trackCount: 0, metricCount: 0, dateRange: null };
30
+ }
31
+ for (const [tid, tData] of Object.entries(s4a.perSong)) {
32
+ trackCount++;
33
+ const trackName = tData.metadata?.name || tData.name || tid;
34
+ for (const metric of METRICS) {
35
+ const mData = tData[metric];
36
+ if (!mData)
37
+ continue;
38
+ // Rule 3: Required fields
39
+ if (mData.current_period_agg == null) {
40
+ issues.push({
41
+ rule: "null_required", severity: "warning", track: trackName, metric,
42
+ message: `${metric}.current_period_agg is null`,
43
+ });
44
+ }
45
+ const ts = mData.current_period_timeseries;
46
+ if (!ts || !Array.isArray(ts) || ts.length === 0)
47
+ continue;
48
+ // Collect dates for gap detection
49
+ const dates = [];
50
+ let prevDate = null;
51
+ const rollingWindow = [];
52
+ for (const pt of ts) {
53
+ const date = pt.x;
54
+ const val = Number(pt.y) || 0;
55
+ if (!date)
56
+ continue;
57
+ dates.push(date);
58
+ if (date < earliestDate)
59
+ earliestDate = date;
60
+ if (date > latestDate)
61
+ latestDate = date;
62
+ // Rule 1: Future dates
63
+ if (date > today) {
64
+ issues.push({
65
+ rule: "future_date", severity: "error", track: trackName, metric, date, value: val,
66
+ message: `Data point in the future: ${date} (today: ${today})`,
67
+ });
68
+ }
69
+ // Rule 2: Negative values
70
+ if (val < 0 && metric !== "streams_per_listener") {
71
+ issues.push({
72
+ rule: "negative", severity: "warning", track: trackName, metric, date, value: val,
73
+ message: `Negative value ${val} — possible bot removal or data correction`,
74
+ });
75
+ }
76
+ // Rule 4: Spike detection (>10× rolling 30-day average)
77
+ rollingWindow.push(val);
78
+ if (rollingWindow.length > 30)
79
+ rollingWindow.shift();
80
+ if (rollingWindow.length >= 14) {
81
+ const avg = rollingWindow.slice(0, -1).reduce((a, b) => a + b, 0) / (rollingWindow.length - 1);
82
+ if (avg > 0 && val > avg * 10) {
83
+ issues.push({
84
+ rule: "spike", severity: "warning", track: trackName, metric, date, value: val,
85
+ message: `Spike: ${val} is ${(val / avg).toFixed(1)}× rolling avg (${avg.toFixed(0)})`,
86
+ });
87
+ }
88
+ }
89
+ // Rule 5: Gap detection (missing days)
90
+ if (prevDate) {
91
+ const prevMs = new Date(prevDate).getTime();
92
+ const curMs = new Date(date).getTime();
93
+ const dayGap = Math.round((curMs - prevMs) / 86400000);
94
+ if (dayGap > 1) {
95
+ issues.push({
96
+ rule: "gap", severity: "warning", track: trackName, metric, date,
97
+ message: `${dayGap - 1} day gap: ${prevDate} → ${date}`,
98
+ });
99
+ }
100
+ }
101
+ prevDate = date;
102
+ }
103
+ }
104
+ }
105
+ return {
106
+ timestamp: new Date().toISOString(),
107
+ issues,
108
+ trackCount,
109
+ metricCount: METRICS.length,
110
+ dateRange: earliestDate < "9999" ? { from: earliestDate, to: latestDate } : null,
111
+ };
112
+ }
113
+ /**
114
+ * Print validation report to console.
115
+ */
116
+ export function printValidationReport(report) {
117
+ const errors = report.issues.filter(i => i.severity === "error");
118
+ const warnings = report.issues.filter(i => i.severity === "warning");
119
+ console.log(`\n=== Validation Report (${report.timestamp}) ===`);
120
+ console.log(`Tracks: ${report.trackCount} | Metrics: ${report.metricCount}`);
121
+ if (report.dateRange)
122
+ console.log(`Date range: ${report.dateRange.from} → ${report.dateRange.to}`);
123
+ console.log(`Issues: ${errors.length} errors, ${warnings.length} warnings`);
124
+ if (errors.length > 0) {
125
+ console.log(`\nERRORS:`);
126
+ for (const e of errors)
127
+ console.log(` ❌ [${e.track || ""}] ${e.message}`);
128
+ }
129
+ if (warnings.length > 0) {
130
+ console.log(`\nWARNINGS (first 20):`);
131
+ for (const w of warnings.slice(0, 20))
132
+ console.log(` ⚠️ [${w.track || ""}] ${w.message}`);
133
+ if (warnings.length > 20)
134
+ console.log(` ... and ${warnings.length - 20} more`);
135
+ }
136
+ if (report.issues.length === 0)
137
+ console.log(`\n✅ All checks passed.`);
138
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "deaf-intelligence",
3
+ "version": "1.0.1",
4
+ "description": "Spotify career intelligence. Track any artist, build history, get insights. MCP server.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "deaf-intelligence": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc && chmod +x dist/index.js",
15
+ "prepublishOnly": "npm run build",
16
+ "start": "node dist/index.js"
17
+ },
18
+ "keywords": [
19
+ "mcp",
20
+ "spotify",
21
+ "music",
22
+ "analytics",
23
+ "artist",
24
+ "archivist",
25
+ "streaming",
26
+ "career",
27
+ "intelligence",
28
+ "model-context-protocol"
29
+ ],
30
+ "author": "Karel Dittrich (https://deaf.audio)",
31
+ "license": "SEE LICENSE IN LICENSE.md",
32
+ "homepage": "https://deaf.audio",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/kareldittrich/deaf-audio.git",
36
+ "directory": "artist-os"
37
+ },
38
+ "engines": {
39
+ "node": ">=20.0.0"
40
+ },
41
+ "dependencies": {
42
+ "@modelcontextprotocol/sdk": "^1.27.1",
43
+ "exceljs": "^4.4.0",
44
+ "node-sqlite3-wasm": "^0.8.55",
45
+ "playwright-core": "^1.58.2",
46
+ "zod": "^4.3.6"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^25.5.0",
50
+ "typescript": "^5.9.3"
51
+ }
52
+ }