Fix VOLUMES/TIMBRE/FM placement: variation properties, not instrument-level

Per RFC §3.2.1.3, VOLUMES, TIMBRE and FM are variation properties. The
previous exporter incorrectly appended them to the first ATTR variation;
they now go into a synthetic root variation (no ATTR) as required.
variationLines() handles these fields directly.

Fix test-parser.mjs fixture paths for standalone execution from repo root.
This commit is contained in:
c0dev0id 2026-06-22 19:30:45 +02:00
parent 17f4529658
commit f318b155cc
2 changed files with 30 additions and 30 deletions

View File

@ -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<VARIATION> 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;
}

View File

@ -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.