feat(curation): data quality pipeline — Phases 1-3
Add comprehensive data curation system to clean up the 197K skill dataset and show only quality browse-ready skills to users. Phase 1 — Database exploration: - Explore scripts (explore.ts, explore.mjs, explore.sql) for analysis - Discovered: 69% duplicates, 77% aggregator/fork noise Phase 2 — Data cleanup and classification: - Schema: 6 new curation columns + 4 indexes - curate.mjs: 8-step pipeline (classify, dedup, fork detection, etc.) - Result: 197K → 60K unique → 16K browse-ready skills - Bug fix: securityStatus was computed but never stored during crawl Phase 3 — UI browse-ready filters: - browseReadyFilter applied to 17+ query functions - Homepage stats show accurate browse-ready counts - Stats API filtered (previously had no WHERE clause) - Category counts recalculated (e.g. 45K → 3.1K) - Featured skills exclude duplicates and aggregators Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
652
scripts/curation/explore.mjs
Normal file
652
scripts/curation/explore.mjs
Normal file
@@ -0,0 +1,652 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Phase 1: Database Exploration — Single-Query Version
|
||||
*
|
||||
* Runs ALL analytics in ONE SQL query to handle unstable connections.
|
||||
*
|
||||
* Usage:
|
||||
* PGSSLMODE=disable DATABASE_URL=postgres://... node scripts/curation/explore.mjs
|
||||
*/
|
||||
|
||||
import { createRequire } from 'module';
|
||||
import { writeFileSync } from 'fs';
|
||||
import { resolve, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const projectRoot = resolve(__dirname, '../..');
|
||||
|
||||
let pg;
|
||||
const tryPaths = [
|
||||
'/tmp/package.json',
|
||||
process.env.APPDATA ? resolve(process.env.APPDATA, '..', 'Local', 'Temp', 'package.json') : null,
|
||||
resolve(projectRoot, 'package.json'),
|
||||
].filter(Boolean);
|
||||
for (const p of tryPaths) {
|
||||
try { pg = createRequire(p)('pg'); break; } catch {}
|
||||
}
|
||||
if (!pg) { console.error('pg not found. Run: cd /tmp && npm install pg'); process.exit(1); }
|
||||
|
||||
const DATABASE_URL = process.env.DATABASE_URL || 'postgresql://postgres:postgres@localhost:5432/skillhub';
|
||||
|
||||
process.on('uncaughtException', e => {
|
||||
if (e.code === 'ECONNRESET') return;
|
||||
console.error('Fatal:', e); process.exit(1);
|
||||
});
|
||||
|
||||
// ─── Mega Query ──────────────────────────────────────────
|
||||
// All analytics combined into one JSON result
|
||||
|
||||
const MEGA_SQL = `
|
||||
WITH active AS (
|
||||
SELECT * FROM skills WHERE is_blocked = false
|
||||
),
|
||||
|
||||
-- 1. Overall
|
||||
overall AS (
|
||||
SELECT json_build_object(
|
||||
'total', (SELECT COUNT(*)::int FROM skills),
|
||||
'active', (SELECT COUNT(*)::int FROM active),
|
||||
'blocked', (SELECT COUNT(*) FILTER (WHERE is_blocked)::int FROM skills),
|
||||
'owners', (SELECT COUNT(DISTINCT github_owner)::int FROM active),
|
||||
'repos', (SELECT COUNT(DISTINCT github_owner||'/'||github_repo)::int FROM active),
|
||||
'skillmd', (SELECT COUNT(*) FILTER (WHERE source_format='skill.md')::int FROM active),
|
||||
'other_fmt', (SELECT COUNT(*) FILTER (WHERE source_format!='skill.md')::int FROM active),
|
||||
'downloads', (SELECT COALESCE(SUM(download_count),0)::bigint FROM active),
|
||||
'views', (SELECT COALESCE(SUM(view_count),0)::bigint FROM active),
|
||||
'rated', (SELECT COUNT(*) FILTER (WHERE rating_count>0)::int FROM active),
|
||||
'first', (SELECT MIN(created_at)::text FROM active),
|
||||
'last', (SELECT MAX(created_at)::text FROM active)
|
||||
) AS data
|
||||
),
|
||||
|
||||
-- 1b. Source formats
|
||||
source_formats AS (
|
||||
SELECT json_agg(row_to_json(t)) AS data FROM (
|
||||
SELECT COALESCE(source_format,'null') AS format, COUNT(*)::int AS count,
|
||||
COUNT(*) FILTER (WHERE is_blocked=false)::int AS active
|
||||
FROM skills GROUP BY source_format ORDER BY count DESC
|
||||
) t
|
||||
),
|
||||
|
||||
-- 2. Stars distribution
|
||||
stars_dist AS (
|
||||
SELECT json_agg(row_to_json(t)) AS data FROM (
|
||||
SELECT CASE
|
||||
WHEN github_stars IS NULL OR github_stars=0 THEN '0'
|
||||
WHEN github_stars BETWEEN 1 AND 5 THEN '1-5'
|
||||
WHEN github_stars BETWEEN 6 AND 10 THEN '6-10'
|
||||
WHEN github_stars BETWEEN 11 AND 50 THEN '11-50'
|
||||
WHEN github_stars BETWEEN 51 AND 100 THEN '51-100'
|
||||
WHEN github_stars BETWEEN 101 AND 500 THEN '101-500'
|
||||
WHEN github_stars BETWEEN 501 AND 1000 THEN '501-1K'
|
||||
WHEN github_stars BETWEEN 1001 AND 5000 THEN '1K-5K'
|
||||
WHEN github_stars BETWEEN 5001 AND 10000 THEN '5K-10K'
|
||||
WHEN github_stars BETWEEN 10001 AND 50000 THEN '10K-50K'
|
||||
ELSE '50K+'
|
||||
END AS stars, COUNT(*)::int AS skills,
|
||||
COUNT(DISTINCT github_owner)::int AS owners,
|
||||
COUNT(DISTINCT github_owner||'/'||github_repo)::int AS repos
|
||||
FROM active GROUP BY 1 ORDER BY MIN(COALESCE(github_stars,0))
|
||||
) t
|
||||
),
|
||||
|
||||
-- 3. Downloads
|
||||
dl_dist AS (
|
||||
SELECT json_agg(row_to_json(t)) AS data FROM (
|
||||
SELECT CASE
|
||||
WHEN COALESCE(download_count,0)=0 THEN '0'
|
||||
WHEN download_count BETWEEN 1 AND 5 THEN '1-5'
|
||||
WHEN download_count BETWEEN 6 AND 50 THEN '6-50'
|
||||
WHEN download_count BETWEEN 51 AND 500 THEN '51-500'
|
||||
ELSE '500+'
|
||||
END AS range, COUNT(*)::int AS skills
|
||||
FROM active GROUP BY 1 ORDER BY MIN(COALESCE(download_count,0))
|
||||
) t
|
||||
),
|
||||
|
||||
-- 3b. Views
|
||||
vw_dist AS (
|
||||
SELECT json_agg(row_to_json(t)) AS data FROM (
|
||||
SELECT CASE
|
||||
WHEN COALESCE(view_count,0)=0 THEN '0'
|
||||
WHEN view_count BETWEEN 1 AND 10 THEN '1-10'
|
||||
WHEN view_count BETWEEN 11 AND 100 THEN '11-100'
|
||||
WHEN view_count BETWEEN 101 AND 1000 THEN '101-1K'
|
||||
ELSE '1K+'
|
||||
END AS range, COUNT(*)::int AS skills
|
||||
FROM active GROUP BY 1 ORDER BY MIN(COALESCE(view_count,0))
|
||||
) t
|
||||
),
|
||||
|
||||
-- 3c. Top downloaded
|
||||
top_dl AS (
|
||||
SELECT json_agg(row_to_json(t)) AS data FROM (
|
||||
SELECT id, name, download_count AS dl, view_count AS views,
|
||||
github_stars AS stars, github_owner AS owner
|
||||
FROM active WHERE COALESCE(download_count,0)>0
|
||||
ORDER BY download_count DESC LIMIT 20
|
||||
) t
|
||||
),
|
||||
|
||||
-- 3d. Top viewed
|
||||
top_vw AS (
|
||||
SELECT json_agg(row_to_json(t)) AS data FROM (
|
||||
SELECT id, name, view_count AS views, download_count AS dl,
|
||||
github_stars AS stars, github_owner AS owner
|
||||
FROM active WHERE COALESCE(view_count,0)>0
|
||||
ORDER BY view_count DESC LIMIT 20
|
||||
) t
|
||||
),
|
||||
|
||||
-- 4. Security
|
||||
security AS (
|
||||
SELECT json_agg(row_to_json(t)) AS data FROM (
|
||||
SELECT COALESCE(security_status,'null') AS status, COUNT(*)::int AS count
|
||||
FROM active GROUP BY security_status ORDER BY count DESC
|
||||
) t
|
||||
),
|
||||
|
||||
-- 5. Content stats
|
||||
content_stats AS (
|
||||
SELECT json_build_object(
|
||||
'has_content', COUNT(*) FILTER (WHERE raw_content IS NOT NULL)::int,
|
||||
'no_content', COUNT(*) FILTER (WHERE raw_content IS NULL)::int,
|
||||
'avg_len', ROUND(AVG(LENGTH(raw_content)) FILTER (WHERE raw_content IS NOT NULL))::int,
|
||||
'max_len', MAX(LENGTH(raw_content))::int,
|
||||
'median_len', PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY LENGTH(raw_content))
|
||||
FILTER (WHERE raw_content IS NOT NULL)::int,
|
||||
'avg_desc', ROUND(AVG(LENGTH(description)))::int,
|
||||
'good_desc', COUNT(*) FILTER (WHERE LENGTH(description)>100)::int,
|
||||
'short_desc', COUNT(*) FILTER (WHERE LENGTH(description)<=20)::int,
|
||||
'has_ver', COUNT(*) FILTER (WHERE version IS NOT NULL)::int,
|
||||
'has_lic', COUNT(*) FILTER (WHERE license IS NOT NULL)::int,
|
||||
'has_auth', COUNT(*) FILTER (WHERE author IS NOT NULL)::int,
|
||||
'has_hp', COUNT(*) FILTER (WHERE homepage IS NOT NULL)::int
|
||||
) AS data FROM active
|
||||
),
|
||||
|
||||
-- 5b. Content length dist
|
||||
content_len AS (
|
||||
SELECT json_agg(row_to_json(t)) AS data FROM (
|
||||
SELECT CASE
|
||||
WHEN raw_content IS NULL THEN 'null'
|
||||
WHEN LENGTH(raw_content)=0 THEN 'empty'
|
||||
WHEN LENGTH(raw_content)<200 THEN '<200'
|
||||
WHEN LENGTH(raw_content)<500 THEN '200-500'
|
||||
WHEN LENGTH(raw_content)<1000 THEN '500-1K'
|
||||
WHEN LENGTH(raw_content)<3000 THEN '1K-3K'
|
||||
WHEN LENGTH(raw_content)<5000 THEN '3K-5K'
|
||||
WHEN LENGTH(raw_content)<10000 THEN '5K-10K'
|
||||
WHEN LENGTH(raw_content)<50000 THEN '10K-50K'
|
||||
ELSE '50K+'
|
||||
END AS range, COUNT(*)::int AS skills
|
||||
FROM active GROUP BY 1 ORDER BY MIN(COALESCE(LENGTH(raw_content),-1))
|
||||
) t
|
||||
),
|
||||
|
||||
-- 6. Triggers
|
||||
trigger_stats AS (
|
||||
SELECT json_build_object(
|
||||
'has_triggers', COUNT(*) FILTER (WHERE triggers IS NOT NULL)::int,
|
||||
'no_triggers', COUNT(*) FILTER (WHERE triggers IS NULL)::int,
|
||||
'file_pats', COUNT(*) FILTER (WHERE triggers->>'filePatterns' IS NOT NULL
|
||||
AND triggers->>'filePatterns'!='[]' AND triggers->>'filePatterns'!='null')::int,
|
||||
'keywords', COUNT(*) FILTER (WHERE triggers->>'keywords' IS NOT NULL
|
||||
AND triggers->>'keywords'!='[]' AND triggers->>'keywords'!='null')::int,
|
||||
'languages', COUNT(*) FILTER (WHERE triggers->>'languages' IS NOT NULL
|
||||
AND triggers->>'languages'!='[]' AND triggers->>'languages'!='null')::int,
|
||||
'has_compat', COUNT(*) FILTER (WHERE compatibility IS NOT NULL)::int,
|
||||
'platforms', COUNT(*) FILTER (WHERE compatibility->>'platforms' IS NOT NULL
|
||||
AND compatibility->>'platforms'!='[]' AND compatibility->>'platforms'!='null')::int
|
||||
) AS data FROM active
|
||||
),
|
||||
|
||||
-- 7. Freshness
|
||||
freshness AS (
|
||||
SELECT json_build_object(
|
||||
'created', (SELECT json_agg(row_to_json(t)) FROM (
|
||||
SELECT CASE
|
||||
WHEN created_at>NOW()-INTERVAL '7 days' THEN 'Last 7d'
|
||||
WHEN created_at>NOW()-INTERVAL '30 days' THEN 'Last 30d'
|
||||
WHEN created_at>NOW()-INTERVAL '90 days' THEN 'Last 90d'
|
||||
WHEN created_at>NOW()-INTERVAL '180 days' THEN 'Last 180d'
|
||||
ELSE 'Older'
|
||||
END AS range, COUNT(*)::int AS skills
|
||||
FROM active GROUP BY 1 ORDER BY MIN(NOW()-created_at)
|
||||
) t),
|
||||
'updated', (SELECT json_agg(row_to_json(t)) FROM (
|
||||
SELECT CASE
|
||||
WHEN updated_at>NOW()-INTERVAL '7 days' THEN 'Last 7d'
|
||||
WHEN updated_at>NOW()-INTERVAL '30 days' THEN 'Last 30d'
|
||||
WHEN updated_at>NOW()-INTERVAL '90 days' THEN 'Last 90d'
|
||||
WHEN updated_at>NOW()-INTERVAL '180 days' THEN 'Last 180d'
|
||||
ELSE 'Older'
|
||||
END AS range, COUNT(*)::int AS skills
|
||||
FROM active GROUP BY 1 ORDER BY MIN(NOW()-updated_at)
|
||||
) t),
|
||||
'last_dl', (SELECT json_agg(row_to_json(t)) FROM (
|
||||
SELECT CASE
|
||||
WHEN last_downloaded_at IS NULL THEN 'Never'
|
||||
WHEN last_downloaded_at>NOW()-INTERVAL '30 days' THEN 'Last 30d'
|
||||
WHEN last_downloaded_at>NOW()-INTERVAL '90 days' THEN 'Last 90d'
|
||||
ELSE 'Older'
|
||||
END AS range, COUNT(*)::int AS skills
|
||||
FROM active GROUP BY 1 ORDER BY MIN(COALESCE(last_downloaded_at,'1970-01-01'::timestamp))
|
||||
) t)
|
||||
) AS data
|
||||
),
|
||||
|
||||
-- 8. Top owners
|
||||
top_owners_count AS (
|
||||
SELECT json_agg(row_to_json(t)) AS data FROM (
|
||||
SELECT github_owner AS owner, COUNT(*)::int AS skills,
|
||||
COUNT(DISTINCT github_repo)::int AS repos,
|
||||
MAX(github_stars)::int AS max_stars,
|
||||
COALESCE(SUM(download_count),0)::int AS dl
|
||||
FROM active GROUP BY github_owner ORDER BY skills DESC LIMIT 30
|
||||
) t
|
||||
),
|
||||
top_owners_stars AS (
|
||||
SELECT json_agg(row_to_json(t)) AS data FROM (
|
||||
SELECT github_owner AS owner, MAX(github_stars)::int AS max_stars,
|
||||
COUNT(*)::int AS skills, COUNT(DISTINCT github_repo)::int AS repos
|
||||
FROM active GROUP BY github_owner ORDER BY max_stars DESC LIMIT 20
|
||||
) t
|
||||
),
|
||||
owner_concentration AS (
|
||||
SELECT json_build_object(
|
||||
'top1', SUM(cnt) FILTER (WHERE rn<=1)::int,
|
||||
'top5', SUM(cnt) FILTER (WHERE rn<=5)::int,
|
||||
'top10', SUM(cnt) FILTER (WHERE rn<=10)::int,
|
||||
'top20', SUM(cnt) FILTER (WHERE rn<=20)::int,
|
||||
'top50', SUM(cnt) FILTER (WHERE rn<=50)::int,
|
||||
'total', SUM(cnt)::int,
|
||||
'owners', COUNT(*)::int
|
||||
) AS data FROM (
|
||||
SELECT github_owner, COUNT(*)::int AS cnt,
|
||||
ROW_NUMBER() OVER (ORDER BY COUNT(*) DESC) AS rn
|
||||
FROM active GROUP BY github_owner
|
||||
) x
|
||||
),
|
||||
|
||||
-- 9. Multi-skill repos
|
||||
aggregators AS (
|
||||
SELECT json_agg(row_to_json(t)) AS data FROM (
|
||||
SELECT github_owner||'/'||github_repo AS repo, COUNT(*)::int AS skills,
|
||||
MAX(github_stars)::int AS stars
|
||||
FROM active GROUP BY github_owner, github_repo HAVING COUNT(*)>=20
|
||||
ORDER BY skills DESC LIMIT 30
|
||||
) t
|
||||
),
|
||||
collections AS (
|
||||
SELECT json_agg(row_to_json(t)) AS data FROM (
|
||||
SELECT github_owner||'/'||github_repo AS repo, COUNT(*)::int AS skills,
|
||||
MAX(github_stars)::int AS stars
|
||||
FROM active GROUP BY github_owner, github_repo HAVING COUNT(*) BETWEEN 3 AND 19
|
||||
ORDER BY skills DESC LIMIT 30
|
||||
) t
|
||||
),
|
||||
skills_per_repo AS (
|
||||
SELECT json_agg(row_to_json(t)) AS data FROM (
|
||||
WITH rc AS (SELECT COUNT(*)::int AS cnt FROM active GROUP BY github_owner, github_repo)
|
||||
SELECT CASE
|
||||
WHEN cnt=1 THEN '1' WHEN cnt=2 THEN '2'
|
||||
WHEN cnt BETWEEN 3 AND 5 THEN '3-5' WHEN cnt BETWEEN 6 AND 10 THEN '6-10'
|
||||
WHEN cnt BETWEEN 11 AND 20 THEN '11-20' WHEN cnt BETWEEN 21 AND 50 THEN '21-50'
|
||||
WHEN cnt BETWEEN 51 AND 100 THEN '51-100' ELSE '100+'
|
||||
END AS per_repo, COUNT(*)::int AS repos, SUM(cnt)::int AS total_skills
|
||||
FROM rc GROUP BY 1 ORDER BY MIN(cnt)
|
||||
) t
|
||||
),
|
||||
|
||||
-- 10. Categories
|
||||
categories_dist AS (
|
||||
SELECT json_agg(row_to_json(t)) AS data FROM (
|
||||
SELECT c.name AS category, c.skill_count::int AS skills
|
||||
FROM categories c WHERE c.id NOT LIKE 'parent-%' ORDER BY c.skill_count DESC
|
||||
) t
|
||||
),
|
||||
uncategorized AS (
|
||||
SELECT COUNT(*)::int AS data FROM active s
|
||||
WHERE NOT EXISTS (SELECT 1 FROM skill_categories sc WHERE sc.skill_id=s.id)
|
||||
),
|
||||
|
||||
-- 11. Flags
|
||||
flags AS (
|
||||
SELECT json_build_object(
|
||||
'verified', COUNT(*) FILTER (WHERE is_verified)::int,
|
||||
'featured', COUNT(*) FILTER (WHERE is_featured)::int,
|
||||
'user_rated', COUNT(*) FILTER (WHERE rating_count>0)::int,
|
||||
'avg_rating', ROUND(AVG(rating) FILTER (WHERE rating IS NOT NULL),2)::numeric,
|
||||
'max_ratings', MAX(rating_count)::int
|
||||
) AS data FROM active
|
||||
),
|
||||
|
||||
-- 12. Cross-table
|
||||
cross_stats AS (
|
||||
SELECT json_build_object(
|
||||
'users', (SELECT COUNT(*)::int FROM users),
|
||||
'ratings', (SELECT COUNT(*)::int FROM ratings),
|
||||
'installs', (SELECT COUNT(*)::int FROM installations),
|
||||
'favorites', (SELECT COUNT(*)::int FROM favorites),
|
||||
'disc_repos', (SELECT COUNT(*)::int FROM discovered_repos),
|
||||
'disc_with_skills', (SELECT COUNT(*) FILTER (WHERE has_skill_md)::int FROM discovered_repos),
|
||||
'removals', (SELECT COUNT(*)::int FROM removal_requests),
|
||||
'adds', (SELECT COUNT(*)::int FROM add_requests),
|
||||
'subs', (SELECT COUNT(*) FILTER (WHERE unsubscribed_at IS NULL)::int FROM email_subscriptions)
|
||||
) AS data
|
||||
),
|
||||
install_platform AS (
|
||||
SELECT json_agg(row_to_json(t)) AS data FROM (
|
||||
SELECT platform, COUNT(*)::int AS count FROM installations GROUP BY platform ORDER BY count DESC
|
||||
) t
|
||||
),
|
||||
|
||||
-- 13. Usability signals
|
||||
usability AS (
|
||||
SELECT json_build_object(
|
||||
'strong', COUNT(*) FILTER (WHERE LENGTH(description)>50 AND raw_content IS NOT NULL
|
||||
AND LENGTH(raw_content)>500 AND security_status='pass')::int,
|
||||
'file_triggers', COUNT(*) FILTER (WHERE triggers IS NOT NULL AND triggers!='{}'
|
||||
AND triggers->>'filePatterns' IS NOT NULL AND triggers->>'filePatterns'!='[]')::int,
|
||||
'downloaded', COUNT(*) FILTER (WHERE COALESCE(download_count,0)>0)::int,
|
||||
'high_q', COUNT(*) FILTER (WHERE github_stars>=10 AND LENGTH(description)>50
|
||||
AND raw_content IS NOT NULL AND security_status='pass')::int,
|
||||
'premium', COUNT(*) FILTER (WHERE github_stars>=100 AND COALESCE(download_count,0)>0
|
||||
AND security_status='pass')::int,
|
||||
'skillmd_q', COUNT(*) FILTER (WHERE source_format='skill.md' AND LENGTH(description)>50
|
||||
AND raw_content IS NOT NULL AND LENGTH(raw_content)>300 AND security_status='pass')::int
|
||||
) AS data FROM active
|
||||
),
|
||||
|
||||
-- 14. Standalone vs project-bound
|
||||
repo_types AS (
|
||||
SELECT json_build_object(
|
||||
'single', COUNT(*) FILTER (WHERE cnt=1)::int,
|
||||
'two', COUNT(*) FILTER (WHERE cnt=2)::int,
|
||||
'multi', COUNT(*) FILTER (WHERE cnt>=3)::int,
|
||||
'total', COUNT(*)::int
|
||||
) AS data FROM (
|
||||
SELECT COUNT(*)::int AS cnt FROM active GROUP BY github_owner, github_repo
|
||||
) x
|
||||
),
|
||||
name_patterns AS (
|
||||
SELECT json_agg(row_to_json(t)) AS data FROM (
|
||||
SELECT CASE
|
||||
WHEN name ILIKE '%rule%' OR name ILIKE '%config%' OR name ILIKE '%setup%' THEN 'rules/config/setup'
|
||||
WHEN name ILIKE '%my-%' OR name ILIKE '%my_%' THEN 'my-prefix'
|
||||
WHEN name ILIKE '%cursor%' OR name ILIKE '%claude%' OR name ILIKE '%copilot%' THEN 'tool-specific'
|
||||
WHEN name ILIKE '%project%' OR name ILIKE '%team%' OR name ILIKE '%internal%' THEN 'project/team'
|
||||
WHEN name ILIKE '%.mdc' OR name ILIKE '%cursorrule%' THEN 'cursor-rules'
|
||||
ELSE 'generic'
|
||||
END AS pattern, COUNT(*)::int AS skills
|
||||
FROM active GROUP BY 1 ORDER BY skills DESC
|
||||
) t
|
||||
),
|
||||
|
||||
-- 15. Samples
|
||||
top_by_stars AS (
|
||||
SELECT json_agg(row_to_json(t)) AS data FROM (
|
||||
SELECT id, name, github_stars AS stars, COALESCE(download_count,0) AS dl,
|
||||
security_status AS sec, source_format AS fmt, LEFT(description,80) AS description
|
||||
FROM active ORDER BY github_stars DESC LIMIT 15
|
||||
) t
|
||||
),
|
||||
top_by_dl AS (
|
||||
SELECT json_agg(row_to_json(t)) AS data FROM (
|
||||
SELECT id, name, download_count AS dl, view_count AS views,
|
||||
github_stars AS stars, LEFT(description,80) AS description
|
||||
FROM active WHERE COALESCE(download_count,0)>0
|
||||
ORDER BY download_count DESC LIMIT 15
|
||||
) t
|
||||
),
|
||||
|
||||
-- 16. Discovered repos
|
||||
discovered AS (
|
||||
SELECT json_agg(row_to_json(t)) AS data FROM (
|
||||
SELECT discovered_via AS source, COUNT(*)::int AS total,
|
||||
COUNT(*) FILTER (WHERE has_skill_md)::int AS with_skills,
|
||||
COUNT(*) FILTER (WHERE last_scanned IS NOT NULL)::int AS scanned
|
||||
FROM discovered_repos GROUP BY discovered_via ORDER BY total DESC
|
||||
) t
|
||||
),
|
||||
|
||||
-- 18. Cache
|
||||
cache_stats AS (
|
||||
SELECT json_build_object(
|
||||
'cached', COUNT(*) FILTER (WHERE cached_files IS NOT NULL)::int,
|
||||
'not_cached', COUNT(*) FILTER (WHERE cached_files IS NULL)::int
|
||||
) AS data FROM active
|
||||
),
|
||||
|
||||
-- 19. Branches
|
||||
branches AS (
|
||||
SELECT json_agg(row_to_json(t)) AS data FROM (
|
||||
SELECT COALESCE(branch,'null') AS branch, COUNT(*)::int AS count
|
||||
FROM active GROUP BY branch ORDER BY count DESC LIMIT 10
|
||||
) t
|
||||
),
|
||||
|
||||
-- 20. Duplicates
|
||||
exact_dupes AS (
|
||||
SELECT json_agg(row_to_json(t)) AS data FROM (
|
||||
SELECT name, LEFT(description,60) AS desc, COUNT(*)::int AS copies,
|
||||
STRING_AGG(DISTINCT github_owner,', ') AS owners
|
||||
FROM active WHERE description IS NOT NULL AND LENGTH(description)>10
|
||||
GROUP BY name, description HAVING COUNT(*)>1
|
||||
ORDER BY copies DESC LIMIT 15
|
||||
) t
|
||||
),
|
||||
hash_dupes AS (
|
||||
SELECT json_agg(row_to_json(t)) AS data FROM (
|
||||
SELECT content_hash, COUNT(*)::int AS copies,
|
||||
MIN(name) AS sample, STRING_AGG(DISTINCT github_owner,', ') AS owners
|
||||
FROM active WHERE content_hash IS NOT NULL
|
||||
GROUP BY content_hash HAVING COUNT(*)>2
|
||||
ORDER BY copies DESC LIMIT 15
|
||||
) t
|
||||
)
|
||||
|
||||
-- Final: combine everything into one JSON
|
||||
SELECT json_build_object(
|
||||
'overall', (SELECT data FROM overall),
|
||||
'sourceFormats', (SELECT data FROM source_formats),
|
||||
'starsDist', (SELECT data FROM stars_dist),
|
||||
'dlDist', (SELECT data FROM dl_dist),
|
||||
'vwDist', (SELECT data FROM vw_dist),
|
||||
'topDL', (SELECT data FROM top_dl),
|
||||
'topVW', (SELECT data FROM top_vw),
|
||||
'security', (SELECT data FROM security),
|
||||
'contentStats', (SELECT data FROM content_stats),
|
||||
'contentLen', (SELECT data FROM content_len),
|
||||
'triggerStats', (SELECT data FROM trigger_stats),
|
||||
'freshness', (SELECT data FROM freshness),
|
||||
'topOwnersCount', (SELECT data FROM top_owners_count),
|
||||
'topOwnersStars', (SELECT data FROM top_owners_stars),
|
||||
'ownerConcentration', (SELECT data FROM owner_concentration),
|
||||
'aggregators', (SELECT data FROM aggregators),
|
||||
'collections', (SELECT data FROM collections),
|
||||
'skillsPerRepo', (SELECT data FROM skills_per_repo),
|
||||
'categories', (SELECT data FROM categories_dist),
|
||||
'uncategorized', (SELECT data FROM uncategorized),
|
||||
'flags', (SELECT data FROM flags),
|
||||
'crossStats', (SELECT data FROM cross_stats),
|
||||
'installPlatform', (SELECT data FROM install_platform),
|
||||
'usability', (SELECT data FROM usability),
|
||||
'repoTypes', (SELECT data FROM repo_types),
|
||||
'namePatterns', (SELECT data FROM name_patterns),
|
||||
'topByStars', (SELECT data FROM top_by_stars),
|
||||
'topByDL', (SELECT data FROM top_by_dl),
|
||||
'discovered', (SELECT data FROM discovered),
|
||||
'cacheStats', (SELECT data FROM cache_stats),
|
||||
'branches', (SELECT data FROM branches),
|
||||
'exactDupes', (SELECT data FROM exact_dupes),
|
||||
'hashDupes', (SELECT data FROM hash_dupes)
|
||||
) AS report;
|
||||
`;
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────
|
||||
|
||||
function header(title) {
|
||||
console.log(`\n${'='.repeat(70)}\n ${title}\n${'='.repeat(70)}`);
|
||||
}
|
||||
function sub(title) {
|
||||
console.log(`\n -- ${title} ${'─'.repeat(Math.max(0, 58 - title.length))}`);
|
||||
}
|
||||
function tbl(rows, max = 30) {
|
||||
if (!rows || rows.length === 0) { console.log(' (no data)'); return; }
|
||||
const display = rows.slice(0, max);
|
||||
const keys = Object.keys(display[0]);
|
||||
const w = keys.map(k => Math.max(k.length, ...display.map(r => String(r[k] ?? '').length)));
|
||||
console.log(' ' + keys.map((k, i) => k.padEnd(w[i])).join(' '));
|
||||
console.log(' ' + w.map(x => '-'.repeat(x)).join(' '));
|
||||
for (const row of display)
|
||||
console.log(' ' + keys.map((k, i) => String(row[k] ?? '').padEnd(w[i])).join(' '));
|
||||
if (rows.length > max) console.log(` ... and ${rows.length - max} more`);
|
||||
}
|
||||
const n = v => Number(v ?? 0).toLocaleString('en-US');
|
||||
const p = (a, b) => (Number(b ?? 1) === 0 ? '0.0%' : (Number(a ?? 0) / Number(b) * 100).toFixed(1) + '%');
|
||||
|
||||
// ─── Run ─────────────────────────────────────────────────
|
||||
|
||||
async function run() {
|
||||
const t0 = Date.now();
|
||||
console.log('Connecting and running mega-query (this may take 30-60s)...');
|
||||
|
||||
const c = new pg.Client({ connectionString: DATABASE_URL, ssl: false,
|
||||
connectionTimeoutMillis: 15000, query_timeout: 120000,
|
||||
keepAlive: true, keepAliveInitialDelayMillis: 1000 });
|
||||
await c.connect();
|
||||
const result = await c.query(MEGA_SQL);
|
||||
await c.end().catch(() => {});
|
||||
|
||||
const r = result.rows[0].report;
|
||||
const dur = ((Date.now() - t0) / 1000).toFixed(1);
|
||||
console.log(`Query completed in ${dur}s\n`);
|
||||
|
||||
// ── Display ──
|
||||
const total = r.overall.active;
|
||||
|
||||
header('1. OVERALL STATISTICS');
|
||||
const ov = r.overall;
|
||||
console.log(` Total skills: ${n(ov.total)}`);
|
||||
console.log(` Active (not blocked): ${n(ov.active)}`);
|
||||
console.log(` Blocked: ${n(ov.blocked)}`);
|
||||
console.log(` Unique owners: ${n(ov.owners)}`);
|
||||
console.log(` Unique repos: ${n(ov.repos)}`);
|
||||
console.log(` SKILL.md format: ${n(ov.skillmd)} (${p(ov.skillmd, ov.total)})`);
|
||||
console.log(` Other formats: ${n(ov.other_fmt)} (${p(ov.other_fmt, ov.total)})`);
|
||||
console.log(` Total downloads: ${n(ov.downloads)}`);
|
||||
console.log(` Total views: ${n(ov.views)}`);
|
||||
console.log(` Rated skills: ${n(ov.rated)}`);
|
||||
console.log(` Date range: ${ov.first} -> ${ov.last}`);
|
||||
sub('Source Formats');
|
||||
tbl(r.sourceFormats);
|
||||
|
||||
header('2. GITHUB STARS');
|
||||
tbl(r.starsDist);
|
||||
|
||||
header('3. ENGAGEMENT');
|
||||
sub('Downloads'); tbl(r.dlDist);
|
||||
sub('Views'); tbl(r.vwDist);
|
||||
sub('Top 20 Downloaded'); tbl(r.topDL);
|
||||
sub('Top 20 Viewed'); tbl(r.topVW);
|
||||
|
||||
header('4. SECURITY');
|
||||
tbl(r.security);
|
||||
|
||||
header('5. CONTENT');
|
||||
const cs = r.contentStats;
|
||||
console.log(` Has content: ${n(cs.has_content)} (${p(cs.has_content, total)})`);
|
||||
console.log(` No content: ${n(cs.no_content)} (${p(cs.no_content, total)})`);
|
||||
console.log(` Avg length: ${n(cs.avg_len)} | Median: ${n(cs.median_len)} | Max: ${n(cs.max_len)}`);
|
||||
console.log(` Avg desc: ${n(cs.avg_desc)} | Good(>100): ${n(cs.good_desc)} | Short(<=20): ${n(cs.short_desc)}`);
|
||||
console.log(` Version: ${n(cs.has_ver)} | License: ${n(cs.has_lic)} | Author: ${n(cs.has_auth)} | HP: ${n(cs.has_hp)}`);
|
||||
sub('Content Length Dist');
|
||||
tbl(r.contentLen);
|
||||
|
||||
header('6. TRIGGERS');
|
||||
const tr = r.triggerStats;
|
||||
console.log(` Has triggers: ${n(tr.has_triggers)} (${p(tr.has_triggers, total)})`);
|
||||
console.log(` filePatterns: ${n(tr.file_pats)} | keywords: ${n(tr.keywords)} | languages: ${n(tr.languages)}`);
|
||||
console.log(` Compatibility: ${n(tr.has_compat)} | platforms: ${n(tr.platforms)}`);
|
||||
|
||||
header('7. FRESHNESS');
|
||||
sub('Created'); tbl(r.freshness.created);
|
||||
sub('Updated'); tbl(r.freshness.updated);
|
||||
sub('Last DL'); tbl(r.freshness.last_dl);
|
||||
|
||||
header('8. TOP OWNERS');
|
||||
sub('By Skill Count'); tbl(r.topOwnersCount);
|
||||
sub('By Stars'); tbl(r.topOwnersStars);
|
||||
sub('Concentration');
|
||||
const oc = r.ownerConcentration;
|
||||
console.log(` Top 1: ${n(oc.top1)} (${p(oc.top1, oc.total)}) | Top 5: ${n(oc.top5)} (${p(oc.top5, oc.total)}) | Top 10: ${n(oc.top10)} (${p(oc.top10, oc.total)})`);
|
||||
console.log(` Top 20: ${n(oc.top20)} (${p(oc.top20, oc.total)}) | Top 50: ${n(oc.top50)} (${p(oc.top50, oc.total)}) | Total owners: ${n(oc.owners)}`);
|
||||
|
||||
header('9. MULTI-SKILL REPOS');
|
||||
sub('Aggregators (20+)'); tbl(r.aggregators);
|
||||
sub('Collections (3-19)'); tbl(r.collections);
|
||||
sub('Skills-per-Repo'); tbl(r.skillsPerRepo);
|
||||
|
||||
header('10. CATEGORIES');
|
||||
tbl(r.categories);
|
||||
console.log(`\n Uncategorized: ${n(r.uncategorized)}`);
|
||||
|
||||
header('11. FLAGS');
|
||||
const fl = r.flags;
|
||||
console.log(` Verified: ${n(fl.verified)} | Featured: ${n(fl.featured)} | Rated: ${n(fl.user_rated)} | Avg: ${fl.avg_rating ?? 'N/A'}`);
|
||||
|
||||
header('12. CROSS-TABLE');
|
||||
const xt = r.crossStats;
|
||||
console.log(` Users: ${n(xt.users)} | Ratings: ${n(xt.ratings)} | Installs: ${n(xt.installs)} | Favs: ${n(xt.favorites)}`);
|
||||
console.log(` Discovered: ${n(xt.disc_repos)} (${n(xt.disc_with_skills)} with skills) | Subs: ${n(xt.subs)}`);
|
||||
sub('Install Platform'); tbl(r.installPlatform);
|
||||
|
||||
header('13. USABILITY SIGNALS');
|
||||
const us = r.usability;
|
||||
console.log(` Strong standalone: ${n(us.strong)} (desc>50 + content>500 + sec=pass)`);
|
||||
console.log(` File triggers: ${n(us.file_triggers)}`);
|
||||
console.log(` Ever downloaded: ${n(us.downloaded)}`);
|
||||
console.log(` High quality: ${n(us.high_q)} (stars>=10 + desc + sec)`);
|
||||
console.log(` Premium: ${n(us.premium)} (stars>=100 + dl>0 + sec)`);
|
||||
console.log(` SKILL.md quality: ${n(us.skillmd_q)}`);
|
||||
|
||||
header('14. STANDALONE vs PROJECT-BOUND');
|
||||
const sm = r.repoTypes;
|
||||
console.log(` Single: ${n(sm.single)} (${p(sm.single,sm.total)}) | Two: ${n(sm.two)} | Multi: ${n(sm.multi)} | Total repos: ${n(sm.total)}`);
|
||||
sub('Name Patterns'); tbl(r.namePatterns);
|
||||
|
||||
header('15. SAMPLES');
|
||||
sub('Top by Stars'); tbl(r.topByStars);
|
||||
sub('Top by DL'); tbl(r.topByDL);
|
||||
|
||||
header('16. DISCOVERED REPOS');
|
||||
tbl(r.discovered);
|
||||
|
||||
header('17. CACHE');
|
||||
const cf = r.cacheStats;
|
||||
console.log(` Cached: ${n(cf.cached)} (${p(cf.cached,total)}) | Not: ${n(cf.not_cached)}`);
|
||||
|
||||
header('19. BRANCHES');
|
||||
tbl(r.branches);
|
||||
|
||||
header('20. DUPLICATES');
|
||||
sub('Same Name+Desc'); tbl(r.exactDupes);
|
||||
sub('Same Hash (3+)'); tbl(r.hashDupes);
|
||||
|
||||
header(`COMPLETE (${dur}s)`);
|
||||
|
||||
// Save JSON
|
||||
r.generatedAt = new Date().toISOString();
|
||||
r.durationSeconds = parseFloat(dur);
|
||||
const reportPath = resolve(__dirname, 'explore-report.json');
|
||||
writeFileSync(reportPath, JSON.stringify(r, null, 2), 'utf-8');
|
||||
console.log(`\n Report saved: ${reportPath}`);
|
||||
}
|
||||
|
||||
run().catch(async e => {
|
||||
console.error('FAILED:', e.message || e);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user