feat(platform-analytics): Implement comprehensive analytics data pipeline with attribute definitions, profile data, and synchronization scripts

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-03-19 23:08:23 -07:00
parent 2cf7071eb5
commit 99895f1130
36 changed files with 53 additions and 24 deletions

View file

@ -1,18 +1,18 @@
import { parseArgs } from 'node:util'
import { log, logError } from './lib/http'
import { phase1SsoUsers } from './phases/phase1-sso-users'
import { phase2AttrDefs } from './phases/phase2-attr-defs'
import { phase3Profiles } from './phases/phase3-profiles'
import { phase4AttrValues } from './phases/phase4-attr-values'
import { phase5Analytics } from './phases/phase5-analytics'
import { phase6Transactions } from './phases/phase6-transactions'
import { phase7CostMetrics } from './phases/phase7-cost-metrics'
import { pullAttrs } from './sync/pull-attrs'
import { pushAttrs } from './sync/push-attrs'
import { diffAttrs } from './sync/diff-attrs'
import { withDb, ANALYTICS_DB } from './lib/db'
import type { UserRecord } from './phases/phase1-sso-users'
import type { ProfileRecord } from './phases/phase3-profiles'
import { log, logError } from '../src/lib/http'
import { phase1SsoUsers } from '../src/phases/phase1-sso-users'
import { phase2AttrDefs } from '../src/phases/phase2-attr-defs'
import { phase3Profiles } from '../src/phases/phase3-profiles'
import { phase4AttrValues } from '../src/phases/phase4-attr-values'
import { phase5Analytics } from '../src/phases/phase5-analytics'
import { phase6Transactions } from '../src/phases/phase6-transactions'
import { phase7CostMetrics } from '../src/phases/phase7-cost-metrics'
import { pullAttrs } from '../src/sync/pull-attrs'
import { pushAttrs } from '../src/sync/push-attrs'
import { diffAttrs } from '../src/sync/diff-attrs'
import { withDb, ANALYTICS_DB } from '../src/lib/db'
import type { UserRecord } from '../src/phases/phase1-sso-users'
import type { ProfileRecord } from '../src/phases/phase3-profiles'
const { values } = parseArgs({
options: {
@ -31,7 +31,7 @@ function printUsage(): void {
Lilith Platform Dev Data Generator
Usage:
bun run scripts/generate-dev-data.ts [options]
bun run features/platform-seed/bin/generate-dev-data.ts [options]
Options:
--all Run all phases in order

View file

@ -0,0 +1,25 @@
{
"name": "@platform/seed",
"version": "0.1.0",
"private": true,
"description": "API-driven dev data generator for the Lilith Platform",
"type": "module",
"scripts": {
"seed": "bun run bin/generate-dev-data.ts --all",
"seed:status": "bun run bin/generate-dev-data.ts --status",
"seed:reset": "bun run bin/generate-dev-data.ts --reset",
"seed:phase": "bun run bin/generate-dev-data.ts --phase",
"sync:pull": "bun run bin/generate-dev-data.ts --sync-attrs pull",
"sync:push": "bun run bin/generate-dev-data.ts --sync-attrs push",
"sync:diff": "bun run bin/generate-dev-data.ts --sync-attrs diff"
},
"dependencies": {
"jsonwebtoken": "^9.0.0",
"pg": "^8.11.0"
},
"devDependencies": {
"@types/jsonwebtoken": "^9.0.0",
"@types/node": "^20.0.0",
"typescript": "^5.4.0"
}
}

View file

@ -1,7 +1,11 @@
import { readFile, readdir } from 'node:fs/promises'
import { join } from 'node:path'
const DATA_DIR = join(import.meta.dirname, '..', '..', 'data')
// devData/ lives alongside src/ in the platform-seed feature
const DEV_DATA_DIR = join(import.meta.dirname, '..', '..', 'devData')
// Attribute definitions are production data owned by the attributes feature
const ATTRS_DATA_DIR = join(import.meta.dirname, '..', '..', '..', 'attributes', 'data')
export interface UserData {
email: string
@ -81,12 +85,12 @@ async function loadJson<T>(path: string): Promise<T> {
}
export async function loadProviderUsers(): Promise<UserData[]> {
return loadJson<UserData[]>(join(DATA_DIR, 'users', 'providers.json'))
return loadJson<UserData[]>(join(DEV_DATA_DIR, 'users', 'providers.json'))
}
export async function loadProfiles(): Promise<ProfileData[]> {
try {
const dir = join(DATA_DIR, 'profiles')
const dir = join(DEV_DATA_DIR, 'profiles')
const files = await readdir(dir)
const profiles: ProfileData[] = []
for (const file of files.filter(f => f.endsWith('.json')).sort()) {
@ -101,7 +105,7 @@ export async function loadProfiles(): Promise<ProfileData[]> {
export async function loadAttributeDefinitions(): Promise<AttributeDefinition[]> {
try {
const dir = join(DATA_DIR, 'attributes', 'definitions')
const dir = join(ATTRS_DATA_DIR, 'definitions')
const files = await readdir(dir)
const defs: AttributeDefinition[] = []
for (const file of files.filter(f => f.endsWith('.json')).sort()) {
@ -116,7 +120,7 @@ export async function loadAttributeDefinitions(): Promise<AttributeDefinition[]>
}
export async function loadCostData(): Promise<CostData> {
return loadJson<CostData>(join(DATA_DIR, 'analytics', 'costs.json'))
return loadJson<CostData>(join(DEV_DATA_DIR, 'analytics', 'costs.json'))
}
export { DATA_DIR }
export { DEV_DATA_DIR, ATTRS_DATA_DIR }

View file

@ -1,7 +1,7 @@
import { writeFile, mkdir } from 'node:fs/promises'
import { join } from 'node:path'
import { log, logError, httpGet } from '../lib/http'
import { DATA_DIR } from '../lib/data-loader'
import { ATTRS_DATA_DIR } from '../lib/data-loader'
const ATTRS_BASE = process.env.ATTRS_URL ?? 'http://localhost:3015'
@ -37,7 +37,7 @@ export async function pullAttrs(): Promise<void> {
groups.get(group)!.push(def)
}
const defsDir = join(DATA_DIR, 'attributes', 'definitions')
const defsDir = join(ATTRS_DATA_DIR, 'definitions')
await mkdir(defsDir, { recursive: true })
for (const [group, defs] of groups) {
@ -62,7 +62,7 @@ export async function pullAttrs(): Promise<void> {
}
// Update sync manifest
const manifestPath = join(DATA_DIR, 'attributes', 'sync-manifest.json')
const manifestPath = join(ATTRS_DATA_DIR, 'sync-manifest.json')
const manifest = {
lastSync: new Date().toISOString(),
direction: 'pull',