Files
usenet-indexer/commands/releases_readable.ts
T
Daan MeijerandClaude Fable 5.1 44c0c73b05
Test and deploy / test (push) Successful in 3m9s
Test and deploy / deploy (push) Successful in 40m8s
Pending ladder by class, twins by content, dropped classes at the edge
The prober names 6% of the unnamed releases it finishes and the pending
queue (44M) grows eleven times faster than it probes. A sample of 44,530
finished pending rows shows the headers predict a name: size bucket, name
shape, completeness, tool and group separate 0% classes from 95% ones,
while the learner keyed on tool and group alone (LOG.md (130)).

- lib/release/classes.ts, names::pending_class, SQL pending_class():
  a release's class (size × shape × partial, 1..70), the three in step.
- Migration 6800: pending_releases.klass and target_bytes (the first
  target's article; candidates carry bytes now), pending_classes with
  each class's climbs, names, waiting and verdict, the claim's index.
- The ladder claims the classes with the best record first (a prior for
  the unexplored, at most a quarter of a claim from one), the cheapest
  target first within one, then the oldest of the rest; records classes
  like signatures and drops them at the same thresholds; discards their
  waiting releases. Claimed rows sorted in code: an UPDATE returns none.
- The rule set's droppedClasses: the edge keeps unnamed releases of a
  dropped class out of the pending store (counted as garbage), the
  explored sample excepted.
- hash_twins.ts: a probed file whose first 16 KiB and size match a named
  catalog release's names a pending release the probe didn't, and sets
  the inner name of a catalog release without one.
- pending:classes classes the rows from before and seeds the record from
  the finished ones; rules:classes publishes the dropped classes;
  releases:readable also marks the rows' candidates unreadable.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-02 12:22:32 +01:00

91 lines
3.8 KiB
TypeScript

import { BaseCommand, flags } from '@adonisjs/core/ace'
import type { CommandOptions } from '@adonisjs/core/types/ace'
import db from '@adonisjs/lucid/services/db'
import { isUnreadableName } from '#lib/release/index'
/** Its place in worker_cursors (the last release id done), for a run that was stopped. */
const CURSOR = 'readable'
/**
* The readable flag of the catalog's releases by today's classifier (isUnreadableName, LOG.md (129)): rows flagged
* readable whose name no one can read ("Zx5AjNaK-mh7CFg6Hf8uu", "Description - Lt5nHjlo3RiuBb3C") get readable =
* false, so the readable filter hides them (an inner name from the prober still shows them), and their probe
* candidates marked unreadable, the prober's first tiers. By the release name,
* which is the shared file stem for most rows; not by the chunks' file names, which the edge judges from now on.
* Batches by id, a dry run by default; --apply writes, and goes on from its cursor when run again.
*/
export default class ReleasesReadable extends BaseCommand {
static commandName = 'releases:readable'
static description =
'Clears the readable flag of catalog rows whose name is unreadable by the current classifier; counts only without --apply'
static options: CommandOptions = { startApp: true }
@flags.number({ description: 'Release ids per batch' })
declare batch: number
@flags.number({ description: 'Stop after this id (a sample)' })
declare toId: number
@flags.boolean({ description: 'Write the flag; without it, count and show a sample' })
declare apply: boolean
@flags.boolean({ description: 'Start from the first id, not the cursor' })
declare restart: boolean
async run() {
const batch = this.batch ?? 20_000
const { rows: ends } = await db.rawQuery(`SELECT min(id) AS lo, max(id) AS hi FROM releases`)
const hi = Math.min(Number(ends[0].hi), this.toId ?? Infinity)
const { rows: cursors } = await db.rawQuery(`SELECT key FROM worker_cursors WHERE name = ?`, [
CURSOR,
])
let after =
this.restart || !this.apply || !cursors[0] ? Number(ends[0].lo) - 1 : Number(cursors[0].key)
let looked = 0
let changed = 0
const sample: string[] = []
const started = Date.now()
while (after < hi) {
const to = Math.min(after + batch, hi)
const { rows } = await db.rawQuery(
`SELECT id, name FROM releases WHERE id > ? AND id <= ? AND readable`,
[after, to]
)
looked += rows.length
const ids: number[] = []
for (const r of rows) {
if (!isUnreadableName(r.name)) continue
ids.push(Number(r.id))
if (sample.length < 40) sample.push(r.name)
}
changed += ids.length
if (this.apply) {
if (ids.length > 0) {
await db.rawQuery(`UPDATE releases SET readable = false WHERE id = ANY(?::bigint[])`, [
ids,
])
// Their probe candidates to the unreadable tiers, which the prober takes first (LOG.md (130)).
await db.rawQuery(
`UPDATE probe_candidates SET unreadable = true WHERE release_id = ANY(?::bigint[]) AND NOT unreadable`,
[ids]
)
}
await db.rawQuery(
`INSERT INTO worker_cursors (name, at, key) VALUES (?, now(), ?) ON CONFLICT (name) DO UPDATE SET at = excluded.at, key = excluded.key`,
[CURSOR, String(to)]
)
}
after = to
if (Math.floor(after / 2_000_000) !== Math.floor((after - batch) / 2_000_000)) {
this.logger.info(
`Up to id ${after} of ${hi}: ${changed} unreadable of ${looked} readable (${Math.round((Date.now() - started) / 1000)} s).`
)
}
}
this.logger.info(
`${this.apply ? 'Cleared' : 'Would clear'} ${changed} of ${looked} readable rows.`
)
if (!this.apply) for (const n of sample) this.logger.info(` ${n}`)
}
}