Files
MaashkinoozheandClaude Opus 5.5 cd6a30a76b Discovery final review fixes: lean publish, robust scoring, cheap prune (BL-0051)
- Cache prune counts directory entries first and only reads files when over
  the cap; tests for cross-kind prune, schema-mismatch and corrupt entries.
- Scoring tolerates malformed manga.json and cached records (non-list tags,
  object or null relations, non-object items); a tag without a rank no longer
  passes the top-tag filter; authors without an integer AniList id are dropped.
- discovery.json publishes only candidates some feed shows; examined/clipped
  still describe the pool. Tests that asserted the published count equals the
  pool size now check the pool via candidatePool() and the published set
  against the feeds.
- The post-harvest rescore failure test now breaks discovery.json right before
  the publish (via a scraper stand-in) instead of relying on tags: 5, and a
  following successful harvest clears the stale lastError.
- Consent text names everything sent to AniList; paused label mentions admin
  harvests; the UI test asserts the XSS seed instead of skipping it.
- Slot-mix test proves the fresh/outside reservations matter.
- Docs: privacy note, fresh fallback reason, per-period cap, dry-run wording,
  published-candidate rule; plan/spec hygiene and spec status.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 09:43:31 +01:00

32 lines
2.0 KiB
JavaScript

#!/usr/bin/env node
'use strict';
// Read-only CLI. Never writes the reader's discovery.json or scraper queue.
const fs = require('node:fs');
const path = require('node:path');
const { loadCatalog } = require('../greatest');
const { generate } = require('./engine');
const { DiscoveryCache } = require('./cache');
if (!process.argv.includes('--dry-run')) {
console.error('Usage: node discovery/cli.js run --dry-run [--data DIR] [--catalog FILE] [--show]');
process.exit(2);
}
const arg = (name, fallback) => { const i = process.argv.indexOf(name); return i < 0 ? fallback : process.argv[i + 1]; };
const data = path.resolve(arg('--data', process.env.DATA || 'data'));
const db = JSON.parse(fs.readFileSync(path.join(data, 'db.json'), 'utf8'));
let library = [];
try { library = JSON.parse(fs.readFileSync(path.join(data, 'library-index.json'), 'utf8')).mangas || []; }
catch (error) { if (error.code !== 'ENOENT') throw error; }
const greatest = loadCatalog(arg('--catalog', path.join(__dirname, '..', 'greatest', 'catalog.json')));
// The harvest cache is only read here (get/list, never put); note get/list still drop a corrupt entry file.
const harvestDir = path.join(data, 'discovery-cache');
const harvestCache = fs.existsSync(harvestDir) ? new DiscoveryCache(harvestDir) : null;
const result = generate({ db, library, greatest, cacheDir: path.join(data, 'scraper', 'metadata', 'anilist'), harvestCache });
// candidates: the ones some feed shows (what a run would publish); examined/clipped: the whole pool.
const summary = { examined: result.examined, clipped: result.clipped, candidates: Object.keys(result.candidates).length,
optedInUsers: Object.values(db.users || {}).filter(u => u.settings?.discoveryEnabled).length,
feedSizes: Object.values(result.feeds).filter(f => !f.disabled).map(f => f.items.length) };
if (process.argv.includes('--show')) summary.titles = Object.values(result.feeds).find(f => !f.disabled)?.items
.map(item => result.candidates[item.candidateId]?.title) || [];
console.log(JSON.stringify(summary, null, 2));