From 937f1b5b310b950f3a3a9c631dcba3d0cdc92340 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 20:58:09 +0000 Subject: [PATCH] fix: restore kontakt compatibility, fail unknown compatibleWith The Plectra bouzouki library listed `kontakt`, which matches no entry. Point it at `native-instruments-kontakt` and turn the advisory W123 into the error E206. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019cRjoMGX966CDY2WcgLvvv --- .changeset/compatible-with-e206.md | 13 +++ CLAUDE.md | 2 +- ...a-series-1-8-string-acoustic-bouzouki.yaml | 2 +- docs/VALIDATION_ERRORS.md | 27 +++++++ schema/CONTEXT.md | 2 +- scripts/__tests__/validate.test.ts | 51 +++++++----- scripts/dataset-audit.ts | 4 +- scripts/generate-context.ts | 2 +- scripts/lib/error-codes.ts | 10 +-- scripts/validate.ts | 80 +++++++++++-------- 10 files changed, 127 insertions(+), 66 deletions(-) create mode 100644 .changeset/compatible-with-e206.md diff --git a/.changeset/compatible-with-e206.md b/.changeset/compatible-with-e206.md new file mode 100644 index 000000000..fdf86afd2 --- /dev/null +++ b/.changeset/compatible-with-e206.md @@ -0,0 +1,13 @@ +--- +"catalog": patch +--- + +Restore Kontakt compatibility on Impact Soundworks Plectra Series 1: 8-string Acoustic Bouzouki + +Its `compatibleWith` named `kontakt`, which matches no entry, so the +Kontakt host was dropped. It now names `native-instruments-kontakt`, +the slug the other 165 Kontakt libraries use. + +`pnpm validate` now fails an unknown `compatibleWith` slug (E206) +instead of warning (the retired W123), which is how this one got +through. No other unknown slug existed on main. diff --git a/CLAUDE.md b/CLAUDE.md index ee907a5de..a6d21b895 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -853,7 +853,7 @@ compatibleWith: - elektron-digitone ``` -Advisory warning W123 fires if a `compatibleWith` slug doesn't match an existing software or hardware file. Category aliases in `schema/category-aliases.yaml` map common synonyms (e.g., `soundbank` → `preset-pack`) to canonical categories. +`pnpm validate` fails (E206) if a `compatibleWith` slug doesn't match an existing software or hardware file. Category aliases in `schema/category-aliases.yaml` map common synonyms (e.g., `soundbank` → `preset-pack`) to canonical categories. ## Accessory Entries diff --git a/data/content/impact-soundworks-plectra-series-1-8-string-acoustic-bouzouki.yaml b/data/content/impact-soundworks-plectra-series-1-8-string-acoustic-bouzouki.yaml index 41f22eb41..28377d06c 100644 --- a/data/content/impact-soundworks-plectra-series-1-8-string-acoustic-bouzouki.yaml +++ b/data/content/impact-soundworks-plectra-series-1-8-string-acoustic-bouzouki.yaml @@ -3,7 +3,7 @@ name: "Plectra Series 1: 8-string Acoustic Bouzouki" manufacturer: impact-soundworks primaryCategory: sound-library compatibleWith: - - kontakt + - native-instruments-kontakt url: https://impactsoundworks.com/product/plectra-series-1-8-string-acoustic-bouzouki/ prices: - amount: 59 diff --git a/docs/VALIDATION_ERRORS.md b/docs/VALIDATION_ERRORS.md index adc0f9b6d..a324c5117 100644 --- a/docs/VALIDATION_ERRORS.md +++ b/docs/VALIDATION_ERRORS.md @@ -604,6 +604,33 @@ similarity scoring. --- +### E206: Unknown compatibleWith Reference + +A `compatibleWith` slug on a software or content entry matches no file in +`data/software/` or `data/hardware/`. + +The entry builds and ships either way, but Studio cannot resolve the slug, so the +compatibility is silently lost. `kontakt` in place of `native-instruments-kontakt` +reached `main` this way while the check was the advisory W123. + +**Example:** + +```yaml +# Wrong - no data/software/kontakt.yaml +compatibleWith: + - kontakt + +# Correct - the file name of the host's entry +compatibleWith: + - native-instruments-kontakt +``` + +**Fix:** Use the slug (file name without `.yaml`) of the host's software or hardware +entry. If the host is not in the catalog yet, add it in the same PR or drop the +reference. + +--- + ## Content Errors (E3xx) ### E300: Invalid Markdown diff --git a/schema/CONTEXT.md b/schema/CONTEXT.md index 8e05b2c47..22cf49fbf 100644 --- a/schema/CONTEXT.md +++ b/schema/CONTEXT.md @@ -737,7 +737,7 @@ Content entries (presets, sample packs, expansions) live in `data/content/` as a - `primaryCategory` - Content category (e.g., `preset`, `preset-pack`, `sample-pack`, `sound-library`) - `compatibleWith` - Optional array of software slugs for host products (e.g., `["serum", "omnisphere-2"]`) - Does NOT have `platforms`, `formats`, or `identifiers` fields -- Advisory warning W123 fires if a `compatibleWith` slug doesn't match an existing software file +- `pnpm validate` fails (E206) if a `compatibleWith` slug doesn't match an existing software or hardware file ### Hardware - `slug` - URL-safe identifier diff --git a/scripts/__tests__/validate.test.ts b/scripts/__tests__/validate.test.ts index 33786f84d..7ce97870a 100644 --- a/scripts/__tests__/validate.test.ts +++ b/scripts/__tests__/validate.test.ts @@ -6,6 +6,7 @@ import { ValidationErrorCode } from "../lib/error-codes.js"; import { loadYamlFileWithPositions } from "../lib/utils.js"; import { COLLECTION_SCHEMAS, + checkCompatibleWith, collectWarnings, detectSupersedeCycle, getErrorCodeFromZodIssue, @@ -602,12 +603,39 @@ platforms: }); }); +describe("checkCompatibleWith", () => { + const PACK = `name: Pack +manufacturer: acme +primaryCategory: preset-pack +compatibleWith: + - serum + - some-synth +`; + + it("returns null when every slug names a software or hardware file", () => { + const file = writeEntry("content", PACK); + expect(checkCompatibleWith(file, new Set(["serum"]), new Set(["some-synth"]))).toBeNull(); + expect( + checkCompatibleWith(writeEntry("content", HARDWARE_OK), new Set(), new Set()) + ).toBeNull(); + }); + + it("E206: an unknown slug is an error, keyed to its line", () => { + // `kontakt` for `native-instruments-kontakt` shipped while this was the + // advisory W123, and Studio lost the compatibility without a word. + const file = writeEntry("content", PACK); + const result = checkCompatibleWith(file, new Set(["serum"]), new Set()); + expect(result?.file).toBe(path.relative(process.cwd(), file)); + expect(result?.details?.map((d) => ({ code: d.code, path: d.path, line: d.line }))).toEqual([ + { code: "E206", path: "compatibleWith[1]", line: 6 }, + ]); + }); +}); + describe("collectWarnings", () => { function warn( yaml: string, ctx: { - software?: Set; - hardware?: Set; mfrUrls?: Map; mfrNames?: Map; } = {}, @@ -620,8 +648,6 @@ describe("collectWarnings", () => { data as WarningContext, document, lineCounter, - ctx.software, - ctx.hardware, ctx.mfrUrls, ctx.mfrNames ); @@ -678,23 +704,6 @@ describe("collectWarnings", () => { ); }); - it("W123: compatibleWith resolves against software or hardware slugs, when either set is given", () => { - const yaml = `name: Pack -manufacturer: acme -primaryCategory: preset-pack -compatibleWith: - - serum - - some-synth -`; - expect(warn(yaml, {}, "content")).toEqual([]); - expect( - warn(yaml, { software: new Set(["serum"]), hardware: new Set(["some-synth"]) }, "content") - ).toEqual([]); - expect(warn(yaml, { software: new Set(["serum"]) }, "content")).toEqual([ - { code: "W123", path: "compatibleWith[1]", line: 6 }, - ]); - }); - it("W124: a link repeating url or an earlier link", () => { const yaml = `${HARDWARE_OK}url: https://acme.example/widget links: diff --git a/scripts/dataset-audit.ts b/scripts/dataset-audit.ts index 370391855..d48e938e6 100644 --- a/scripts/dataset-audit.ts +++ b/scripts/dataset-audit.ts @@ -26,8 +26,8 @@ * model-number families ("Bassysm-F" vs "Bassysm-J") * that edit-distance matching drowns you in. * 2. broken-compatible — `compatibleWith` slug resolves to no software or - * hardware entry. (validate's W123 only covers - * software/hardware entries; content is checked here.) + * hardware entry. (validate fails on this as E206; + * kept here so the audit report stays complete.) * 3. orphan-manufacturer — a manufacturer referenced by zero products. * 4. thin-description — description present but suspiciously short. * 5. aggregator-url — canonical `url` points at an aggregator diff --git a/scripts/generate-context.ts b/scripts/generate-context.ts index 14cfe3fec..60424e379 100644 --- a/scripts/generate-context.ts +++ b/scripts/generate-context.ts @@ -233,7 +233,7 @@ Content entries (presets, sample packs, expansions) live in \`data/content/\` as - \`primaryCategory\` - Content category (e.g., \`preset\`, \`preset-pack\`, \`sample-pack\`, \`sound-library\`) - \`compatibleWith\` - Optional array of software slugs for host products (e.g., \`["serum", "omnisphere-2"]\`) - Does NOT have \`platforms\`, \`formats\`, or \`identifiers\` fields -- Advisory warning W123 fires if a \`compatibleWith\` slug doesn't match an existing software file +- \`pnpm validate\` fails (E206) if a \`compatibleWith\` slug doesn't match an existing software or hardware file ### Hardware - \`slug\` - URL-safe identifier diff --git a/scripts/lib/error-codes.ts b/scripts/lib/error-codes.ts index 048dd19c0..41df08b9e 100644 --- a/scripts/lib/error-codes.ts +++ b/scripts/lib/error-codes.ts @@ -56,6 +56,7 @@ export enum ValidationErrorCode { E205_DUPLICATE_CAPABILITY = "E205", E203_PARENT_COMPANY_NOT_FOUND = "E203", E204_IO_TRANSLATION_MISMATCH = "E204", + E206_UNKNOWN_COMPATIBLE_WITH = "E206", // Content errors (E3xx) E300_INVALID_MARKDOWN = "E300", @@ -72,7 +73,6 @@ export enum ValidationErrorCode { // Advisory warnings (W1xx) — non-blocking W120_UNKNOWN_IO_TYPE = "W120", W121_UNKNOWN_IO_CONNECTION = "W121", - W123_UNKNOWN_COMPATIBLE_WITH = "W123", W124_DUPLICATE_URL = "W124", W125_MANUFACTURER_URL_IN_LINKS = "W125", W126_SPECS_OVERLAP = "W126", @@ -274,6 +274,10 @@ const ERROR_INFO: Record = { title: "Duplicate capability", anchor: "e205-duplicate-capability", }, + [ValidationErrorCode.E206_UNKNOWN_COMPATIBLE_WITH]: { + title: "Unknown compatibleWith reference", + anchor: "e206-unknown-compatiblewith-reference", + }, // Content errors [ValidationErrorCode.E300_INVALID_MARKDOWN]: { @@ -320,10 +324,6 @@ const ERROR_INFO: Record = { title: "Unknown IO connection", anchor: "w121-unknown-io-connection", }, - [ValidationErrorCode.W123_UNKNOWN_COMPATIBLE_WITH]: { - title: "Unknown compatibleWith reference", - anchor: "w123-unknown-compatible-with", - }, [ValidationErrorCode.W124_DUPLICATE_URL]: { title: "Duplicate URL", anchor: "w124-duplicate-url", diff --git a/scripts/validate.ts b/scripts/validate.ts index c09e70a9e..2a920cc08 100644 --- a/scripts/validate.ts +++ b/scripts/validate.ts @@ -1461,6 +1461,43 @@ export function detectSupersedeCycle( return null; } +/** + * E206: every `compatibleWith` slug names a software or hardware file. + * An unknown slug builds and ships, and Studio drops the compatibility + * without a word (`kontakt` for `native-instruments-kontakt`). The check + * was the advisory W123, which is how that one reached `main`. + */ +export function checkCompatibleWith( + filePath: string, + softwareSlugs: Set, + hardwareSlugs: Set +): ValidationError | null { + const { data, document, lineCounter } = loadYamlFileWithPositions(filePath); + const compatibleWith = (data as { compatibleWith?: unknown })?.compatibleWith; + if (!Array.isArray(compatibleWith)) return null; + + const errorCode = ValidationErrorCode.E206_UNKNOWN_COMPATIBLE_WITH; + const details: ValidationErrorDetail[] = []; + for (let i = 0; i < compatibleWith.length; i++) { + const slug = compatibleWith[i]; + if (softwareSlugs.has(slug) || hardwareSlugs.has(slug)) continue; + const line = getLineForPath(document, lineCounter, ["compatibleWith", i]); + details.push({ + code: errorCode, + message: `Unknown compatibleWith reference '${slug}'. No matching software or hardware file found.`, + path: `compatibleWith[${i}]`, + line: line ?? undefined, + docsUrl: getDocsUrl(errorCode), + }); + } + if (details.length === 0) return null; + return { + file: path.relative(process.cwd(), filePath), + errors: details.map((d) => `${d.path}${d.line ? `:${d.line}` : ""}: ${d.message}`), + details, + }; +} + // ============================================================================= // ADVISORY WARNING COLLECTION // ============================================================================= @@ -1469,7 +1506,6 @@ export interface WarningContext { name?: string; primaryCategory?: string; io?: Array<{ name: string; type: string; connection: string; maxConnections?: number }>; - compatibleWith?: string[]; url?: string; links?: Array<{ url: string }>; manufacturer?: string; @@ -1491,8 +1527,6 @@ export function collectWarnings( data: WarningContext, document: ReturnType["document"], lineCounter: ReturnType["lineCounter"], - allSoftwareSlugs?: Set, - allHardwareSlugs?: Set, manufacturerUrlMap?: Map, manufacturerNameMap?: Map ): ValidationWarning | null { @@ -1574,24 +1608,6 @@ export function collectWarnings( } } - // Check compatibleWith references (advisory) — checks both software and hardware slugs - if (Array.isArray(data.compatibleWith) && (allSoftwareSlugs || allHardwareSlugs)) { - for (let i = 0; i < data.compatibleWith.length; i++) { - const slug = data.compatibleWith[i]; - const inSoftware = allSoftwareSlugs?.has(slug) ?? false; - const inHardware = allHardwareSlugs?.has(slug) ?? false; - if (!inSoftware && !inHardware) { - const line = getLineForPath(document, lineCounter, ["compatibleWith", i]); - warnings.push({ - code: ValidationErrorCode.W123_UNKNOWN_COMPATIBLE_WITH, - message: `Unknown compatibleWith reference '${slug}'. No matching software or hardware file found.`, - path: `compatibleWith[${i}]`, - line: line ?? undefined, - }); - } - } - } - // Check for duplicate URLs within the file if (Array.isArray(data.links)) { const seenUrls = new Set(); @@ -1865,13 +1881,13 @@ function validate(): ValidationResult { } } - // allSoftwareSlugs used for compatibleWith validation (software only, not content) + // allSoftwareSlugs used for compatibleWith validation (E206; software only, not content) const allSoftwareSlugs = new Set(); for (const file of softwareFiles) { allSoftwareSlugs.add(path.basename(file, path.extname(file))); } - // allHardwareSlugs used for compatibleWith validation (content can target hardware too) + // allHardwareSlugs used for compatibleWith validation (E206; content can target hardware too) const allHardwareSlugs = new Set(); for (const file of hardwareFiles) { allHardwareSlugs.add(path.basename(file, path.extname(file))); @@ -1971,7 +1987,9 @@ function validate(): ValidationResult { // Validate software (supersedes must reference valid software ID) for (const file of softwareFiles) { - const error = validateFile(file, SoftwareSchema, allManufacturers, softwareIds); + const error = + validateFile(file, SoftwareSchema, allManufacturers, softwareIds) ?? + checkCompatibleWith(file, allSoftwareSlugs, allHardwareSlugs); if (error) { errors.push(error); } else { @@ -1984,8 +2002,6 @@ function validate(): ValidationResult { data as WarningContext, document, lineCounter, - allSoftwareSlugs, - allHardwareSlugs, manufacturerUrlMap, manufacturerNameMap ); @@ -2010,12 +2026,14 @@ function validate(): ValidationResult { // Validate content (supersedes must reference valid content ID) for (const file of contentFiles) { - const error = validateFile(file, ContentSchema, allManufacturers, contentIds); + const error = + validateFile(file, ContentSchema, allManufacturers, contentIds) ?? + checkCompatibleWith(file, allSoftwareSlugs, allHardwareSlugs); if (error) { errors.push(error); } else { stats.content++; - // Collect advisory warnings for valid files (compatibleWith checks against software and hardware slugs) + // Collect advisory warnings for valid files try { const { data, document, lineCounter } = loadYamlFileWithPositions(file); const w = collectWarnings( @@ -2023,8 +2041,6 @@ function validate(): ValidationResult { data as WarningContext, document, lineCounter, - allSoftwareSlugs, - allHardwareSlugs, manufacturerUrlMap, manufacturerNameMap ); @@ -2050,8 +2066,6 @@ function validate(): ValidationResult { data as WarningContext, document, lineCounter, - undefined, - allHardwareSlugs, manufacturerUrlMap, undefined ); @@ -2077,8 +2091,6 @@ function validate(): ValidationResult { data as WarningContext, document, lineCounter, - undefined, - allHardwareSlugs, manufacturerUrlMap, undefined );