Align package.json scripts and their references across scripts/, infra/, and db/ SQL with the dev-first workflow naming scheme. Co-authored-by: Cursor <cursoragent@cursor.com>
182 lines
5.3 KiB
JavaScript
182 lines
5.3 KiB
JavaScript
require('dotenv').config();
|
|
const path = require('path');
|
|
const pool = require('../server/db');
|
|
const INFLUENCES = require('./art-influences-data');
|
|
const { savePaintingImages } = require('./image-fetcher');
|
|
const { discoverInfluencesForWork } = require('./influence-discovery');
|
|
const {
|
|
normalizeInfluencedBy,
|
|
loadMovements,
|
|
resolveWorkRef,
|
|
resolveAndInsertSource,
|
|
} = require('./influence-resolver');
|
|
|
|
const FETCH_IMAGES = process.argv.includes('--fetch-images');
|
|
const DISCOVER = process.argv.includes('--discover');
|
|
const DISCOVER_ONLY = process.argv.includes('--discover-only');
|
|
const LIMIT = parseInt(process.argv.find((a) => a.startsWith('--limit='))?.split('=')[1] || '0', 10);
|
|
const IMAGE_DIR = path.resolve(process.env.IMAGE_DIR || path.join(__dirname, '../data/images'));
|
|
|
|
async function applyEdge(edge, movementsByName, stats) {
|
|
const work = await resolveWorkRef(
|
|
pool,
|
|
edge.work,
|
|
movementsByName,
|
|
FETCH_IMAGES,
|
|
IMAGE_DIR,
|
|
savePaintingImages
|
|
);
|
|
if (!work) {
|
|
stats.failed += 1;
|
|
console.warn(`✗ unresolved work: ${edge.work.artist} / ${edge.work.title}`);
|
|
return;
|
|
}
|
|
|
|
const sources = normalizeInfluencedBy(edge.influencedBy);
|
|
for (const sourceRef of sources) {
|
|
const result = await resolveAndInsertSource(
|
|
pool,
|
|
work,
|
|
sourceRef,
|
|
edge,
|
|
movementsByName,
|
|
FETCH_IMAGES,
|
|
IMAGE_DIR,
|
|
savePaintingImages
|
|
);
|
|
if (!result.ok) {
|
|
if (result.reason !== 'self' && result.reason !== 'self artist') {
|
|
stats.failed += 1;
|
|
console.warn(
|
|
`✗ ${edge.work.artist} / ${edge.work.title} <- ${sourceRef.type || 'painting'} ${result.reason}`
|
|
);
|
|
} else {
|
|
stats.skipped += 1;
|
|
}
|
|
continue;
|
|
}
|
|
if (result.inserted) {
|
|
stats.added += 1;
|
|
console.log(`→ ${edge.work.artist} / ${edge.work.title} ← [${sourceRef.type || 'painting'}] ${result.label}`);
|
|
} else {
|
|
stats.skipped += 1;
|
|
}
|
|
}
|
|
|
|
if (DISCOVER || DISCOVER_ONLY) {
|
|
const discovered = await discoverInfluencesForWork({
|
|
artist: edge.work.artist,
|
|
title: edge.work.title,
|
|
year: edge.year ?? edge.work.year ?? work.year,
|
|
});
|
|
for (const sourceRef of discovered) {
|
|
const result = await resolveAndInsertSource(
|
|
pool,
|
|
work,
|
|
sourceRef,
|
|
sourceRef,
|
|
movementsByName,
|
|
false,
|
|
IMAGE_DIR,
|
|
null
|
|
);
|
|
if (result.inserted) {
|
|
stats.discovered += 1;
|
|
console.log(`~ discovered ${edge.work.artist} / ${edge.work.title} ← [${sourceRef.type}] ${result.label}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async function discoverCatalog(limit) {
|
|
const { rows } = await pool.query(
|
|
`SELECT p.id, p.title, p.year, a.name AS artist_name
|
|
FROM paintings p
|
|
JOIN artists a ON a.id = p.artist_id
|
|
ORDER BY a.name, p.sort_order, p.year NULLS LAST`
|
|
);
|
|
|
|
const targets = limit > 0 ? rows.slice(0, limit) : rows;
|
|
const movementsByName = await loadMovements(pool);
|
|
let discovered = 0;
|
|
|
|
for (const row of targets) {
|
|
const refs = await discoverInfluencesForWork({
|
|
artist: row.artist_name,
|
|
title: row.title,
|
|
year: row.year,
|
|
});
|
|
for (const sourceRef of refs) {
|
|
const result = await resolveAndInsertSource(
|
|
pool,
|
|
{ id: row.id, year: row.year, artist_id: null },
|
|
sourceRef,
|
|
sourceRef,
|
|
movementsByName,
|
|
false,
|
|
IMAGE_DIR,
|
|
null
|
|
);
|
|
if (result.inserted) {
|
|
discovered += 1;
|
|
console.log(`~ discovered ${row.artist_name} / ${row.title} ← [${sourceRef.type}] ${result.label}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
return discovered;
|
|
}
|
|
|
|
async function main() {
|
|
const tableCheck = await pool.query(`
|
|
SELECT EXISTS (
|
|
SELECT FROM information_schema.tables
|
|
WHERE table_schema = 'public' AND table_name = 'painting_influence_sources'
|
|
) AS ok
|
|
`);
|
|
if (!tableCheck.rows[0]?.ok) {
|
|
console.error('Run npm run dev:migrate:influence-sources before update-influences.');
|
|
process.exit(1);
|
|
}
|
|
|
|
const movementsByName = await loadMovements(pool);
|
|
const stats = { added: 0, skipped: 0, failed: 0, discovered: 0 };
|
|
|
|
if (!DISCOVER_ONLY) {
|
|
const edges = LIMIT > 0 ? INFLUENCES.slice(0, LIMIT) : INFLUENCES;
|
|
for (const edge of edges) {
|
|
await applyEdge(edge, movementsByName, stats);
|
|
}
|
|
}
|
|
|
|
if (DISCOVER_ONLY) {
|
|
stats.discovered += await discoverCatalog(LIMIT);
|
|
}
|
|
|
|
const [totalSources, totalLegacy, connected] = await Promise.all([
|
|
pool.query('SELECT COUNT(*)::int AS n FROM painting_influence_sources'),
|
|
pool.query('SELECT COUNT(*)::int AS n FROM painting_influences'),
|
|
pool.query(`
|
|
SELECT COUNT(DISTINCT a.id)::int AS n
|
|
FROM artists a
|
|
JOIN paintings p ON p.artist_id = a.id
|
|
LEFT JOIN painting_influence_sources pis ON pis.painting_id = p.id
|
|
LEFT JOIN painting_influences pi ON pi.painting_id = p.id OR pi.influenced_by_painting_id = p.id
|
|
WHERE pis.id IS NOT NULL OR pi.id IS NOT NULL
|
|
`),
|
|
]);
|
|
|
|
console.log(
|
|
`\nDone: ${stats.added} curated added, ${stats.discovered} discovered, ${stats.skipped} skipped, ${stats.failed} failed`
|
|
);
|
|
console.log(`Total influence sources: ${totalSources.rows[0].n} (legacy painting edges: ${totalLegacy.rows[0].n})`);
|
|
console.log(`Artists connected to influence graph: ${connected.rows[0].n}`);
|
|
|
|
await pool.end();
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|