From c64341c032e10645c3ebb55f824f3707f915d796 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Fri, 25 Sep 2026 09:18:02 +0000 Subject: [PATCH] automotive: index the collection by the part that failed Ten feeds over the data already flowing, no new source and no new credential. The honest framing first, because it shaped the result: there is no free car-parts catalogue with year/make/model fitment. ACES/VCdb (Auto Care Association) and TecDoc are both subscription, and eBay's Browse API -- whose compatibility_filter is the only free source of real fitment anywhere, and only for motor-vehicle categories -- is not usable here. Most retailer catalogues are also displaying licensed ACES data, so scraping fitment off them redistributes someone else's licensed database, which is a worse exposure than a robots.txt violation. RockAuto's robots.txt names ClaudeBot explicitly; Autodoc, PartSouq and parts.cat.com sit behind Cloudflare or Akamai. What IS free, keyless and already ingested is NHTSA's component taxonomy, which goes down to the individual fastener -- one campaign reads SUSPENSION:FRONT:SPRINGS:LEAF SPRING ASSEMBLY:U-BOLT, LEAF SPRING TO AXLE. The nhtsa-recalls adapter has always slugified Component into tags; nothing was reading them as a part index. Live right now: 4,914 recalls and 13,044 complaints. So these are feeds rather than an adapter: brakes, airbags and restraints, tires and wheels, electrical, fuel system, engine and powertrain, steering and suspension, structure and body, glass and wipers, equipment. Each carries recalls AND complaints, because a recall is the manufacturer admitting it and a complaint is an owner noticing first, and either alone is half the story. Every tag was checked against the live API before being written down, which turned up a wart worth knowing. `slugify` DROPS "/" instead of hyphenating it, so a component containing one collapses into a single run-on word: LATCHES/LOCKS/LINKAGES is `latcheslockslinkages` and VISIBILITY/WIPER is `visibilitywiper`. The hyphenated spellings match nothing -- I wrote `visibility-wiper` first and it returned zero. The feeds use what is actually in the data, and a test pins both so nobody corrects the spelling and silently empties two feeds. Fixing slugify would be the real repair, but it would re-tag every adapter in the deployment and orphan the tags already written, so it is documented rather than changed. 9 new tests; 44 passing across the automotive files; lint clean. Full suite 2008 tests with the usual 5 Redis/Postgres integration failures this box cannot run. Co-Authored-By: Claude Opus 5 (1M context) --- packages/core/src/seed.js | 126 +++++++++++++++++++++++++++++ test/automotive-part-feeds.test.js | 92 +++++++++++++++++++++ 2 files changed, 218 insertions(+) create mode 100644 test/automotive-part-feeds.test.js diff --git a/packages/core/src/seed.js b/packages/core/src/seed.js index db77863..1e601a1 100644 --- a/packages/core/src/seed.js +++ b/packages/core/src/seed.js @@ -809,6 +809,132 @@ export const DEFAULT_FEEDS = [ name: 'Makes, models and years', query: { kinds: ['model'] }, }, + /* + * The same automotive data indexed by the PART that failed rather than + * by the vehicle it was bolted to. + * + * There is no free car-parts catalogue with year/make/model fitment -- + * ACES/VCdb and TecDoc are both subscription, and eBay's Browse API is + * the only free source of real fitment, which this deployment cannot + * use. What IS free and already flowing is NHTSA's component taxonomy, + * which goes down to the individual fastener ('SUSPENSION:FRONT: + * SPRINGS:LEAF SPRING ASSEMBLY:U-BOLT, LEAF SPRING TO AXLE'). + * + * So these are feeds, not a new source: the adapter already slugifies + * Component into tags, and nothing needs to be fetched again. Every tag + * below was checked against the live API before being written down. + * + * Two of them look misspelt and are not. `slugify` drops "/" instead of + * turning it into a hyphen, so a component containing one collapses into a + * single run-on word: LATCHES/LOCKS/LINKAGES is `latcheslockslinkages` and + * VISIBILITY/WIPER is `visibilitywiper`. The hyphenated spellings match + * nothing. Changing slugify would be the real fix, but it would silently + * re-tag every adapter in the deployment and orphan the tags already + * written, so the feeds use what is actually in the data. + */ + { + collection: 'automotive', + slug: 'parts-brakes', + name: 'Brakes', + description: + 'Braking recalls and complaints across every manufacturer: hydraulic, air and parking brakes.', + /* Recalls and complaints together: a recall is the manufacturer + admitting it, a complaint is an owner noticing first. */ + query: { + kinds: ['recall', 'complaint'], + tags: ['service-brakes-hydraulic', 'service-brakes-air', 'parking-brake'], + }, + }, + { + collection: 'automotive', + slug: 'parts-airbags', + name: 'Airbags and restraints', + description: 'The restraint system: airbags, inflators, seat belts and child seats.', + /* Recalls and complaints together: a recall is the manufacturer + admitting it, a complaint is an owner noticing first. */ + query: { kinds: ['recall', 'complaint'], tags: ['air-bags', 'seat-belts', 'child-seat'] }, + }, + { + collection: 'automotive', + slug: 'parts-tires-and-wheels', + name: 'Tires and wheels', + description: + 'Tire and wheel recalls, including the tire-only campaigns that never name a vehicle.', + /* Recalls and complaints together: a recall is the manufacturer + admitting it, a complaint is an owner noticing first. */ + query: { kinds: ['recall', 'complaint'], tags: ['tires', 'wheels'] }, + }, + { + collection: 'automotive', + slug: 'parts-electrical', + name: 'Electrical system', + description: + 'Wiring, charging, control modules and lighting — the largest single component category.', + /* Recalls and complaints together: a recall is the manufacturer + admitting it, a complaint is an owner noticing first. */ + query: { kinds: ['recall', 'complaint'], tags: ['electrical-system', 'exterior-lighting'] }, + }, + { + collection: 'automotive', + slug: 'parts-fuel-system', + name: 'Fuel system', + description: 'Pumps, lines, tanks and injection, where a defect usually means a fire risk.', + /* Recalls and complaints together: a recall is the manufacturer + admitting it, a complaint is an owner noticing first. */ + query: { + kinds: ['recall', 'complaint'], + tags: ['fuel-system-gasoline', 'fuel-system-diesel', 'fuel-system-other'], + }, + }, + { + collection: 'automotive', + slug: 'parts-engine-and-powertrain', + name: 'Engine and powertrain', + description: 'Engine, cooling, transmission, driveline and axles.', + /* Recalls and complaints together: a recall is the manufacturer + admitting it, a complaint is an owner noticing first. */ + query: { + kinds: ['recall', 'complaint'], + tags: ['engine', 'power-train', 'engine-and-engine-cooling'], + }, + }, + { + collection: 'automotive', + slug: 'parts-steering-and-suspension', + name: 'Steering and suspension', + description: 'Steering gear, columns, springs, control arms and the fasteners holding them on.', + /* Recalls and complaints together: a recall is the manufacturer + admitting it, a complaint is an owner noticing first. */ + query: { kinds: ['recall', 'complaint'], tags: ['steering', 'suspension'] }, + }, + { + collection: 'automotive', + slug: 'parts-structure-and-body', + name: 'Structure and body', + description: 'Frame, body, latches and locks.', + /* Recalls and complaints together: a recall is the manufacturer + admitting it, a complaint is an owner noticing first. */ + query: { kinds: ['recall', 'complaint'], tags: ['structure', 'latcheslockslinkages'] }, + }, + { + collection: 'automotive', + slug: 'parts-visibility', + name: 'Glass, mirrors and wipers', + description: 'Windshields, mirrors, defrosters and wiper systems.', + /* Recalls and complaints together: a recall is the manufacturer + admitting it, a complaint is an owner noticing first. */ + query: { kinds: ['recall', 'complaint'], tags: ['visibilitywiper', 'visibility'] }, + }, + { + collection: 'automotive', + slug: 'parts-equipment', + name: 'Equipment and aftermarket parts', + description: + 'Recalls filed against a part rather than a vehicle: aftermarket equipment, cameras and accessories.', + /* Recalls and complaints together: a recall is the manufacturer + admitting it, a complaint is an owner noticing first. */ + query: { kinds: ['recall', 'complaint'], tags: ['equipment', 'back-over-prevention'] }, + }, /* Markets. The first two are the worldwide half and the rest are US, which is the shape of what is actually given away: the register of the world's venues is public, and their prices are not. */ diff --git a/test/automotive-part-feeds.test.js b/test/automotive-part-feeds.test.js new file mode 100644 index 0000000..2d09c7e --- /dev/null +++ b/test/automotive-part-feeds.test.js @@ -0,0 +1,92 @@ +import { describe, expect, test } from 'bun:test'; +import { slugify } from '../packages/core/src/adapter.js'; + +process.env.DATABASE_URL ??= 'postgres://test:test@localhost:5432/test'; +process.env.SITE_URL ??= 'https://nichedb.test'; +const { DEFAULT_FEEDS } = await import('../packages/core/src/seed.js'); + +const partFeeds = DEFAULT_FEEDS.filter( + (f) => f.collection === 'automotive' && f.slug.startsWith('parts-'), +); + +describe('the automotive collection, indexed by the part that failed', () => { + test('there is a feed per major component group', () => { + expect(partFeeds.length).toBeGreaterThanOrEqual(10); + const slugs = partFeeds.map((f) => f.slug); + for (const expected of [ + 'parts-brakes', + 'parts-airbags', + 'parts-tires-and-wheels', + 'parts-electrical', + 'parts-fuel-system', + 'parts-engine-and-powertrain', + 'parts-steering-and-suspension', + ]) { + expect(slugs).toContain(expected); + } + }); + + test('every part feed carries both recalls and complaints', () => { + // A recall is the manufacturer admitting it; a complaint is an owner + // noticing first. A feed with only one of them is half the story. + for (const feed of partFeeds) { + expect(feed.query.kinds).toEqual(['recall', 'complaint']); + } + }); + + test('every part feed filters on at least one component tag', () => { + for (const feed of partFeeds) { + expect(Array.isArray(feed.query.tags)).toBe(true); + expect(feed.query.tags.length).toBeGreaterThan(0); + } + }); + + test('slugs and names are unique', () => { + const slugs = partFeeds.map((f) => f.slug); + const names = partFeeds.map((f) => f.name); + expect(new Set(slugs).size).toBe(slugs.length); + expect(new Set(names).size).toBe(names.length); + }); + + test('every part feed explains itself', () => { + for (const feed of partFeeds) { + expect(typeof feed.description).toBe('string'); + expect(feed.description.length).toBeGreaterThan(20); + } + }); +}); + +describe('the tags that look misspelt and are not', () => { + /* + * `slugify` drops "/" rather than turning it into a hyphen, so an NHTSA + * component containing one collapses into a single run-on word. Both of + * these were checked against the live API: the run-on form returns items and + * the hyphenated form returns nothing. These tests exist so nobody + * "corrects" the spelling and silently empties two feeds. + */ + test('slugify drops a slash instead of hyphenating it', () => { + expect(slugify('LATCHES/LOCKS/LINKAGES')).toBe('latcheslockslinkages'); + expect(slugify('VISIBILITY/WIPER')).toBe('visibilitywiper'); + }); + + test('it does hyphenate spaces and commas, which is why most tags look normal', () => { + expect(slugify('SERVICE BRAKES, AIR')).toBe('service-brakes-air'); + expect(slugify('FUEL SYSTEM, GASOLINE')).toBe('fuel-system-gasoline'); + expect(slugify('BACK OVER PREVENTION')).toBe('back-over-prevention'); + }); + + test('the feeds use the run-on spellings that are actually in the data', () => { + const tags = partFeeds.flatMap((f) => f.query.tags); + expect(tags).toContain('latcheslockslinkages'); + expect(tags).toContain('visibilitywiper'); + expect(tags).not.toContain('latches-locks-linkages'); + expect(tags).not.toContain('visibility-wiper'); + }); + + test('every tag is what slugify would produce for some component', () => { + // Guards against a hand-written tag that no adapter could ever emit. + for (const tag of partFeeds.flatMap((f) => f.query.tags)) { + expect(slugify(tag)).toBe(tag); + } + }); +});