From 4e4060697d359aa76c104683f9dd7a2285c0be43 Mon Sep 17 00:00:00 2001 From: Jeffrey Louden Date: Fri, 10 Jul 2026 07:58:30 -0400 Subject: [PATCH 1/4] Add IO position enrichment tool + backfill Eventide H90 (#212) Starts issue #212 (rowPosition/columnPosition on IO entries). - Add `pnpm enrich-io ` interactive tool to assign columnPosition (left-to-right) and rowPosition (top-to-bottom) to a hardware entry's IO ports, writing back to YAML with formatting preserved. - Document the ordering convention in schema/CONTEXT.md (via generate-context.ts) and CLAUDE.md, including "one io entry per physical jack". - Fully correct the Eventide H90 as the validation case: all 14 discrete rear-panel jacks with column/row grid positions (2x2 audio in/out grids), replacing an incomplete 5-entry list. Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/io-positions-enrichment.md | 20 ++ CLAUDE.md | 14 ++ .../eventide-audio-h90-harmonizer.yaml | 146 ++++++++++-- package.json | 1 + schema/CONTEXT.md | 15 ++ scripts/enrich-io.ts | 225 ++++++++++++++++++ scripts/generate-context.ts | 15 ++ 7 files changed, 421 insertions(+), 15 deletions(-) create mode 100644 .changeset/io-positions-enrichment.md create mode 100644 scripts/enrich-io.ts diff --git a/.changeset/io-positions-enrichment.md b/.changeset/io-positions-enrichment.md new file mode 100644 index 000000000..9bd62efc5 --- /dev/null +++ b/.changeset/io-positions-enrichment.md @@ -0,0 +1,20 @@ +--- +"catalog": minor +--- + +Add IO port positioning (`columnPosition` / `rowPosition`) enrichment. + +Starts issue #212: IO ports can now carry `columnPosition` (left-to-right) and +`rowPosition` (top-to-bottom) values describing their spatial arrangement on a +device edge, so the setup graph can render port layouts accurately. The schema +already accepted these fields; this adds the convention, tooling, and first data. + +- New `pnpm enrich-io ` interactive tool to assign column/row positions to a + hardware entry's IO ports, writing back to YAML while preserving formatting. +- Document the ordering convention in `schema/CONTEXT.md` (via the generator) and + `CLAUDE.md`. +- Fully correct and backfill the Eventide H90 Harmonizer as the validation case: + its IO now lists all 14 discrete rear-panel jacks (Inputs 1–4, Outputs 1–4, + Exp/Ctl 1–2, MIDI In, MIDI Out/Thru, USB-C, Power) with column/row positions, + replacing an incomplete 5-entry list that omitted the audio I/O and MIDI Out and + mis-positioned the jacks. diff --git a/CLAUDE.md b/CLAUDE.md index 129df9f07..3003389c2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -117,6 +117,20 @@ preamp/mixer outputs; a powered speaker input is never `line`. (guitars/basses), `position`. Every product entry must include `primaryCategory`. These are enforced by `pnpm validate`, not just review. +**Port ordering (optional):** `columnPosition` and `rowPosition` place a port +within its `position` edge so the setup graph can render the layout. Both are +1-based, numbered independently per edge, and go right after `position`: +`columnPosition` is left-to-right (column 1 = leftmost, viewing that face +head-on); `rowPosition` is top-to-bottom (row 1 = topmost). Stacked ports share a +`columnPosition` and differ by `rowPosition`; a single-row edge uses +`rowPosition: 1`. This is visual info from photos/manuals — assign it with +`pnpm enrich-io `, not during bulk import. + +One `io` entry per **physical** jack (`maxConnections: 1`). Do not collapse an L/R +pair into one stereo entry with `maxConnections: 2`, and do not split one jack in +two — bulk imports get this wrong. E.g. the Eventide H90 has 14 discrete jacks +(Inputs 1–4, Outputs 1–4, Exp/Ctl 1–2, MIDI In, MIDI Out/Thru, USB-C, Power). + ## Content Entries Content entries (presets, sample packs, expansions) live in `data/content/` as a separate collection. Content `primaryCategory` values include `preset`, `preset-pack`, `sample-pack`, `drum-sample-pack`, `loop-pack`, `sound-library`, `soundfont`, `impulse-response`, and `multisample`. diff --git a/data/hardware/eventide-audio-h90-harmonizer.yaml b/data/hardware/eventide-audio-h90-harmonizer.yaml index 8778771d9..6e9ac674a 100644 --- a/data/hardware/eventide-audio-h90-harmonizer.yaml +++ b/data/hardware/eventide-audio-h90-harmonizer.yaml @@ -7,41 +7,157 @@ description: A next-generation multi-effects powerhouse with 74 algorithms and comprehensive I/O, designed for guitar, bass, synths, and more, offering flexible routing and extensive sound possibilities. io: - - name: Expression Input 1 + # All jacks are on the rear (Top) edge. Inputs and Outputs are arranged in + # 2x2 grids (columns left-to-right, rows top-to-bottom); the remaining jacks + # form a single row to their right. See the H90 Quick Reference Guide. + - name: Input 1 signalFlow: input category: audio - type: cv/gate + type: instrument connection: 1/4-inch + connectorDetail: + - TS maxConnections: 1 - position: Bottom - - name: Expression Input 2 + position: Top + columnPosition: 1 + rowPosition: 1 + - name: Input 2 signalFlow: input category: audio - type: cv/gate + type: instrument connection: 1/4-inch + connectorDetail: + - TS maxConnections: 1 - position: Bottom - - name: MIDI in + position: Top + columnPosition: 1 + rowPosition: 2 + - name: Input 3 (Insert Return / Dual Path 2) signalFlow: input - category: midi - type: midi - connection: 5-pin din + category: audio + type: line + connection: 1/4-inch + connectorDetail: + - TS maxConnections: 1 - position: Bottom - - name: USB Control + position: Top + columnPosition: 2 + rowPosition: 1 + - name: Input 4 (Insert Return / Dual Path 2) signalFlow: input + category: audio + type: line + connection: 1/4-inch + connectorDetail: + - TS + maxConnections: 1 + position: Top + columnPosition: 2 + rowPosition: 2 + - name: Output 1 + signalFlow: output + category: audio + type: line + connection: 1/4-inch + connectorDetail: + - TS + maxConnections: 1 + position: Top + columnPosition: 3 + rowPosition: 1 + - name: Output 2 + signalFlow: output + category: audio + type: line + connection: 1/4-inch + connectorDetail: + - TS + maxConnections: 1 + position: Top + columnPosition: 3 + rowPosition: 2 + - name: Output 3 (Insert Send / Dual Path 2) + signalFlow: output + category: audio + type: line + connection: 1/4-inch + connectorDetail: + - TS + maxConnections: 1 + position: Top + columnPosition: 4 + rowPosition: 1 + - name: Output 4 (Insert Send / Dual Path 2) + signalFlow: output + category: audio + type: line + connection: 1/4-inch + connectorDetail: + - TS + maxConnections: 1 + position: Top + columnPosition: 4 + rowPosition: 2 + - name: Exp/Ctl 1 + signalFlow: input + category: audio + type: expression + connection: 1/4-inch + connectorDetail: + - TRS + maxConnections: 1 + position: Top + columnPosition: 5 + rowPosition: 1 + - name: Exp/Ctl 2 + signalFlow: input + category: audio + type: expression + connection: 1/4-inch + connectorDetail: + - TRS + maxConnections: 1 + position: Top + columnPosition: 5 + rowPosition: 2 + - name: USB-C + signalFlow: bidirectional category: digital type: usb connection: usb-c maxConnections: 1 - position: Bottom - - name: Power Input + position: Top + columnPosition: 6 + rowPosition: 1 + - name: Power signalFlow: input category: power type: power connection: barrel + connectorDetail: + - center-positive + maxConnections: 1 + position: Top + columnPosition: 7 + rowPosition: 1 + - name: MIDI In + signalFlow: input + category: midi + type: midi + connection: 5-pin din + maxConnections: 1 + position: Top + columnPosition: 8 + rowPosition: 1 + - name: MIDI Out/Thru + signalFlow: output + category: midi + type: midi + connection: 5-pin din maxConnections: 1 - position: Bottom + position: Top + columnPosition: 9 + rowPosition: 1 specs: |- - 74 professional algorithms - Dual algorithm design diff --git a/package.json b/package.json index 531590ac3..13cc4954d 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "version": "changeset version", "release": "changeset publish", "assign-ids": "tsx scripts/assign-ids.ts", + "enrich-io": "tsx scripts/enrich-io.ts", "format": "tsx scripts/format-yaml.ts", "format:check": "prettier --check \"data/**/*.yaml\"", "validate-urls": "tsx scripts/validate-urls.ts", diff --git a/schema/CONTEXT.md b/schema/CONTEXT.md index adebec1d3..1029cb0d3 100644 --- a/schema/CONTEXT.md +++ b/schema/CONTEXT.md @@ -782,6 +782,21 @@ Accessory entries (cables, stands, acoustic treatment) live in `data/accessories **Aliases:** +### Port Ordering: columnPosition / rowPosition (optional) + +`columnPosition` and `rowPosition` describe the spatial arrangement of ports +**within a single `position` edge**, so the setup graph can render port layouts +accurately. Both are optional 1-based integers; numbering is independent per edge. + +- `columnPosition`: left-to-right order, viewing that face head-on. Column 1 = leftmost. +- `rowPosition`: top-to-bottom (front-to-back) order. Row 1 = topmost / nearest the front. +- Ports stacked vertically share a `columnPosition` and differ by `rowPosition`. + A single-row edge uses `rowPosition: 1` on every port. +- Place `columnPosition`/`rowPosition` immediately after `position`. + +These are visual/spatial values best read off product photos or the manufacturer's +rear-panel diagram — assign them with `pnpm enrich-io `, not during bulk import. + ### IO Types (advisory — unknown values produce warnings) - `line` diff --git a/scripts/enrich-io.ts b/scripts/enrich-io.ts new file mode 100644 index 000000000..597944e23 --- /dev/null +++ b/scripts/enrich-io.ts @@ -0,0 +1,225 @@ +/** + * Interactive IO Position Enrichment + * + * Assigns `columnPosition` / `rowPosition` to a hardware entry's IO ports so the + * Studio setup graph can render the physical port layout. See issue #212. + * + * These values are visual/spatial information best read off product photos or the + * manufacturer's rear-panel diagram, so this is a manual enrichment pass rather + * than part of bulk import. + * + * Conventions (also documented in schema/CONTEXT.md): + * - columnPosition: 1-based left-to-right order of ports on a given `position` + * edge, viewing that face head-on. Column 1 = leftmost. + * - rowPosition: 1-based top-to-bottom (front-to-back) order on that edge. + * Row 1 = topmost / nearest the front. Single-row edges use rowPosition 1. + * - Numbering is independent per `position` edge. + * + * Usage: + * pnpm enrich-io [--dry-run] [--open] + * + * hardware filename without .yaml (e.g. eventide-audio-h90-harmonizer) + * --dry-run print the changes without writing the file + * --open open the product URL in a browser (macOS) to view port photos + * + * At each prompt: enter a positive integer, press Enter to keep the current + * value, or type `-` to clear the value. + */ + +import { execFile } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; +import { stdin as input, stdout as output } from "node:process"; +import readline from "node:readline/promises"; +import { isMap, isSeq, type Pair, parseDocument, type Scalar, type YAMLMap } from "yaml"; +import { DATA_DIR } from "./lib/utils.js"; + +interface IOPortView { + name?: string; + signalFlow?: string; + position?: string; + columnPosition?: number; + rowPosition?: number; +} + +interface ChangeRecord { + name: string; + field: "columnPosition" | "rowPosition"; + from: number | undefined; + to: number | undefined; +} + +/** Resolve the string value of a Pair's key. */ +function pairKey(pair: Pair): string | undefined { + const key = pair.key as Scalar | string | null | undefined; + if (key && typeof key === "object" && "value" in key) { + return key.value == null ? undefined : String(key.value); + } + return key == null ? undefined : String(key); +} + +/** + * Set (or clear) a numeric field on a port map, keeping it in canonical order: + * the field is moved to immediately after the first present key in `afterKeys`. + * Returns true if the document was modified. + */ +function applyField( + portNode: YAMLMap, + key: string, + newValue: number | undefined, + afterKeys: string[] +): boolean { + const items = portNode.items; + const currentIdx = items.findIndex((it) => pairKey(it) === key); + const existing = currentIdx >= 0 ? portNode.get(key) : undefined; + + if (newValue === undefined) { + if (currentIdx >= 0) { + portNode.delete(key); + return true; + } + return false; + } + + if (existing === newValue) return false; + + portNode.set(key, newValue); + + // Re-locate the (possibly newly-appended) key to just after its anchor. + const idx = items.findIndex((it) => pairKey(it) === key); + const [item] = items.splice(idx, 1); + let insertAt = 0; + for (const anchor of afterKeys) { + const anchorIdx = items.findIndex((it) => pairKey(it) === anchor); + if (anchorIdx >= 0) { + insertAt = anchorIdx + 1; + break; + } + } + items.splice(insertAt, 0, item); + return true; +} + +/** + * Prompt for a positive integer; Enter keeps current, `-` clears, EOF keeps. + * + * Reads via `nextLine` (backed by readline's async iterator) rather than + * `rl.question()` so piped / non-TTY input is consumed line-by-line without + * dropping lines. + */ +async function askNumber( + nextLine: () => Promise, + label: string, + current: number | undefined +): Promise { + output.write(` ${label} [${current ?? "-"}]: `); + const raw = await nextLine(); + if (raw === undefined) { + output.write("\n"); + return current; // EOF — keep existing value + } + const answer = raw.trim(); + if (answer === "") return current; + if (answer === "-" || answer.toLowerCase() === "none") return undefined; + const n = Number.parseInt(answer, 10); + if (!Number.isInteger(n) || n < 1 || String(n) !== answer) { + console.log(" ! must be a positive integer — keeping current value"); + return current; + } + return n; +} + +async function main(): Promise { + const args = process.argv.slice(2); + const dryRun = args.includes("--dry-run"); + const openImages = args.includes("--open"); + const slugArg = args.find((a) => !a.startsWith("--")); + + if (!slugArg) { + console.error("Usage: pnpm enrich-io [--dry-run] [--open]"); + process.exit(1); + } + + const slug = slugArg.replace(/\.ya?ml$/, ""); + const file = path.join(DATA_DIR, "hardware", `${slug}.yaml`); + if (!fs.existsSync(file)) { + console.error(`No hardware entry found at data/hardware/${slug}.yaml`); + process.exit(1); + } + + const doc = parseDocument(fs.readFileSync(file, "utf-8")); + const ioSeq = doc.get("io"); + if (!isSeq(ioSeq) || ioSeq.items.length === 0) { + console.error(`${slug} has no 'io' entries to enrich.`); + process.exit(1); + } + + const data = doc.toJSON() as { name?: string; url?: string }; + console.log(`\n${data.name ?? slug}`); + if (data.url) { + console.log(` ${data.url}`); + if (openImages && process.platform === "darwin") { + execFile("open", [data.url], () => {}); + } + } + console.log( + " Assign columnPosition (left→right) and rowPosition (top→bottom) per edge.\n" + + " Enter a number, press Enter to keep, or type '-' to clear.\n" + ); + + const rl = readline.createInterface({ input, output }); + const lineIterator = rl[Symbol.asyncIterator](); + const nextLine = async (): Promise => { + const { value, done } = await lineIterator.next(); + return done ? undefined : (value as string); + }; + const changes: ChangeRecord[] = []; + + for (let i = 0; i < ioSeq.items.length; i++) { + const portNode = ioSeq.items[i]; + if (!isMap(portNode)) continue; + const port = portNode.toJSON() as IOPortView; + const name = port.name ?? `port ${i + 1}`; + + console.log( + `[${i + 1}/${ioSeq.items.length}] ${name} — ${port.signalFlow ?? "?"}, ` + + `position: ${port.position ?? "(none)"}` + ); + + const col = await askNumber(nextLine, "columnPosition", port.columnPosition); + const row = await askNumber(nextLine, "rowPosition", port.rowPosition); + + if (applyField(portNode, "columnPosition", col, ["position"])) { + changes.push({ name, field: "columnPosition", from: port.columnPosition, to: col }); + } + if (applyField(portNode, "rowPosition", row, ["columnPosition", "position"])) { + changes.push({ name, field: "rowPosition", from: port.rowPosition, to: row }); + } + } + + rl.close(); + + if (changes.length === 0) { + console.log("\nNo changes."); + return; + } + + console.log(`\n${changes.length} change(s):`); + for (const c of changes) { + console.log(` ${c.name}: ${c.field} ${c.from ?? "-"} → ${c.to ?? "-"}`); + } + + if (dryRun) { + console.log("\n(dry run — file not written)"); + return; + } + + fs.writeFileSync(file, doc.toString()); + console.log(`\n✓ Wrote data/hardware/${slug}.yaml`); + console.log(" Next: run 'pnpm format' then 'pnpm validate'."); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/generate-context.ts b/scripts/generate-context.ts index 9f1c438a0..b59b4df56 100644 --- a/scripts/generate-context.ts +++ b/scripts/generate-context.ts @@ -273,6 +273,21 @@ ${Object.entries(context.ioPositionAliases) .map(([alias, canonical]) => `- \`${alias}\` → \`${canonical}\``) .join("\n")} +### Port Ordering: columnPosition / rowPosition (optional) + +\`columnPosition\` and \`rowPosition\` describe the spatial arrangement of ports +**within a single \`position\` edge**, so the setup graph can render port layouts +accurately. Both are optional 1-based integers; numbering is independent per edge. + +- \`columnPosition\`: left-to-right order, viewing that face head-on. Column 1 = leftmost. +- \`rowPosition\`: top-to-bottom (front-to-back) order. Row 1 = topmost / nearest the front. +- Ports stacked vertically share a \`columnPosition\` and differ by \`rowPosition\`. + A single-row edge uses \`rowPosition: 1\` on every port. +- Place \`columnPosition\`/\`rowPosition\` immediately after \`position\`. + +These are visual/spatial values best read off product photos or the manufacturer's +rear-panel diagram — assign them with \`pnpm enrich-io \`, not during bulk import. + ### IO Types (advisory — unknown values produce warnings) ${context.ioTypes.map((v) => `- \`${v}\``).join("\n")} From 3afa8eed9aaf251ea561c14aff3564742a0e8b80 Mon Sep 17 00:00:00 2001 From: Jeffrey Louden Date: Fri, 10 Jul 2026 17:17:25 -0400 Subject: [PATCH 2/4] H90: model audio I/O level as software-switchable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The H90's inputs and outputs can each be toggled between instrument and line level in software (indicated by the rear-panel "Line Lvl" LEDs). Normalize all four inputs to `type: instrument` and all four outputs to `type: line` as the representative defaults, and add a per-port description noting the level is switchable. Also describe the insert/dual-path role of Inputs/Outputs 3–4. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../eventide-audio-h90-harmonizer.yaml | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/data/hardware/eventide-audio-h90-harmonizer.yaml b/data/hardware/eventide-audio-h90-harmonizer.yaml index 6e9ac674a..548532f23 100644 --- a/data/hardware/eventide-audio-h90-harmonizer.yaml +++ b/data/hardware/eventide-audio-h90-harmonizer.yaml @@ -10,6 +10,10 @@ io: # All jacks are on the rear (Top) edge. Inputs and Outputs are arranged in # 2x2 grids (columns left-to-right, rows top-to-bottom); the remaining jacks # form a single row to their right. See the H90 Quick Reference Guide. + # Audio jacks are 1/4-inch TS; each in/out can be toggled between instrument + # and line level in the H90 software (the rear-panel "Line Lvl" LEDs indicate + # the current setting). `type` records the typical default; the toggle is noted + # in each port's description. - name: Input 1 signalFlow: input category: audio @@ -21,6 +25,7 @@ io: position: Top columnPosition: 1 rowPosition: 1 + description: Instrument or line level, switchable in software. - name: Input 2 signalFlow: input category: audio @@ -32,10 +37,11 @@ io: position: Top columnPosition: 1 rowPosition: 2 + description: Instrument or line level, switchable in software. - name: Input 3 (Insert Return / Dual Path 2) signalFlow: input category: audio - type: line + type: instrument connection: 1/4-inch connectorDetail: - TS @@ -43,10 +49,12 @@ io: position: Top columnPosition: 2 rowPosition: 1 + description: Insert return or Dual Path 2 input. Instrument or line level, + switchable in software. - name: Input 4 (Insert Return / Dual Path 2) signalFlow: input category: audio - type: line + type: instrument connection: 1/4-inch connectorDetail: - TS @@ -54,6 +62,8 @@ io: position: Top columnPosition: 2 rowPosition: 2 + description: Insert return or Dual Path 2 input. Instrument or line level, + switchable in software. - name: Output 1 signalFlow: output category: audio @@ -65,6 +75,7 @@ io: position: Top columnPosition: 3 rowPosition: 1 + description: Instrument or line level, switchable in software. - name: Output 2 signalFlow: output category: audio @@ -76,6 +87,7 @@ io: position: Top columnPosition: 3 rowPosition: 2 + description: Instrument or line level, switchable in software. - name: Output 3 (Insert Send / Dual Path 2) signalFlow: output category: audio @@ -87,6 +99,8 @@ io: position: Top columnPosition: 4 rowPosition: 1 + description: Insert send or Dual Path 2 output. Instrument or line level, + switchable in software. - name: Output 4 (Insert Send / Dual Path 2) signalFlow: output category: audio @@ -98,6 +112,8 @@ io: position: Top columnPosition: 4 rowPosition: 2 + description: Insert send or Dual Path 2 output. Instrument or line level, + switchable in software. - name: Exp/Ctl 1 signalFlow: input category: audio From 2bf01d58e68fcad6dedc925b143a7dd570a1c2df Mon Sep 17 00:00:00 2001 From: Jeffrey Louden Date: Fri, 10 Jul 2026 17:39:38 -0400 Subject: [PATCH 3/4] Address review: validate enrich-io slug, clarify rowPosition axis - enrich-io.ts: reject slugs that aren't the documented safe format (SLUG_PATTERN) before path.join, preventing path traversal. - Docs: define rowPosition with a single unambiguous axis (top-to-bottom as viewed head-on), dropping the contradictory "front-to-back" wording, in generate-context.ts / CONTEXT.md and the enrich-io.ts header. Co-Authored-By: Claude Opus 4.8 (1M context) --- schema/CONTEXT.md | 2 +- scripts/enrich-io.ts | 12 ++++++++++-- scripts/generate-context.ts | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/schema/CONTEXT.md b/schema/CONTEXT.md index 1029cb0d3..609c68a9b 100644 --- a/schema/CONTEXT.md +++ b/schema/CONTEXT.md @@ -789,7 +789,7 @@ Accessory entries (cables, stands, acoustic treatment) live in `data/accessories accurately. Both are optional 1-based integers; numbering is independent per edge. - `columnPosition`: left-to-right order, viewing that face head-on. Column 1 = leftmost. -- `rowPosition`: top-to-bottom (front-to-back) order. Row 1 = topmost / nearest the front. +- `rowPosition`: top-to-bottom order, viewing that face head-on. Row 1 = topmost. - Ports stacked vertically share a `columnPosition` and differ by `rowPosition`. A single-row edge uses `rowPosition: 1` on every port. - Place `columnPosition`/`rowPosition` immediately after `position`. diff --git a/scripts/enrich-io.ts b/scripts/enrich-io.ts index 597944e23..0248889a5 100644 --- a/scripts/enrich-io.ts +++ b/scripts/enrich-io.ts @@ -11,8 +11,8 @@ * Conventions (also documented in schema/CONTEXT.md): * - columnPosition: 1-based left-to-right order of ports on a given `position` * edge, viewing that face head-on. Column 1 = leftmost. - * - rowPosition: 1-based top-to-bottom (front-to-back) order on that edge. - * Row 1 = topmost / nearest the front. Single-row edges use rowPosition 1. + * - rowPosition: 1-based top-to-bottom order, viewing that face head-on. + * Row 1 = topmost. Single-row edges use rowPosition 1. * - Numbering is independent per `position` edge. * * Usage: @@ -32,6 +32,7 @@ import path from "node:path"; import { stdin as input, stdout as output } from "node:process"; import readline from "node:readline/promises"; import { isMap, isSeq, type Pair, parseDocument, type Scalar, type YAMLMap } from "yaml"; +import { SLUG_PATTERN } from "./lib/schema-loader.js"; import { DATA_DIR } from "./lib/utils.js"; interface IOPortView { @@ -141,6 +142,13 @@ async function main(): Promise { } const slug = slugArg.replace(/\.ya?ml$/, ""); + if (!SLUG_PATTERN.test(slug)) { + console.error( + `Invalid slug '${slug}'. Slugs are lowercase letters, numbers, and hyphens ` + + "(no path separators)." + ); + process.exit(1); + } const file = path.join(DATA_DIR, "hardware", `${slug}.yaml`); if (!fs.existsSync(file)) { console.error(`No hardware entry found at data/hardware/${slug}.yaml`); diff --git a/scripts/generate-context.ts b/scripts/generate-context.ts index b59b4df56..5ee30c776 100644 --- a/scripts/generate-context.ts +++ b/scripts/generate-context.ts @@ -280,7 +280,7 @@ ${Object.entries(context.ioPositionAliases) accurately. Both are optional 1-based integers; numbering is independent per edge. - \`columnPosition\`: left-to-right order, viewing that face head-on. Column 1 = leftmost. -- \`rowPosition\`: top-to-bottom (front-to-back) order. Row 1 = topmost / nearest the front. +- \`rowPosition\`: top-to-bottom order, viewing that face head-on. Row 1 = topmost. - Ports stacked vertically share a \`columnPosition\` and differ by \`rowPosition\`. A single-row edge uses \`rowPosition: 1\` on every port. - Place \`columnPosition\`/\`rowPosition\` immediately after \`position\`. From 5c7a479f74efd11c4af51d327fff6136ef7b9a15 Mon Sep 17 00:00:00 2001 From: Jeffrey Louden Date: Fri, 10 Jul 2026 17:53:46 -0400 Subject: [PATCH 4/4] H90: MIDI In/Out on the Left side, not the rear Per confirmation, the H90's MIDI In and Out/Thru DIN jacks are on the left side of the pedal, not the rear (Top) edge with the audio I/O. Move both to position: Left as their own edge (columns 1-2, row 1) and update the io layout comment. Co-Authored-By: Claude Opus 4.8 (1M context) --- data/hardware/eventide-audio-h90-harmonizer.yaml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/data/hardware/eventide-audio-h90-harmonizer.yaml b/data/hardware/eventide-audio-h90-harmonizer.yaml index 548532f23..84442e9e0 100644 --- a/data/hardware/eventide-audio-h90-harmonizer.yaml +++ b/data/hardware/eventide-audio-h90-harmonizer.yaml @@ -7,9 +7,10 @@ description: A next-generation multi-effects powerhouse with 74 algorithms and comprehensive I/O, designed for guitar, bass, synths, and more, offering flexible routing and extensive sound possibilities. io: - # All jacks are on the rear (Top) edge. Inputs and Outputs are arranged in - # 2x2 grids (columns left-to-right, rows top-to-bottom); the remaining jacks - # form a single row to their right. See the H90 Quick Reference Guide. + # Most jacks are on the rear (Top) edge: Inputs and Outputs in 2x2 grids + # (columns left-to-right, rows top-to-bottom), then Exp/Ctl, USB-C, and Power + # in a single row to their right. The MIDI In and Out/Thru jacks are on the + # Left side of the pedal. See the H90 Quick Reference Guide. # Audio jacks are 1/4-inch TS; each in/out can be toggled between instrument # and line level in the H90 software (the rear-panel "Line Lvl" LEDs indicate # the current setting). `type` records the typical default; the toggle is noted @@ -162,8 +163,8 @@ io: type: midi connection: 5-pin din maxConnections: 1 - position: Top - columnPosition: 8 + position: Left + columnPosition: 1 rowPosition: 1 - name: MIDI Out/Thru signalFlow: output @@ -171,8 +172,8 @@ io: type: midi connection: 5-pin din maxConnections: 1 - position: Top - columnPosition: 9 + position: Left + columnPosition: 2 rowPosition: 1 specs: |- - 74 professional algorithms