chore(platform): document script CLI output

What changed:
- Added explicit no-console policy for platform-service CLI/codegen scripts.
- Replaced the remaining migrate-referrals catch any with unknown narrowing.
- Added a TODO for making migrate-referrals --help independent of service env loading.

Warning impact:
- Platform script lint warnings: 78 -> 0.
- Workspace lint: 90 -> 12 warnings.

Verification:
- pnpm --filter @lysnrai/platform-service build
- pnpm --filter @lysnrai/platform-service exec eslint scripts --ext .ts
- pnpm lint

Note:
- migrate-referrals --help still requires service env due eager config import; TODO added for a follow-up behavior-safe refactor.
This commit is contained in:
Saravana Achu Mac 2026-05-04 16:45:42 -07:00
parent 80b9587adb
commit 2c9dc1870d
2 changed files with 32 additions and 13 deletions

View File

@ -16,6 +16,8 @@
* @module platform-service/scripts/gen-module * @module platform-service/scripts/gen-module
*/ */
/* eslint-disable no-console -- This generator is a CLI; console output is its user interface. */
import { promises as fs } from 'node:fs'; import { promises as fs } from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';

View File

@ -18,10 +18,15 @@
* npx tsx scripts/migrate-referrals.ts --mode new-only * npx tsx scripts/migrate-referrals.ts --mode new-only
*/ */
/* eslint-disable no-console -- This migration script is a CLI; console output is its progress and summary UI. */
import { CosmosClient, type Container } from '@azure/cosmos'; import { CosmosClient, type Container } from '@azure/cosmos';
import { config } from '../src/lib/config.js'; import { config } from '../src/lib/config.js';
import type { ReferralDoc } from '../src/modules/referrals/types.js'; import type { ReferralDoc } from '../src/modules/referrals/types.js';
// TODO(migration-cli): Defer service config loading so `--help` can run without
// requiring COSMOS_ENDPOINT, COSMOS_KEY, and JWT_SECRET in the environment.
interface MigrationOptions { interface MigrationOptions {
productId?: string; productId?: string;
batchSize: number; batchSize: number;
@ -30,6 +35,16 @@ interface MigrationOptions {
dryRun: boolean; dryRun: boolean;
} }
function getErrorCode(error: unknown): number | undefined {
return typeof error === 'object' && error !== null && 'code' in error
? Number((error as { code: unknown }).code)
: undefined;
}
function getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
function parseArgs(): MigrationOptions { function parseArgs(): MigrationOptions {
const args = process.argv.slice(2); const args = process.argv.slice(2);
const options: MigrationOptions = { const options: MigrationOptions = {
@ -150,11 +165,11 @@ async function backfillProduct(
}) })
.fetchAll(); .fetchAll();
const existingIds = new Set(existingDocs.map((d) => d.id)); const existingIds = new Set(existingDocs.map(d => d.id));
console.log(`[${productId}] ${existingIds.size} documents already in new container`); console.log(`[${productId}] ${existingIds.size} documents already in new container`);
// Filter docs needing migration // Filter docs needing migration
const toMigrate = oldDocs.filter((d) => !existingIds.has(d.id)); const toMigrate = oldDocs.filter(d => !existingIds.has(d.id));
console.log(`[${productId}] ${toMigrate.length} documents need migration`); console.log(`[${productId}] ${toMigrate.length} documents need migration`);
if (toMigrate.length === 0) { if (toMigrate.length === 0) {
@ -172,7 +187,7 @@ async function backfillProduct(
); );
await Promise.all( await Promise.all(
batch.map(async (doc) => { batch.map(async doc => {
try { try {
if (!doc.referrerId) { if (!doc.referrerId) {
result.errors.push(`Doc ${doc.id}: missing referrerId (required for new PK)`); result.errors.push(`Doc ${doc.id}: missing referrerId (required for new PK)`);
@ -181,11 +196,11 @@ async function backfillProduct(
await newContainer.items.create(doc); await newContainer.items.create(doc);
result.migrated++; result.migrated++;
} catch (err: any) { } catch (err: unknown) {
if (err.code === 409) { if (getErrorCode(err) === 409) {
result.skipped++; result.skipped++;
} else { } else {
result.errors.push(`Doc ${doc.id}: ${err.message}`); result.errors.push(`Doc ${doc.id}: ${getErrorMessage(err)}`);
} }
} }
}) })
@ -217,8 +232,8 @@ async function verifyConsistency(
.fetchAll(), .fetchAll(),
]); ]);
const oldMap = new Map(oldDocs.map((d) => [d.id, d])); const oldMap = new Map(oldDocs.map(d => [d.id, d]));
const newMap = new Map(newDocs.map((d) => [d.id, d])); const newMap = new Map(newDocs.map(d => [d.id, d]));
// Check docs in both containers for consistency // Check docs in both containers for consistency
for (const [id, newDoc] of newMap) { for (const [id, newDoc] of newMap) {
@ -298,7 +313,7 @@ async function main(): Promise<void> {
console.log(` Skipped: ${result.skipped}`); console.log(` Skipped: ${result.skipped}`);
if (result.errors.length > 0) { if (result.errors.length > 0) {
console.log(` Errors: ${result.errors.length}`); console.log(` Errors: ${result.errors.length}`);
result.errors.slice(0, 5).forEach((e) => console.log(` - ${e}`)); result.errors.slice(0, 5).forEach(e => console.log(` - ${e}`));
if (result.errors.length > 5) { if (result.errors.length > 5) {
console.log(` ... and ${result.errors.length - 5} more`); console.log(` ... and ${result.errors.length - 5} more`);
} }
@ -313,14 +328,16 @@ async function main(): Promise<void> {
console.log(`\n Verifying consistency...`); console.log(`\n Verifying consistency...`);
const verifyResult = await verifyConsistency(oldContainer, newContainer, productId); const verifyResult = await verifyConsistency(oldContainer, newContainer, productId);
const realInconsistencies = verifyResult.inconsistencies.filter( const realInconsistencies = verifyResult.inconsistencies.filter(
(i) => !i.issue.includes('pending backfill') i => !i.issue.includes('pending backfill')
); );
if (realInconsistencies.length === 0) { if (realInconsistencies.length === 0) {
console.log(` Consistency check passed (pending: ${verifyResult.inconsistencies.filter(i => i.issue.includes('pending backfill')).length})`); console.log(
` Consistency check passed (pending: ${verifyResult.inconsistencies.filter(i => i.issue.includes('pending backfill')).length})`
);
} else { } else {
console.log(` WARNING: ${realInconsistencies.length} inconsistencies found:`); console.log(` WARNING: ${realInconsistencies.length} inconsistencies found:`);
realInconsistencies.slice(0, 5).forEach((i) => console.log(` - ${i.id}: ${i.issue}`)); realInconsistencies.slice(0, 5).forEach(i => console.log(` - ${i.id}: ${i.issue}`));
} }
} }
} }
@ -345,7 +362,7 @@ async function main(): Promise<void> {
console.log('3. Monitor for issues, then delete old container when confident'); console.log('3. Monitor for issues, then delete old container when confident');
} }
main().catch((err) => { main().catch(err => {
console.error('Migration failed:', err); console.error('Migration failed:', err);
process.exit(1); process.exit(1);
}); });