diff --git a/static/exporter.js b/static/exporter.js index 4227e16..ee1f09b 100644 --- a/static/exporter.js +++ b/static/exporter.js @@ -62,6 +62,7 @@ function labelSpecLines(ls) { // ── Variation ───────────────────────────────────────────────────────────── // Returns YAML lines for one variation MAPPING (no leading "- "). +// RFC §3.2.1.3: VOLUMES, TIMBRE are variation properties, not instrument-level. function variationLines(v) { const lines = []; @@ -70,25 +71,34 @@ function variationLines(v) { for (const ls of (v.labelSpecs ?? [])) lines.push(...labelSpecLines(ls)); if (v.spread?.length) lines.push(`SPREAD: [${v.spread.join(', ')}]`); if (v.railsbackCurve) { const rc = serializeShape(v.railsbackCurve); if (rc) lines.push(`RAILSBACK_CURVE: "${rc}"`); } + const vol = serializeShape(v.volumes); + if (vol) lines.push(`VOLUMES: "${vol}"`); + const timbre = serializeShape(v.timbre); + if (timbre) lines.push(`TIMBRE: "${timbre}"`); + for (const fm of (v.fmModulations ?? [])) lines.push(`FM: "${serializeFm(fm)}"`); for (const sv of (v.subvariations ?? [])) lines.push(...variationLines(sv)); return lines; } // ── Instrument character block ───────────────────────────────────────────── -// VOLUMES / TIMBRE / FM appear at depth 01 (direct instrument children per RFC §3.2.1.3). -// RAILSBACK_CURVE is depth 02 (inside variation) and is emitted by variationLines(). +// VOLUMES, TIMBRE, FM are variation properties (RFC §3.2.1.3). The AST parser +// stores them on the instrument because they appear at depth 01 (root variation +// is implicit when no character: wrapper exists). Promote them into a synthetic +// root variation here so the export structure is RFC-correct. function instrCharacterLines(instr) { - const extraLines = []; - const vol = serializeShape(instr.volumes); - if (vol) extraLines.push(`VOLUMES: "${vol}"`); - const timbre = serializeShape(instr.timbre); - if (timbre) extraLines.push(`TIMBRE: "${timbre}"`); - for (const fm of (instr.fmModulations ?? [])) extraLines.push(`FM: "${serializeFm(fm)}"`); - const variations = instr.variations ?? []; - const syntheticRoot = instr.basicProperties - ? { basicProperties: instr.basicProperties, labelSpecs: [], subvariations: [], spread: null, dependsOn: null } + const hasRootProps = instr.basicProperties || instr.volumes || instr.timbre || + (instr.fmModulations ?? []).length > 0; + const syntheticRoot = hasRootProps + ? { + basicProperties: instr.basicProperties, + labelSpecs: [], subvariations: [], spread: null, + dependsOn: null, railsbackCurve: null, + volumes: instr.volumes, + timbre: instr.timbre, + fmModulations: instr.fmModulations ?? [], + } : null; const allVariations = [ @@ -97,21 +107,17 @@ function instrCharacterLines(instr) { ]; if (allVariations.length <= 1) { - // Single variation — emit as MAPPING directly under character: - const vLines = allVariations.length - ? [...variationLines(allVariations[0]), ...extraLines] - : extraLines; + const vLines = allVariations.length ? variationLines(allVariations[0]) : []; return vLines.map(l => ` ${l}`); } // Multiple variations — RFC MAYBE_LIST as YAML sequence. const result = []; - for (let i = 0; i < allVariations.length; i++) { - const vLines = variationLines(allVariations[i]); - const allLines = i === 0 ? [...vLines, ...extraLines] : vLines; - if (!allLines.length) continue; - result.push(` - ${allLines[0]}`); - for (const l of allLines.slice(1)) result.push(` ${l}`); + for (const v of allVariations) { + const vLines = variationLines(v); + if (!vLines.length) continue; + result.push(` - ${vLines[0]}`); + for (const l of vLines.slice(1)) result.push(` ${l}`); } return result; } diff --git a/test-parser.mjs b/test-parser.mjs index 9f612f6..babfaef 100644 --- a/test-parser.mjs +++ b/test-parser.mjs @@ -1,15 +1,12 @@ #!/usr/bin/env node // Fixture-based compliance test for ast-parser.js + exporter.js -// Run: node score_editors/vue3_neusik/test-parser.mjs +// Run: node test-parser.mjs import { readFileSync } from 'fs'; import { parseAstLog, buildModel } from './static/ast-parser.js'; import { exportInstrument, patchScore } from './static/exporter.js'; -const FIXTURE = new URL( - '../../PLAN/vue3js-app-proposal-for-sdk-claude/fixtures/ast.log', - import.meta.url -); +const FIXTURE = new URL('./fixtures/ast.log', import.meta.url); const text = readFileSync(FIXTURE, 'utf8'); let pass = 0, fail = 0; @@ -191,10 +188,7 @@ if (alpha) { // ── patchScore ───────────────────────────────────────────────────────────── section('patchScore'); -const SCORE_FIXTURE = new URL( - '../../PLAN/vue3js-app-proposal-for-sdk-claude/fixtures/pathetique.spls', - import.meta.url -); +const SCORE_FIXTURE = new URL('./fixtures/pathetique.spls', import.meta.url); const rawScore = readFileSync(SCORE_FIXTURE, 'utf8'); // pathetique.spls contains alpha and ki; dev/piano is a linked instrument not embedded.