Single-archive rules: stub archives and nonsense pieces dropped at the edge
Test and deploy / test (push) Successful in 3m0s
Test and deploy / deploy (push) Successful in 4m21s

The judge gets each file's articles seen and bytes, and whether the window
closed on the release. A settled release of one unsplit archive posted to 2+
groups is dropped as a stub archive when its articles are <= 256 KiB with the
release >= 1 MB, or it is in 4+ groups, or by a webmail poster (those two
under 100 MB); releases touching music, e-book, comics, tabs or fonts groups
are exempt. A one-article archive of 100 kB-1 MB under a release-like,
unscrambled name without spaces or an e-book format is dropped as a nonsense
piece. Both switched by the rule set (dropStubArchives, dropNonsensePieces),
mirrored in lib/rules, sampled like every drop (LOG.md (123)-(127)).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Daan MeijerandClaude Fable 5.1 committed 2026-10-31 11:35:46 +01:00
1 parent 2f1bbf2538
commit e470e231d0
8 files changed
+397 -15

No files matched your search

+1 -1
View File
@@ -1,2 +1,2 @@
export { uploadTool, signatureGroup, signatureKey, explored } from './upload_tools.js'
export { RuleSet, DEFAULT_RULES, isRandomFile, isScrambledFile, ownerOf, posterWords, type Rules, type Verdict } from './rule_set.js'
export { RuleSet, DEFAULT_RULES, isRandomFile, isScrambledFile, ownerOf, posterWords, stubArchiveLegs, isNonsensePiece, STUB_ARTICLE_BYTES, STUB_MIN_BYTES, STUB_MAX_BYTES, STUB_WIDE_GROUPS, NONSENSE_MIN_BYTES, NONSENSE_MAX_BYTES, type Rules, type Verdict, type JudgedFile } from './rule_set.js'
+103 -4
View File
@@ -30,6 +30,18 @@ export interface Rules {
* again under other titles (119k releases on 2026-10-16; LOG.md 2026-10-16 (65)).
*/
droppedPosters: string[]
/**
* Drop settled releases of one unsplit archive without a par2, posted to 2+ groups, whose articles are at most
* STUB_ARTICLE_BYTES with the release at least STUB_MIN_BYTES, or posted to 4+ groups, or by a webmail poster, the
* last two under STUB_MAX_BYTES: the exe-stub and recycled-installer floods under film and software names (55%
* of the catalog on 2026-10-30; 99.3% of the known fakes, 0 known content; LOG.md (123)-(126)).
*/
dropStubArchives: boolean
/**
* Drop one-article archives of NONSENSE_MIN_BYTES to NONSENSE_MAX_BYTES under a release-like, unscrambled name:
* the newznzb.* pieces (9% of the catalog; LOG.md (123), (125)).
*/
dropNonsensePieces: boolean
}
export const DEFAULT_RULES: Omit<Rules, 'priorities' | 'droppedSignatures'> = {
@@ -39,9 +51,31 @@ export const DEFAULT_RULES: Omit<Rules, 'priorities' | 'droppedSignatures'> = {
samplePercent: 1,
dropExecutables: true,
droppedPosters: ['celebmovie', 'celebwow', 'celebfx', 'iluvcelebs', 'nudecelebs', 'celebsextapes'],
dropStubArchives: true,
dropNonsensePieces: true,
}
export type Verdict = { rule: null } | { rule: 'poster' | 'executable' | 'garbage' | 'per-article'; key: string; sample: boolean }
/** The stub-archive rule's bounds (edge/src/names.rs): Power-Post's 250 kB default article, the fakes' size span. */
export const STUB_ARTICLE_BYTES = 262144
export const STUB_MIN_BYTES = 1_000_000
export const STUB_MAX_BYTES = 100_000_000
export const STUB_WIDE_GROUPS = 4
/** The nonsense-piece rule's bounds: under 100 kB are cue/log sidecars and adverts, 1 MB and up is rule 1's. */
export const NONSENSE_MIN_BYTES = 100_000
export const NONSENSE_MAX_BYTES = 1_000_000
export type Verdict = { rule: null } | { rule: 'poster' | 'executable' | 'garbage' | 'per-article' | 'stub-archive' | 'nonsense-piece'; key: string; sample: boolean }
/** A file as the judge sees it: its name, parts, groups, poster, and the articles seen with their bytes. */
export interface JudgedFile {
name: string
totalParts: number
groups: string[]
firstMessageId: string
poster: string
segments: number
bytes: number
}
/** A poster as droppedPosters matches it: lowercased, letters and digits only. */
export function posterWords(poster: string): string {
@@ -51,6 +85,61 @@ export function posterWords(poster: string): string {
/** Files that come along with a release's content: they don't make it one of executables or not. */
const SIDECAR = /\.(par2|nfo|sfv|txt|jpe?g|png|nzb)$/i
const EXECUTABLE = /\.(exe|scr|com|pif|bat|cmd|vbs|lnk)\s*$/i
/** One zip, rar or 7z that is not a volume of a set. */
const ARCHIVE = /\.(zip|rar|7z)$/i
const VOLUME = /\.(part\d+\.rar|r\d\d|z\d\d|7z\.\d\d\d|\d\d\d)$/i
/** A poster at a free mail provider: the stub floods' throwaway accounts. */
const WEBMAIL = /@(gmail|googlemail|yahoo|hotmail|outlook|live|msn|aol|gmx|web|t-online|freenet|mail|yandex|icloud|me|hushmail|protonmail|libero|orange|free|arcor|wanadoo|ymail|rocketmail|inbox|mail\.ru|bk|list)\./i
/** A name that reads as a release: a year, a scene tag, a group suffix or dotted words. */
const RELEASE_LIKE = /(\b(19|20)\d\d\b|[._-](720p|1080p|2160p|x264|x265|hevc|bluray|web-?dl|webrip|hdtv|xvid|s\d\de\d\d|german|multi|crack|repack|proper|flac|mp3|xxx|mp4|retail|keygen|portable|incl|v\d+(\.\d+)+)\b|-[A-Za-z0-9]{2,}$|\.[A-Za-z0-9]+\.[A-Za-z0-9]+\.)/i
/**
* Groups whose communities post real single small archives (karaoke zips, e-books, comics, tabs, fonts) the way the
* stub floods do (Power-Post defaults, a crosspost or two): a release touching one is exempt from the stub-archive
* rule. Costs 16 of 32,308 known fakes in the 217k sample (LOG.md (127)).
*/
const STUB_EXEMPT_GROUPS = /(sound|mp3|karaoke|music|audio|flac|lossless|e-?book|epub|comic|guitar|font|magazine)/i
/** The single unsplit archive a release consists of, or null. */
function singleArchive(files: JudgedFile[]): JudgedFile | null {
return files.length === 1 && ARCHIVE.test(files[0].name) && !VOLUME.test(files[0].name) ? files[0] : null
}
/**
* The legs of the stub-archive rule a settled single archive trips: "A" (articles <= STUB_ARTICLE_BYTES, release >=
* STUB_MIN_BYTES: a one-article release always has small articles, so e-books and sidecars are exempt), "X" (4+
* groups) and "W" (webmail poster), the last two under STUB_MAX_BYTES (the fakes stop at ~90 MB; hash-named uploads
* and real albums above it don't). Empty when none.
*/
export function stubArchiveLegs(f: JudgedFile): string {
if (f.groups.length < 2 || f.groups.some((g) => STUB_EXEMPT_GROUPS.test(g))) return ''
let legs = ''
if (f.bytes / Math.max(f.segments, 1) <= STUB_ARTICLE_BYTES && f.bytes >= STUB_MIN_BYTES) legs += 'A'
if (f.groups.length >= STUB_WIDE_GROUPS && f.bytes < STUB_MAX_BYTES) legs += 'X'
if (f.bytes < STUB_MAX_BYTES && WEBMAIL.test(f.poster)) legs += 'W'
return legs
}
/** An e-book by its name ("Author - Title (2020) (epub).rar"): one small article is a book, not a nonsense piece. */
const EBOOK = /\b(epub|mobi|azw3?|pdf|djvu|cbr|cbz|e-?books?)\b/i
/**
* A nonsense piece: one article, one small archive under a release-like, unscrambled name without spaces (the
* newznzb.* pieces are scene-styled or random with a "-XX" suffix; books and albums are written with spaces).
*/
export function isNonsensePiece(f: JudgedFile): boolean {
return (
f.totalParts === 1 &&
f.segments === 1 &&
f.bytes >= NONSENSE_MIN_BYTES &&
f.bytes < NONSENSE_MAX_BYTES &&
!f.name.includes(' ') &&
RELEASE_LIKE.test(fileStem(f.name)) &&
!EBOOK.test(f.name) &&
!isScrambledFile(f.name) &&
!isRandomFile(f.name)
)
}
export function isRandomFile(name: string) {
return looksRandom(fileStem(name))
@@ -98,10 +187,12 @@ export class RuleSet {
/**
* A release's verdict from its files: dropped when its poster is; when its content is bare
* executables; else kept as a whole when any file has a readable name; else dropped as garbage when its signature or tool is
* dropped, as per-article random subjects when every file has few parts.
* executables; when it is a settled single archive that is a stub or a nonsense piece; else kept as a whole when
* any file has a readable name; else dropped as garbage when its signature or tool is dropped, as per-article
* random subjects when every file has few parts. `settled`: the window closed on the release (a par2 can still
* join an open one), which the single-archive rules need.
*/
judge(releaseKey: string, files: { name: string; totalParts: number; groups: string[]; firstMessageId: string; poster: string }[]): Verdict {
judge(releaseKey: string, files: JudgedFile[], settled = true): Verdict {
const sample = explored(releaseKey, this.rules.samplePercent)
if (this.posters.length > 0 && files.length > 0) {
const words = posterWords(files[0].poster)
@@ -114,6 +205,14 @@ export class RuleSet {
return { rule: 'executable', key: EXECUTABLE.exec(content[0].name)![1].toLowerCase(), sample }
}
}
const archive = settled ? singleArchive(files) : null
if (archive) {
if (this.rules.dropStubArchives) {
const legs = stubArchiveLegs(archive)
if (legs !== '') return { rule: 'stub-archive', key: legs, sample }
}
if (this.rules.dropNonsensePieces && isNonsensePiece(archive)) return { rule: 'nonsense-piece', key: uploadTool(archive.firstMessageId), sample }
}
if (files.some((f) => !isRandomFile(f.name))) return { rule: null }
const first = files[0]
const tool = uploadTool(first.firstMessageId)