Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/compatible-with-e206.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions docs/VALIDATION_ERRORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion schema/CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 30 additions & 21 deletions scripts/__tests__/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ValidationErrorCode } from "../lib/error-codes.js";
import { loadYamlFileWithPositions } from "../lib/utils.js";
import {
COLLECTION_SCHEMAS,
checkCompatibleWith,
collectWarnings,
detectSupersedeCycle,
getErrorCodeFromZodIssue,
Expand Down Expand Up @@ -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<string>;
hardware?: Set<string>;
mfrUrls?: Map<string, string>;
mfrNames?: Map<string, string>;
} = {},
Expand All @@ -620,8 +648,6 @@ describe("collectWarnings", () => {
data as WarningContext,
document,
lineCounter,
ctx.software,
ctx.hardware,
ctx.mfrUrls,
ctx.mfrNames
);
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions scripts/dataset-audit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion scripts/generate-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions scripts/lib/error-codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -274,6 +274,10 @@ const ERROR_INFO: Record<ValidationErrorCode, ErrorInfoEntry> = {
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]: {
Expand Down Expand Up @@ -320,10 +324,6 @@ const ERROR_INFO: Record<ValidationErrorCode, ErrorInfoEntry> = {
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",
Expand Down
80 changes: 46 additions & 34 deletions scripts/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>,
hardwareSlugs: Set<string>
): 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
// =============================================================================
Expand All @@ -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;
Expand All @@ -1491,8 +1527,6 @@ export function collectWarnings(
data: WarningContext,
document: ReturnType<typeof loadYamlFileWithPositions>["document"],
lineCounter: ReturnType<typeof loadYamlFileWithPositions>["lineCounter"],
allSoftwareSlugs?: Set<string>,
allHardwareSlugs?: Set<string>,
manufacturerUrlMap?: Map<string, string>,
manufacturerNameMap?: Map<string, string>
): ValidationWarning | null {
Expand Down Expand Up @@ -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<string>();
Expand Down Expand Up @@ -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<string>();
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<string>();
for (const file of hardwareFiles) {
allHardwareSlugs.add(path.basename(file, path.extname(file)));
Expand Down Expand Up @@ -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 {
Expand All @@ -1984,8 +2002,6 @@ function validate(): ValidationResult {
data as WarningContext,
document,
lineCounter,
allSoftwareSlugs,
allHardwareSlugs,
manufacturerUrlMap,
manufacturerNameMap
);
Expand All @@ -2010,21 +2026,21 @@ 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(
file,
data as WarningContext,
document,
lineCounter,
allSoftwareSlugs,
allHardwareSlugs,
manufacturerUrlMap,
manufacturerNameMap
);
Expand All @@ -2050,8 +2066,6 @@ function validate(): ValidationResult {
data as WarningContext,
document,
lineCounter,
undefined,
allHardwareSlugs,
manufacturerUrlMap,
undefined
);
Expand All @@ -2077,8 +2091,6 @@ function validate(): ValidationResult {
data as WarningContext,
document,
lineCounter,
undefined,
allHardwareSlugs,
manufacturerUrlMap,
undefined
);
Expand Down
Loading