SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%

Grade parser: descriptive grade words (EX-MT 6, MINT+ 9.5, VG+ 4.5, NM/MT 9.8); a grade recorded without a grader never overrides the title; ri regrade --recheck

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed 13 days ago (Sep 11, 2026) parent b12ede0

5 changed files +30 −6

modified packages/taxonomy/src/index.ts +1 −1
@@ -155,7 +155,7 @@ export function getGrader(slugOrAlias: string | null | undefined): GraderDef | u
155 155
156 156 // Grader token, optional "/DNA" (PSA/DNA autograph slabs), optional descriptive word, then either a
157 157 // numeric grade ("10", "9.5", "(8.5)") or an authentication-only marker ("Authentic", "AUTH", "A").
158 −const GRADE_RE = /\b(PSA|BGS|BECKETT|CGC|SGC|TAG|ACE|CBCS|PGX|WATA|VGA|PCGS|NGC|ANACS|ICCS|PMG)\b(?:\s*\/?\s*DNA\b)?[\s:\-\[(]*(?:GEM\s*MT|GEM\s*MINT|MINT|NM-?MT|PRISTINE|BLACK\s*LABEL)?[\s:\-\[(]*(?:(\d{1,3}(?:\.\d)?)(?![\d.])|\b(AUTHENTIC|AUTH|CERTIFIED\s+AUTHENTIC)\b)?/gi;
158 +const GRADE_RE = /\b(PSA|BGS|BECKETT|CGC|SGC|TAG|ACE|CBCS|PGX|WATA|VGA|PCGS|NGC|ANACS|ICCS|PMG)\b(?:\s*\/?\s*DNA\b)?[\s:\-\[(]*(?:GEM\s*MT|GEM\s*MINT|PRISTINE|BLACK\s*LABEL|NM[-\/]?MT\+?|EX[-\/]?MT\+?|VG[-\/]?EX\+?|MINT\+?|NM\+?|EX\+?|VG\+?|FN\+?|GD\+?|MT\+?|PR|FR)?[\s:\-\[(]*(?:(\d{1,3}(?:\.\d)?)(?![\d.])|\b(AUTHENTIC|AUTH|CERTIFIED\s+AUTHENTIC)\b)?/gi;
159 159 /** Grader tokens that are also ordinary words ("Tag Team", "Ace Attorney") — only trusted when followed by a grade. */
160 160 const AMBIGUOUS_GRADERS = new Set(['tag', 'ace']);
161 161 /** Authentication-only slabs (no numeric grade). Kept apart from raw and from graded variants. */
modified packages/taxonomy/src/taxonomy.test.ts +7 −0
@@ -32,6 +32,13 @@ describe('taxonomy', () => {
32 32 expect(parseGradeFromTitle('Brad Davis [PSA/DNA Certified Authentic Card & Auto]')).toMatchObject({ grader: 'psa', grade: 'authentic' });
33 33 expect(parseGradeFromTitle('Charizard PSA 2021 release')).toMatchObject({ grader: 'psa', grade: null }); // a year is not a grade
34 34 expect(parseGradeFromTitle('Batman #1 CGC (8.5) white pages')).toMatchObject({ grader: 'cgc', grade: '8.5' });
35 + // descriptive grade words between grader and number (Goldin, SCP, PriceCharting titles)
36 + expect(parseGradeFromTitle('1973 Topps Test Pin-Ups Hank Aaron - PSA EX-MT 6')).toMatchObject({ grader: 'psa', grade: '6' });
37 + expect(parseGradeFromTitle('2022 Panini Donruss #374 Brock Purdy Rookie Card - SGC MINT+ 9.5')).toMatchObject({ grader: 'sgc', grade: '9.5' });
38 + expect(parseGradeFromTitle('The Amazing Spider-Man #26 (Marvel, 1965) CGC VG+ 4.5 Cream pages')).toMatchObject({ grader: 'cgc', grade: '4.5' });
39 + expect(parseGradeFromTitle('The Incredible Hulk #318 CGC NM/MT 9.8 White pages')).toMatchObject({ grader: 'cgc', grade: '9.8' });
40 + expect(parseGradeFromTitle('X-Men #2 (Marvel, 1963) CGC FN+ 6.5')).toMatchObject({ grader: 'cgc', grade: '6.5' });
41 + expect(parseGradeFromTitle('Amazing Spider-Man #11 CGC 2.0 1964 Marvel')).toMatchObject({ grader: 'cgc', grade: '2.0' });
35 42 });
36 43 it('keeps slabs with an unreadable grade out of the raw variant', () => {
37 44 expect(variantKey({ grader: 'psa', grade: null })).toEqual({ key: 'psa|unknown', label: 'PSA · grade unknown' });
modified workers/cli.ts +1 −1
@@ -127,7 +127,7 @@ async function main() {
127 127 case 'regrade': {
128 128 // ri regrade [--kind sale|listing|both] [--connector id] [--limit N] [--dry-run]
129 129 const kind = (str('kind') ?? 'both') as 'sale' | 'listing' | 'both';
130 − print(await regradeRecords({ kind, connectorId: str('connector'), limit: num('limit', 50_000), dryRun: Boolean(flags['dry-run']) }));
130 + print(await regradeRecords({ kind, connectorId: str('connector'), limit: num('limit', 50_000), dryRun: Boolean(flags['dry-run']), recheck: Boolean(flags.recheck) }));
131 131 break;
132 132 }
133 133 case 'value': {
modified workers/entity-resolution/regrade.ts +17 −3
@@ -62,13 +62,25 @@ async function candidates(kind: 'sale' | 'listing', limit: number, connectorId?:
62 62 return rows as Row[];
63 63 }
64 64
65 −export async function regradeRecords(opts: { kind?: 'sale' | 'listing' | 'both'; limit?: number; connectorId?: string; dryRun?: boolean } = {}): Promise<RegradeResult> {
65 +/** Rows touched by an earlier regrade pass (audit_log action = regraded), to re-verify with the current parser. */
66 +async function regradedRows(kind: 'sale' | 'listing', limit: number): Promise<Row[]> {
67 + const t = kind === 'sale' ? sales : listings;
68 + const rows = await db()
69 + .select({ id: t.id, assetId: t.assetId, variantId: t.variantId, grader: t.grader, grade: t.grade, rawTitle: t.rawTitle, variantKey: assetVariants.variantKey, vGrader: assetVariants.grader, vGrade: assetVariants.grade, condition: assetVariants.condition, completeness: assetVariants.completeness, sizeLabel: assetVariants.sizeLabel })
70 + .from(t)
71 + .leftJoin(assetVariants, eq(assetVariants.id, t.variantId))
72 + .where(sql`${t.id} in (select entity_id from audit_log where action = 'regraded' and entity_type = ${kind})`)
73 + .limit(limit);
74 + return rows as Row[];
75 +}
76 +
77 +export async function regradeRecords(opts: { kind?: 'sale' | 'listing' | 'both'; limit?: number; connectorId?: string; dryRun?: boolean; recheck?: boolean } = {}): Promise<RegradeResult> {
66 78 const kinds: Array<'sale' | 'listing'> = opts.kind === 'sale' ? ['sale'] : opts.kind === 'listing' ? ['listing'] : ['sale', 'listing'];
67 79 const limit = opts.limit ?? 50_000;
68 80 const res: RegradeResult = { scanned: 0, moved: 0, unknownGrade: 0, unchanged: 0, assets: 0 };
69 81 const touched = new Set<string>();
70 82 for (const kind of kinds) {
71 − const rows = await candidates(kind, limit, opts.connectorId);
83 + const rows = opts.recheck ? await regradedRows(kind, limit) : await candidates(kind, limit, opts.connectorId);
72 84 res.scanned += rows.length;
73 85 const audits: Array<{ entityType: string; entityId: string; action: string; reason: string; details: Record<string, unknown> }> = [];
74 86 for (const r of rows) {
@@ -78,7 +90,9 @@ export async function regradeRecords(opts: { kind?: 'sale' | 'listing' | 'both';
78 90 res.unchanged++;
79 91 continue;
80 92 }
81 − const grade = r.grade ?? (parsed.grader === grader ? parsed.grade : null);
93 + // grade recorded WITH a grader is trusted; a grade recorded without one is a condition value → title wins
94 + const titleGrade = parsed.grader === grader ? parsed.grade : null;
95 + const grade = opts.recheck ? (titleGrade ?? r.grade) : r.grader && r.grader !== 'raw' ? (r.grade ?? titleGrade) : titleGrade;
82 96 const target = await ensureVariant(r.assetId, { grader, grade, qualifier: parsed.qualifier, certificationNumber: null }, { condition: r.condition, completeness: r.completeness }, r.sizeLabel);
83 97 if (target.id === r.variantId && r.grader === grader && r.grade === grade) {
84 98 res.unchanged++;
modified workers/entity-resolution/writers.ts +4 −1
@@ -95,7 +95,10 @@ export function refineGrade(grade: Grade, rawTitle: string | null | undefined):
95 95 const parsed = parseGradeFromTitle(rawTitle);
96 96 if (!parsed.grader || parsed.grader === 'raw') return grade;
97 97 if (grade.grader && parsed.grader !== grade.grader) return grade; // never contradict the connector's grader
98 − return { ...grade, grader: grade.grader ?? parsed.grader, grade: grade.grade ?? parsed.grade, qualifier: grade.qualifier ?? parsed.qualifier };
98 + // A grade recorded without a grader is a condition-scale value (e.g. a price-guide tab), not a slab
99 + // grade: when the title reveals the slab, the title's grade wins.
100 + const g = grade.grader ? (grade.grade ?? parsed.grade) : parsed.grade;
101 + return { ...grade, grader: grade.grader ?? parsed.grader, grade: g, qualifier: grade.qualifier ?? parsed.qualifier };
99 102 }
100 103
101 104 async function applyCatalog(rec: NormalizedCatalogItem): Promise<ApplyResult> {
102 105