Three-level core resolution with priority overrides → dictionaries/model → suspects:
1. dicts/overrides.txt (highest priority)
Point fixes via exact lookup on Raw BEFORE the positional logic.
Format: <original line> => artist | title. An override fixes
Artist/Title and shifts the dictionary tail zone by the actual core
length (overrideCoreLength, fallback 2 tokens).
2. dicts/artists.txt
Whitelist of hyphenated artists (Iku-Turso, Post-Mortem, ...): a
[0]-[1] hit → Artist = 2 tokens, Title = [2], tail zone shifted.
One entry covers all releases of the artist automatically.
3. Suspected title continuation
Unknown tokens between the title zone and the tail (followed later by
dictionary markers) no longer pollute UnknownTokens; they go to a
dedicated TitleExtra field + warning "suspected title continuation" —
a reviewer hint that the core is probably longer than 2 tokens.
Catalogue numbers (parenthesized tokens) are resolved with FIRST
priority and never land in suspects.
Type field (normalized base release type):
- new normalization map dicts/types.txt (<Base> = variants):
EP = EP, CDS, CDM, 7INCH_VINYL, CDEP, ..., REMASTERED_EP; Demo = ...;
Split; Single; Compilation; Collaboration (curator-extendable)
- tokens matched against edition/source are additionally normalized to
the base type (first match wins); edition.txt/source.txt untouched,
Source stays raw
- TypeDistribution map[string]int added to GroupStats and the report
(console/JSON)
- OST/INSTRUMENTAL are release traits, not types; excluded from the map
Tests: old cases updated to the new suspect semantics; acceptance tests
added for overrides (Milicia_Oscura-Anti-Cristo), artist dictionary
(Iku-Turso), the suspect section, Type normalization (CDEP → EP with
raw Source=CDEP, first-match-wins) and TypeDistribution.
go build / go vet / go test ./internal/releaseparser/... — all green.
Generalise the parser: the MOONBLOOD group profile is gone, the parser and
its dictionaries are now group-agnostic under internal/releaseparser/groups.
Positional model:
- [0]=Artist, [1]=Title are fixed and never matched against dictionaries, so
titles like "Split"/"Demo"/"De" are never eaten.
- [len-1]=Group is always read from the line, not from the query key.
- [len-2]=Year when it is a 4-digit 19xx|20xx token; otherwise the slot is
resolved as a middle-zone token (e.g. FLAC is still picked up as format).
- The middle zone is resolved via case-insensitive dictionaries in the order
format -> source -> bit-depth -> resolution -> edition -> language ->
catalogue number; stored values keep their original case.
Catalogue numbers: any "(...)" token in the middle zone is a catalogue number
regardless of content (digits, roman numerals, dots, underscores, mixed case);
splitting never breaks tokens inside parentheses.
Dictionaries moved to internal/releaseparser/groups/resources/dicts and
embedded via go:embed; dead profile fields (Name, ExpectedTailOrder) removed;
profile comments rewritten in British English.
File-based AnalyzeGroup/TestParseMoonblood removed — titles now come from the
database (cmd/srrdb: srrdb <releaser>..., config/secret.yaml DSN).
Validation output (cmd/srrdb):
- tmp/releaseparser/<group>.json — per-group parsed ParsedRelease records
- tmp/releaseparser/<releaser>.report.json — per-request aggregate (same as console)
- review_unknown_tokens.txt — unknown/warning rows for manual review
Tests updated for the positional model; added
TestParseCatalogNumberWithDotAndRoman for (DYN.3140-2) and (GOWP_DCCCLXXXIX).
go build, go vet and go test all pass.