#!/usr/bin/env node import { createRequire } from 'node:module'; import { mkdir, writeFile } from 'node:fs/promises'; import { dirname, resolve } from 'node:path'; const FIXTURE_VERSION = 'v2.6-large-data-fixture-1'; const ID_PREFIX = 'perf-'; const BASE_DATE = new Date('2026-07-01T09:00:00.000Z'); const OUTPUT_PATH = '.tmp/perf-fixture.json'; const SIZE_PRESETS = { small: { products: 1, projectsPerProduct: 2, versionsPerProject: 2, requirementsPerVersion: 20, looseRequirementsPerProduct: 20, plansPerVersion: 3, devTasksPerRequirement: 2, testCasesPerRequirement: 2, bugsPerVersion: 12, activitiesPerVersion: 16, users: 8, }, medium: { products: 2, projectsPerProduct: 8, versionsPerProject: 4, requirementsPerVersion: 80, looseRequirementsPerProduct: 300, plansPerVersion: 3, devTasksPerRequirement: 3, testCasesPerRequirement: 3, bugsPerVersion: 80, activitiesPerVersion: 120, users: 40, }, large: { products: 4, projectsPerProduct: 20, versionsPerProject: 6, requirementsPerVersion: 150, looseRequirementsPerProduct: 2000, plansPerVersion: 3, devTasksPerRequirement: 4, testCasesPerRequirement: 4, bugsPerVersion: 180, activitiesPerVersion: 300, users: 120, }, }; const DEV_STATUSES = ['todo', 'in_progress', 'testing', 'submitted']; const TEST_STATUSES = ['pending', 'running', 'passed', 'failed', 'blocked']; const BUG_STATUSES = ['open', 'fixing', 'fixed', 'verifying', 'closed']; const BUG_SEVERITIES = ['normal', 'major', 'critical']; const PLAN_TYPES = ['research', 'product', 'ui']; const REQUIREMENT_STATUSES = ['adopted', 'planned', 'developing', 'testing', 'released']; function parseArgs(argv) { const args = { size: 'small', dryRun: false, json: false, output: OUTPUT_PATH, }; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; if (arg === '--dry-run') { args.dryRun = true; continue; } if (arg === '--json') { args.json = true; continue; } if (arg === '--size') { args.size = argv[++index] ?? args.size; continue; } if (arg.startsWith('--size=')) { args.size = arg.slice('--size='.length); continue; } if (arg === '--output') { args.output = argv[++index] ?? args.output; continue; } if (arg.startsWith('--output=')) { args.output = arg.slice('--output='.length); continue; } if (arg === '--help' || arg === '-h') { printHelp(); process.exit(0); } throw new Error(`Unknown argument: ${arg}`); } if (!SIZE_PRESETS[args.size]) { throw new Error(`Unsupported size "${args.size}". Use one of: ${Object.keys(SIZE_PRESETS).join(', ')}`); } return args; } function printHelp() { console.log(`Usage: node scripts/seed-large-dataset.mjs [--size small|medium|large] [--dry-run] [--json] Creates deterministic V2.6 performance fixture data using perf-* ids. Options: --size Fixture size. Defaults to small. --dry-run Build and summarize rows without writing PostgreSQL. --json Print the summary as JSON. --output Metadata path written after a real seed. Defaults to ${OUTPUT_PATH}. `); } function pad(value, width = 3) { return String(value).padStart(width, '0'); } function hoursAfter(hours) { return new Date(BASE_DATE.getTime() + hours * 60 * 60 * 1000); } function daysAfter(days) { return hoursAfter(days * 24); } function priority(index) { return index % 5; } function buildFixture(sizeName) { const preset = SIZE_PRESETS[sizeName]; const users = buildUsers(preset.users); const products = []; const projects = []; const versions = []; const requirements = []; const versionPlans = []; const devTasks = []; const testCases = []; const bugs = []; const workActivities = []; const xiaobaoRiskSummaries = []; for (let productNo = 1; productNo <= preset.products; productNo += 1) { const productId = `${ID_PREFIX}product-${pad(productNo)}`; products.push({ id: productId, name: `Performance Product ${pad(productNo)}`, description: `Deterministic ${sizeName} fixture product ${productNo}`, createdAt: daysAfter(productNo), updatedAt: daysAfter(productNo), }); for (let looseNo = 1; looseNo <= preset.looseRequirementsPerProduct; looseNo += 1) { requirements.push(buildRequirement({ productId, projectId: null, versionId: null, productNo, projectNo: 0, versionNo: 0, requirementNo: looseNo, loose: true, creatorId: users[looseNo % users.length].id, })); } for (let projectNo = 1; projectNo <= preset.projectsPerProduct; projectNo += 1) { const projectId = `${ID_PREFIX}project-${pad(productNo)}-${pad(projectNo)}`; projects.push({ id: projectId, productId, name: `Performance Project ${pad(productNo)}-${pad(projectNo)}`, description: `Hot-path project ${projectNo}`, createdAt: daysAfter(projectNo), updatedAt: daysAfter(projectNo), }); for (let versionNo = 1; versionNo <= preset.versionsPerProject; versionNo += 1) { const versionId = `${ID_PREFIX}version-${pad(productNo)}-${pad(projectNo)}-${pad(versionNo)}`; const version = buildVersion({ productId, projectId, versionId, productNo, projectNo, versionNo, users, }); versions.push(version); for (let planNo = 1; planNo <= preset.plansPerVersion; planNo += 1) { versionPlans.push(buildVersionPlan({ productId, projectId, versionId, planNo, ownerId: users[(planNo + versionNo) % users.length].id, })); } const versionRequirementIds = []; for (let requirementNo = 1; requirementNo <= preset.requirementsPerVersion; requirementNo += 1) { const req = buildRequirement({ productId, projectId, versionId, productNo, projectNo, versionNo, requirementNo, loose: false, creatorId: users[requirementNo % users.length].id, }); requirements.push(req); versionRequirementIds.push(req.id); for (let taskNo = 1; taskNo <= preset.devTasksPerRequirement; taskNo += 1) { devTasks.push(buildDevTask({ productId, projectId, versionId, requirementId: req.id, requirementProductId: productId, requirementNo, taskNo, assigneeId: users[(requirementNo + taskNo) % users.length].id, })); } for (let caseNo = 1; caseNo <= preset.testCasesPerRequirement; caseNo += 1) { testCases.push(buildTestCase({ productId, projectId, versionId, requirementId: req.id, requirementProductId: productId, requirementNo, caseNo, assigneeId: users[(requirementNo + caseNo + 2) % users.length].id, })); } } for (let bugNo = 1; bugNo <= preset.bugsPerVersion; bugNo += 1) { bugs.push(buildBug({ productId, projectId, versionId, bugNo, assigneeId: users[(bugNo + versionNo) % users.length].id, testCaseId: testCases.find((row) => row.versionId === versionId)?.id, })); } for (let activityNo = 1; activityNo <= preset.activitiesPerVersion; activityNo += 1) { const source = devTasks.find((row) => row.versionId === versionId && row.assigneeId === users[activityNo % users.length].id) ?? devTasks.find((row) => row.versionId === versionId); workActivities.push(buildWorkActivity({ productId, projectId, versionId, activityNo, actor: users[activityNo % users.length], source, })); } xiaobaoRiskSummaries.push(buildXiaobaoSummary({ version, bugCount: preset.bugsPerVersion, unfinishedCount: versionRequirementIds.length, })); } } } return { size: sizeName, anchors: { productId: `${ID_PREFIX}product-001`, projectId: `${ID_PREFIX}project-001-001`, versionId: `${ID_PREFIX}version-001-001-001`, userId: `${ID_PREFIX}user-dev-01`, }, rows: { users, products, projects, versions, requirements, versionPlans, devTasks, testCases, bugs, workActivities, xiaobaoRiskSummaries, }, }; } function buildUsers(count) { return Array.from({ length: count }, (_, index) => { const no = index + 1; return { id: `${ID_PREFIX}user-dev-${pad(no, 2)}`, email: `perf-user-${pad(no, 2)}@example.test`, name: `Perf User ${pad(no, 2)}`, username: `perf_user_${pad(no, 2)}`, departmentId: no % 3 === 0 ? 'dept-2-3' : no % 2 === 0 ? 'dept-2-2' : 'dept-2-1', roleId: no % 3 === 0 ? 'role-test' : 'role-dev', phone: '', password: 'Perf@2026', isSystem: false, createdAt: BASE_DATE, updatedAt: BASE_DATE, }; }); } function buildVersion(input) { const releaseDate = daysAfter(20 + input.versionNo); return { id: input.versionId, productId: input.productId, projectId: input.projectId, name: `V${input.productNo}.${input.projectNo}.${input.versionNo}`, description: 'Performance hot-path version', status: input.versionNo % 4 === 0 ? 'paused' : 'developing', currentStage: input.versionNo % 2 === 0 ? 'testing' : 'development', startDate: daysAfter(input.versionNo), expectedReleaseDate: releaseDate, releaseDate, members: input.users.slice(0, Math.min(6, input.users.length)).map((user, index) => ({ id: user.id, name: user.name, role: index % 3 === 0 ? 'testing' : index % 2 === 0 ? 'backend' : 'frontend', })), progress: [], priority: priority(input.versionNo), links: {}, createdAt: daysAfter(input.versionNo), updatedAt: daysAfter(input.versionNo + 1), }; } function buildRequirement(input) { const scoped = input.loose ? `LOOSE-${pad(input.productNo)}-${pad(input.requirementNo, 5)}` : `${pad(input.productNo)}-${pad(input.projectNo)}-${pad(input.versionNo)}-${pad(input.requirementNo, 5)}`; return { id: `${ID_PREFIX}req-${scoped}`, productId: input.productId, projectId: input.projectId, versionId: input.versionId, code: `REQ-${scoped}`, title: `Requirement ${scoped}`, description: `Deterministic requirement ${scoped}`, status: input.loose ? 'adopted' : REQUIREMENT_STATUSES[input.requirementNo % REQUIREMENT_STATUSES.length], priority: priority(input.requirementNo), type: input.requirementNo % 2 === 0 ? 'feature' : 'optimization', sourceType: input.requirementNo % 3 === 0 ? 'customer' : 'internal', sourceTarget: input.requirementNo % 3 === 0 ? 'perf-customer' : 'perf-team', platform: input.requirementNo % 2 === 0 ? 'web,ios' : 'web', creatorId: input.creatorId, createdAt: hoursAfter(input.requirementNo), updatedAt: hoursAfter(input.requirementNo + 1), }; } function buildVersionPlan(input) { const type = PLAN_TYPES[(input.planNo - 1) % PLAN_TYPES.length]; return { id: `${ID_PREFIX}plan-${input.versionId}-${type}`, versionId: input.versionId, productId: input.productId, projectId: input.projectId, type, title: `${type} plan for ${input.versionId}`, status: input.planNo === 1 ? 'completed' : 'in_progress', ownerId: input.ownerId, expectedStartAt: daysAfter(input.planNo), expectedEndAt: daysAfter(input.planNo + 3), actualStartAt: daysAfter(input.planNo), completedAt: input.planNo === 1 ? daysAfter(input.planNo + 2) : null, resultUrl: input.planNo === 1 ? 'https://example.test/prototype' : null, requirementCoverage: [], logs: [], createdAt: daysAfter(input.planNo), updatedAt: daysAfter(input.planNo + 1), }; } function buildDevTask(input) { const status = DEV_STATUSES[(input.requirementNo + input.taskNo) % DEV_STATUSES.length]; const blocked = status !== 'submitted' && input.taskNo % 11 === 0; return { id: `${ID_PREFIX}dev-${input.versionId}-${pad(input.requirementNo, 5)}-${pad(input.taskNo, 2)}`, versionId: input.versionId, productId: input.productId, projectId: input.projectId, requirementId: input.requirementId, requirementProductId: input.requirementProductId, categoryId: null, code: `DEV-${pad(input.requirementNo, 5)}-${pad(input.taskNo, 2)}`, title: `Dev task ${pad(input.requirementNo, 5)}-${pad(input.taskNo, 2)}`, description: 'Performance fixture development task', status, priority: priority(input.requirementNo + input.taskNo), assigneeId: input.assigneeId, creatorId: input.assigneeId, isBlocked: blocked, blockReason: blocked ? 'Fixture blocker' : null, expectedStartAt: daysAfter(input.taskNo), expectedEndAt: daysAfter(input.taskNo + 2), startDate: status === 'todo' ? null : daysAfter(input.taskNo), completedAt: status === 'submitted' ? daysAfter(input.taskNo + 2) : null, estimateHours: 4 + (input.taskNo % 5), aiEstimateHours: 3 + (input.taskNo % 4), references: [{ type: 'requirement', id: input.requirementId }], aiDraft: false, aiDraftAt: null, createdAt: hoursAfter(input.requirementNo + input.taskNo), updatedAt: hoursAfter(input.requirementNo + input.taskNo + 1), }; } function buildTestCase(input) { const status = TEST_STATUSES[(input.requirementNo + input.caseNo) % TEST_STATUSES.length]; return { id: `${ID_PREFIX}case-${input.versionId}-${pad(input.requirementNo, 5)}-${pad(input.caseNo, 2)}`, versionId: input.versionId, productId: input.productId, projectId: input.projectId, requirementId: input.requirementId, requirementProductId: input.requirementProductId, categoryId: null, code: `TC-${pad(input.requirementNo, 5)}-${pad(input.caseNo, 2)}`, title: `Test case ${pad(input.requirementNo, 5)}-${pad(input.caseNo, 2)}`, description: 'Performance fixture test case', status, roundNo: input.caseNo % 3 === 0 ? 2 : 1, priority: priority(input.requirementNo + input.caseNo), assigneeId: input.assigneeId, creatorId: input.assigneeId, plannedTestAt: daysAfter(input.caseNo + 3), plannedEndAt: daysAfter(input.caseNo + 4), startedAt: status === 'pending' ? null : daysAfter(input.caseNo + 3), completedAt: ['passed', 'failed', 'blocked'].includes(status) ? daysAfter(input.caseNo + 4) : null, estimateHours: 2 + (input.caseNo % 4), aiEstimateHours: 1 + (input.caseNo % 3), references: [{ type: 'requirement', id: input.requirementId }], aiDraft: false, aiDraftAt: null, createdAt: hoursAfter(input.requirementNo + input.caseNo), updatedAt: hoursAfter(input.requirementNo + input.caseNo + 1), }; } function buildBug(input) { const status = BUG_STATUSES[input.bugNo % BUG_STATUSES.length]; const severity = BUG_SEVERITIES[input.bugNo % BUG_SEVERITIES.length]; return { id: `${ID_PREFIX}bug-${input.versionId}-${pad(input.bugNo, 5)}`, versionId: input.versionId, productId: input.productId, projectId: input.projectId, testCaseId: input.testCaseId ?? null, testCaseVersionId: input.testCaseId ? input.versionId : null, code: `BUG-${pad(input.bugNo, 5)}`, title: `Bug ${pad(input.bugNo, 5)}`, description: 'Performance fixture bug', status, severity, priority: priority(input.bugNo), assigneeId: input.assigneeId, reporterId: input.assigneeId, plannedFixAt: daysAfter(input.bugNo % 10), resolvedAt: ['fixed', 'verifying', 'closed'].includes(status) ? daysAfter((input.bugNo % 10) + 1) : null, closedAt: status === 'closed' ? daysAfter((input.bugNo % 10) + 2) : null, resolution: status === 'closed' ? 'fixed' : null, createdAt: hoursAfter(input.bugNo), updatedAt: hoursAfter(input.bugNo + 1), }; } function buildWorkActivity(input) { const source = input.source; return { id: `${ID_PREFIX}activity-${input.versionId}-${pad(input.activityNo, 5)}`, versionId: input.versionId, productId: input.productId, projectId: input.projectId, actorId: input.actor.id, actorName: input.actor.name, sourceType: 'dev_task', sourceId: source?.id ?? `${ID_PREFIX}missing-source`, sourceVersionId: input.versionId, action: input.activityNo % 5 === 0 ? 'blocked' : 'progress', title: `Activity ${pad(input.activityNo, 5)}`, metadata: { category: input.activityNo % 5 === 0 ? 'risk' : 'progress', summary: `Performance activity ${input.activityNo}`, }, occurredAt: hoursAfter(input.activityNo), createdAt: hoursAfter(input.activityNo), }; } function buildXiaobaoSummary(input) { const riskScore = Math.min(100, 40 + (input.bugCount % 50)); const riskLevel = riskScore >= 75 ? 'likely_delayed' : 'at_risk'; const riskSignature = `${input.version.id}|${riskLevel}|${riskScore}|${input.unfinishedCount}`; return { versionId: input.version.id, riskLevel, riskScore, confidence: 72, forecastReleaseDate: daysAfter(24), riskSignature, summary: { versionId: input.version.id, versionName: input.version.name, riskLevel, riskScore, confidence: 72, riskSignature, dirty: false, signals: { unfinishedCount: input.unfinishedCount, openBugCount: input.bugCount, criticalBugCount: Math.floor(input.bugCount / 12), failedTestCount: Math.floor(input.unfinishedCount / 8), blockedCount: Math.floor(input.unfinishedCount / 15), silentRiskCount: Math.floor(input.unfinishedCount / 20), }, reasons: [ { key: 'remaining_work', title: 'Remaining fixture work', detail: `Fixture has ${input.unfinishedCount} scoped requirements.`, severity: 'warning', }, ], currentSnapshot: { versionId: input.version.id, date: BASE_DATE.toISOString().slice(0, 10), riskScore, riskLevel, openBugCount: input.bugCount, criticalBugCount: Math.floor(input.bugCount / 12), failedTestCount: Math.floor(input.unfinishedCount / 8), blockedCount: Math.floor(input.unfinishedCount / 15), silentRiskCount: Math.floor(input.unfinishedCount / 20), confidence: 72, createdAt: BASE_DATE.toISOString(), }, }, dirty: false, recomputedAt: BASE_DATE, updatedAt: BASE_DATE, }; } function summarizeFixture(fixture) { return { fixtureVersion: FIXTURE_VERSION, size: fixture.size, anchors: fixture.anchors, counts: Object.fromEntries( Object.entries(fixture.rows).map(([key, rows]) => [key, rows.length]), ), }; } async function seedDatabase(fixture, outputPath) { const serverRequire = createRequire(new URL('../apps/server/package.json', import.meta.url)); const { PrismaClient } = serverRequire('@prisma/client'); const prisma = new PrismaClient(); try { await clearPerfRows(prisma); await prisma.user.createMany({ data: fixture.rows.users, skipDuplicates: true }); await prisma.product.createMany({ data: fixture.rows.products, skipDuplicates: true }); await prisma.project.createMany({ data: fixture.rows.projects, skipDuplicates: true }); await prisma.version.createMany({ data: fixture.rows.versions, skipDuplicates: true }); await prisma.requirement.createMany({ data: fixture.rows.requirements, skipDuplicates: true }); await prisma.versionPlan.createMany({ data: fixture.rows.versionPlans, skipDuplicates: true }); await prisma.devTask.createMany({ data: fixture.rows.devTasks, skipDuplicates: true }); await prisma.testCase.createMany({ data: fixture.rows.testCases, skipDuplicates: true }); await prisma.bug.createMany({ data: fixture.rows.bugs, skipDuplicates: true }); await prisma.workActivity.createMany({ data: fixture.rows.workActivities, skipDuplicates: true }); await prisma.xiaobaoRiskSummary.createMany({ data: fixture.rows.xiaobaoRiskSummaries, skipDuplicates: true }); } finally { await prisma.$disconnect(); } const summary = summarizeFixture(fixture); await mkdir(dirname(outputPath), { recursive: true }); await writeFile(outputPath, `${JSON.stringify(summary, null, 2)}\n`, 'utf8'); return summary; } async function clearPerfRows(prisma) { await prisma.$transaction([ prisma.xiaobaoRiskSummary.deleteMany({ where: { versionId: { startsWith: `${ID_PREFIX}version-` } } }), prisma.workActivity.deleteMany({ where: { id: { startsWith: `${ID_PREFIX}activity-` } } }), prisma.bug.deleteMany({ where: { id: { startsWith: `${ID_PREFIX}bug-` } } }), prisma.testCase.deleteMany({ where: { id: { startsWith: `${ID_PREFIX}case-` } } }), prisma.devTask.deleteMany({ where: { id: { startsWith: `${ID_PREFIX}dev-` } } }), prisma.versionPlan.deleteMany({ where: { id: { startsWith: `${ID_PREFIX}plan-` } } }), prisma.requirement.deleteMany({ where: { id: { startsWith: `${ID_PREFIX}req-` } } }), prisma.version.deleteMany({ where: { id: { startsWith: `${ID_PREFIX}version-` } } }), prisma.project.deleteMany({ where: { id: { startsWith: `${ID_PREFIX}project-` } } }), prisma.product.deleteMany({ where: { id: { startsWith: `${ID_PREFIX}product-` } } }), prisma.user.deleteMany({ where: { id: { startsWith: `${ID_PREFIX}user-` } } }), ]); } async function main() { const args = parseArgs(process.argv.slice(2)); const fixture = buildFixture(args.size); const summary = summarizeFixture(fixture); if (args.dryRun) { if (args.json) { console.log(JSON.stringify(summary, null, 2)); } else { printSummary(summary, 'dry-run'); } return; } const outputPath = resolve(process.cwd(), args.output); const seeded = await seedDatabase(fixture, outputPath); if (args.json) { console.log(JSON.stringify(seeded, null, 2)); } else { printSummary(seeded, `seeded; metadata=${args.output}`); } } function printSummary(summary, mode) { console.log(`Large dataset fixture ${mode}`); console.log(`version=${summary.fixtureVersion} size=${summary.size}`); console.log(`anchors=${JSON.stringify(summary.anchors)}`); for (const [name, count] of Object.entries(summary.counts)) { console.log(`${name}=${count}`); } } main().catch((error) => { console.error(error instanceof Error ? error.message : error); process.exitCode = 1; });