Skip to content

Add typed TS SDK with C++ parity checks#2

Open
fcastrovilli wants to merge 1 commit into
mainfrom
t3code/typed-js-output-strategy
Open

Add typed TS SDK with C++ parity checks#2
fcastrovilli wants to merge 1 commit into
mainfrom
t3code/typed-js-output-strategy

Conversation

@fcastrovilli
Copy link
Copy Markdown
Collaborator

@fcastrovilli fcastrovilli commented Mar 23, 2026

Summary

  • Added a new typed TypeScript/JavaScript SDK that mirrors the C++ music++ semantics for Max and web usage.
  • Introduced shared generated static data and build/test plumbing to keep TS outputs aligned with the C++ reference implementation.
  • Added parity, differential, fuzz, and example tests to validate behavior against the C++ contract.
  • Updated repository docs to describe the C++/TS split and the parity workflow.

Testing

  • Not run in this environment.
  • Covered by new TS test suites: parity, extended parity, differential parity, fuzz parity, core, and examples.
  • Added C++ spec runner support to generate/verify the shared contract used by the TS SDK.

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced TypeScript SDK package as a consumable library alternative, mirroring core C++ functionality.
    • Added shared scale database generation system for unified data across language implementations.
    • Established parity testing infrastructure verifying semantic equivalence between C++ and TypeScript.
  • Documentation

    • Updated README with complete monorepo structure and formalized C++/TS contract.
    • Added TypeScript SDK documentation with contributor workflows and build guidance.

- Introduce `packages/ts-sdk` as the consumable JS/TS package
- Move shared static scale data into `packages/shared-data`
- Add C++ spec runner and parity tests to keep outputs aligned
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 23, 2026

📝 Walkthrough

Walkthrough

This PR establishes a complete Turborepo monorepo architecture by introducing a comprehensive TypeScript SDK (packages/ts-sdk) with full parity to C++ implementations, a shared data package (packages/shared-data) for generating scale dictionaries, and C++ SDK enhancements including a spec runner CLI tool. The changes include scale data generation scripts, extensive vector/harmony/automation/analysis operations, test suites validating C++/TS parity, and build configuration.

Changes

Cohort / File(s) Summary
Documentation & Project Structure
README.md, packages/ts-sdk/README.md
Updated README to reflect full monorepo with packages/ts-sdk and packages/shared-data; introduced C++/TS contract and parity-driven modification workflow.
C++ SDK Build & Configuration
packages/cpp-sdk/CMakeLists.txt, packages/cpp-sdk/package.json
Added musicplusplus-spec executable target from tools/spec_runner.cpp; updated npm build scripts to use execFileSync with argument arrays; added prebuild step for scale data generation and new spec script.
C++ SDK Header Fixes
packages/cpp-sdk/src/binaryVector.h, packages/cpp-sdk/src/distances.h, packages/cpp-sdk/src/matrixDistance.h, packages/cpp-sdk/src/measures.h, packages/cpp-sdk/src/noteNames.h, packages/cpp-sdk/src/quantizeTranspose.h, packages/cpp-sdk/src/scaleDictionary.h, packages/cpp-sdk/src/slonimsky.h, packages/cpp-sdk/examples/classtest.cpp
Fixed include casing (lowercase headers), corrected entropy/normalization logic, replaced hardcoded scales with generated data include, fixed guard macros, improved Euclidean algorithm to preserve original step values.
C++ Spec Runner
packages/cpp-sdk/tools/spec_runner.cpp
Added new CLI tool (1342 lines) implementing musicplusplus-spec executable with comprehensive command dispatch covering vector conversions, alignment, vector algebra, rhythm generation, matrix operations, automation, note naming, chord analysis, scale database, quantization, melody operations, and Slonimsky sequences; includes JSON serialization and argument parsing.
Shared Data Generation
packages/shared-data/package.json, packages/shared-data/scripts/generate-scale-data.mjs
Added scale data generation infrastructure that reads scales.json, validates structure, generates C++ scaleDictionaryData.inc include file and TypeScript scales.generated.ts module with ScaleRecord interface and SCALE_RECORDS constant.
TypeScript SDK - Core Vectors
packages/ts-sdk/src/core/positionVector.ts, packages/ts-sdk/src/core/intervalVector.ts, packages/ts-sdk/src/core/binaryVector.ts, packages/ts-sdk/src/core/types.ts, packages/ts-sdk/src/core/internal.ts, packages/ts-sdk/src/core/conversions.ts
Implemented immutable vector classes with modular arithmetic, comprehensive scalar/vector operations, conversions between representations, and shared utility functions for numeric manipulation.
TypeScript SDK - Core Utilities
packages/ts-sdk/src/core/distances.ts, packages/ts-sdk/src/core/distribution.ts, packages/ts-sdk/src/core/helpers.ts, packages/ts-sdk/src/core/rhythm.ts, packages/ts-sdk/src/core/vectors.ts, packages/ts-sdk/src/core/index.ts
Added distance metrics (Euclidean, edit, Hamming, Manhattan, variation), distribution normalization/CDF, alignment and pitch helpers, rhythm generators (Euclidean, Clough-Douthett, deep rhythm, tihai), and tri-representation vector wrapper.
TypeScript SDK - Selection & Harmony
packages/ts-sdk/src/selection/index.ts, packages/ts-sdk/src/harmony/index.ts
Implemented voice selection across position/interval combinations and Scale/Chord classes with comprehensive transformation options (mode, inversion, mirror, shift, rotation).
TypeScript SDK - Matrix Operations
packages/ts-sdk/src/matrix/index.ts
Added modal/transposition/rototranslation matrix generation, filtering, distance computation, and complexity-based selection with multiple distance metric support.
TypeScript SDK - Analysis
packages/ts-sdk/src/analysis/types.ts, packages/ts-sdk/src/analysis/shared.ts, packages/ts-sdk/src/analysis/measures.ts, packages/ts-sdk/src/analysis/quantize.ts, packages/ts-sdk/src/analysis/index.ts
Implemented comprehensive rhythm/measure analytics (entropy, evenness, symmetry, spectra), quantization and transposition across scales, with 20+ exported analysis functions.
TypeScript SDK - Automation
packages/ts-sdk/src/automation/index.ts
Added voice-leading, degree, and modal automation functions with complexity-based selection and scale remapping via autoScale.
TypeScript SDK - Melody & Naming
packages/ts-sdk/src/melody/index.ts, packages/ts-sdk/src/naming/types.ts, packages/ts-sdk/src/naming/chordAnalysis.ts, packages/ts-sdk/src/naming/noteNamingSystem.ts, packages/ts-sdk/src/naming/index.ts
Implemented melody hierarchy analysis with triple-select ornament application, MIDI-to-note-name conversion with diatonic handling, and chord analysis with interval detection and naming.
TypeScript SDK - Data & Slonimsky
packages/ts-sdk/src/data/index.ts, packages/ts-sdk/src/slonimsky/index.ts
Added scale database lookup and Slonimsky sequence transformations (interpolation, ultrapolation, infrapolation variants).
TypeScript SDK - Main Entrypoint
packages/ts-sdk/src/index.ts
Added root export aggregating all public APIs from core, selection, harmony, matrix, automation, naming, data, analysis, melody, and slonimsky modules.
TypeScript SDK - Build Configuration
packages/ts-sdk/package.json, packages/ts-sdk/tsconfig.json, packages/ts-sdk/tsup.config.ts
Added package manifest with CommonJS/ESM/types exports, build scripts (prebuild scale generation, tsup bundling, declaration emission, ES5 check), test workflows, and TypeScript/tsup build configuration.
TypeScript SDK - Test Support
packages/ts-sdk/test/support/spec.ts
Added test helper to locate and execute C++ spec runner, parsing JSON/numeric outputs.
TypeScript SDK - Test Suites
packages/ts-sdk/test/core.test.ts, packages/ts-sdk/test/differential-parity.test.ts, packages/ts-sdk/test/parity.test.ts, packages/ts-sdk/test/parity-extended.test.ts, packages/ts-sdk/test/examples.test.ts, packages/ts-sdk/test/fuzz-parity.test.ts
Added comprehensive Vitest suites validating: core vector operations, semantic parity with C++ spec runner, extended functionality (selection, harmony), fuzzing with seeded RNGs, and representative usage examples.
TypeScript SDK - Fixtures
packages/ts-sdk/fixtures/max-js/smoke.cjs, packages/ts-sdk/fixtures/web-smoke/index.html, packages/ts-sdk/fixtures/web-smoke/main.ts
Added CommonJS and ES module smoke tests for runtime validation in Node and browser environments.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

🐰 Hops with joy through vectors bright,
TS mirrors C++ with all its might,
Scales dance between the languages two,
Harmony, selection, automation too!
From core to automation's grace,
Tests ensure we've set the pace.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/typed-js-output-strategy

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (22)
packages/ts-sdk/src/slonimsky/index.ts-17-28 (1)

17-28: ⚠️ Potential issue | 🟠 Major

minInterval returns undefined for sequences with fewer than 2 elements.

When sequence.length < 2, intervals() returns an empty array, making sequenceIntervals[0] undefined. The function then returns undefined, violating its declared number return type.

🐛 Proposed fix: Add guard for empty intervals
 export function minInterval(sequence: SlonimskySequence): number {
   var sequenceIntervals = intervals(sequence);
+  if (sequenceIntervals.length === 0) {
+    throw new Error("Cannot compute minInterval for sequence with fewer than 2 elements");
+  }
   var current = sequenceIntervals[0];
 
   for (var index = 1; index < sequenceIntervals.length; index += 1) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ts-sdk/src/slonimsky/index.ts` around lines 17 - 28, minInterval
currently reads sequenceIntervals[0] which is undefined when intervals(sequence)
returns an empty array for sequences with fewer than 2 elements; add a guard at
the start of minInterval to handle an empty intervals array (e.g., const
sequenceIntervals = intervals(sequence); if (sequenceIntervals.length === 0)
return 0;) before accessing sequenceIntervals[0], keeping the rest of the loop
and return behavior intact and referencing the minInterval function and
intervals helper.
packages/ts-sdk/test/support/spec.ts-6-16 (1)

6-16: ⚠️ Potential issue | 🟠 Major

Add timeout and make spec-runner path resolution robust.

The hardcoded executable path lacks platform awareness (no .exe for Windows) and build-layout flexibility, and execFileSync without a timeout can hang test jobs indefinitely.

Apply timeout bounds and add fallback path resolution with existence checks:

🔧 Proposed hardening patch
 import { execFileSync } from "node:child_process";
+import { existsSync } from "node:fs";
 import { dirname, resolve } from "node:path";
 import { fileURLToPath } from "node:url";
 
 const currentDirectory = dirname(fileURLToPath(import.meta.url));
-const specRunner = resolve(currentDirectory, "../../../cpp-sdk/build/musicplusplus-spec");
+const specRunnerCandidates = [
+  process.env.MUSICPP_SPEC_RUNNER,
+  resolve(currentDirectory, "../../../cpp-sdk/build/musicplusplus-spec"),
+  resolve(currentDirectory, "../../../cpp-sdk/build/musicplusplus-spec.exe"),
+  resolve(currentDirectory, "../../../cpp-sdk/build/Debug/musicplusplus-spec"),
+  resolve(currentDirectory, "../../../cpp-sdk/build/Debug/musicplusplus-spec.exe"),
+  resolve(currentDirectory, "../../../cpp-sdk/build/Release/musicplusplus-spec"),
+  resolve(currentDirectory, "../../../cpp-sdk/build/Release/musicplusplus-spec.exe"),
+].filter(Boolean) as string[];
+
+const specRunner = specRunnerCandidates.find((candidate) => existsSync(candidate));
+if (!specRunner) {
+  throw new Error("musicplusplus-spec executable not found. Set MUSICPP_SPEC_RUNNER or build cpp-sdk first.");
+}
 
 export function spec(command: string, args: Record<string, string>): any {
   const cliArgs: string[] = [command];
   const keys = Object.keys(args);
@@
-  const output = execFileSync(specRunner, cliArgs, { encoding: "utf8" }).trim();
+  const output = execFileSync(specRunner, cliArgs, {
+    encoding: "utf8",
+    timeout: 15_000,
+    maxBuffer: 1024 * 1024,
+  }).trim();

Note: Verify the exact output paths that CMake uses in your build layout and adjust the candidates list if needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ts-sdk/test/support/spec.ts` around lines 6 - 16, The spec function
currently uses a hardcoded specRunner path and calls execFileSync without a
timeout; update spec to build a list of candidate paths (using currentDirectory
and resolve) that include platform-specific names (add ".exe" on Windows) and
alternate build-layout locations, check fs.existsSync for each candidate and
pick the first existing path as specRunner, and invoke execFileSync with a
reasonable timeout option (e.g., 30s) and proper encoding to avoid hanging
tests; ensure errors throw if no candidate is found.
packages/ts-sdk/package.json-27-27 (1)

27-27: ⚠️ Potential issue | 🟠 Major

verify should build before smoke tests.

Line 27 runs smoke checks that depend on dist/index.js and dist/index.mjs, but verify never creates dist. This makes verification order-dependent.

🐛 Proposed fix
-    "verify": "npm run typecheck && npm run test && npm run test:smoke",
+    "verify": "npm run typecheck && npm run test && npm run build && npm run test:smoke",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ts-sdk/package.json` at line 27, The verify npm script currently
runs typecheck, test, and test:smoke but never builds artifacts required by the
smoke tests; update the "verify" script in package.json to invoke the build step
before running "test:smoke" (e.g., ensure "npm run build" is executed prior to
"npm run test:smoke") so that the smoke tests which depend on dist/index.js and
dist/index.mjs have the produced artifacts.
packages/cpp-sdk/package.json-8-8 (1)

8-8: ⚠️ Potential issue | 🟠 Major

spec script does not execute the spec runner.

Line 8 only prints the binary path, so no contract generation/verification actually happens.

🐛 Proposed fix
-        "spec": "npm run build && node -e \"var p=process.platform==='win32'?'windows':'unix';var f=p==='windows'?'build/windows/musicplusplus-spec.exe':'build/unix/musicplusplus-spec';console.log(f);\"",
+        "spec": "npm run build && node -e \"var p=process.platform==='win32'?'windows':'unix';var f=p==='windows'?'build/windows/musicplusplus-spec.exe':'build/unix/musicplusplus-spec';require('child_process').execFileSync(f,[],{stdio:'inherit'});\"",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cpp-sdk/package.json` at line 8, The "spec" npm script currently
only prints the path to the spec binary rather than running it; update the
"spec" script in package.json so it actually invokes the platform-specific
binary instead of console.log. Replace the node -e command so it resolves the
correct binary (same p and f logic) and then runs it (e.g., via
child_process.execFileSync or spawnSync with stdio:'inherit') so the spec runner
executes and returns its exit code; keep the platform-detection logic but call
the binary rather than printing its path.
packages/cpp-sdk/package.json-9-9 (1)

9-9: ⚠️ Potential issue | 🟠 Major

test currently masks regressions by always succeeding.

Line 9 exits with code 0 unconditionally, so workspace test pipelines get a false green signal even when parity is broken.

🐛 Proposed fix
-        "test": "node -e \"process.exit(0)\"",
+        "test": "npm run spec",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cpp-sdk/package.json` at line 9, The package.json "test" script
currently always exits 0 ("test": "node -e \"process.exit(0)\"") which masks
failures; update the "test" script to invoke the real test runner for this
package (for example replace with the project's test command like "jest" or
"vitest" or "npm run test:unit") so CI runs actual tests, and if there's no test
suite yet change the script to a failing placeholder (e.g. a non-zero exit) to
avoid false green signals until real tests are added.
packages/ts-sdk/src/data/index.ts-11-13 (1)

11-13: ⚠️ Potential issue | 🟠 Major

Clone scale records at the API boundary.

readonly does not protect these objects at runtime. Storing scales by reference and returning this.scales.slice() means outside mutations can change later findScale() results and leak shared state between ScaleDatabase instances.

Suggested fix
   public constructor(scales: readonly ScaleRecord[] = SCALE_RECORDS) {
-    this.scales = scales;
+    this.scales = scales.map(function (scale) {
+      return {
+        intervals: scale.intervals.slice(),
+        scaleName: scale.scaleName,
+        sheetName: scale.sheetName
+      };
+    });
   }
@@
   public getAllScales(): readonly ScaleMatch[] {
-    return this.scales.slice();
+    return this.scales.map(function (scale) {
+      return {
+        intervals: scale.intervals.slice(),
+        scaleName: scale.scaleName,
+        sheetName: scale.sheetName
+      };
+    });
   }

Also applies to: 62-63

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ts-sdk/src/data/index.ts` around lines 11 - 13, The constructor for
ScaleDatabase currently stores the provided scales array by reference (public
constructor(scales: readonly ScaleRecord[] = SCALE_RECORDS) { this.scales =
scales; }) which allows external mutation to affect internal state and later
findScale results; update the constructor to deep-clone or at least shallow-copy
the incoming array and its objects (clone SCALE_RECORDS when used as default)
before assigning to this.scales, and ensure any accessor that returns scales
(the method that currently returns this.scales.slice() around lines 62-63)
returns a cloned array of cloned ScaleRecord objects to prevent leaking mutable
references; reference ScaleDatabase, constructor, this.scales, findScale, and
SCALE_RECORDS when making these defensive copies.
packages/ts-sdk/src/analysis/quantize.ts-37-39 (1)

37-39: ⚠️ Potential issue | 🟠 Major

Use the destination modulus when synthesizing output notes.

outNote and the returned notes vector are keyed off inputScale.mod. If callers transpose between scales with different moduli, octave placement and the result metadata are wrong. If cross-mod input is unsupported, this should be validated up front instead.

Suggested fix
-  var mod = inputScale.mod;
+  var inputMod = inputScale.mod;
+  var outputMod = outputScale.mod;
@@
-    var inPitchClass = (note - inRoot) % mod;
+    var inPitchClass = (note - inRoot) % inputMod;
     if (inPitchClass < 0) {
-      inPitchClass += mod;
+      inPitchClass += inputMod;
     }
 
-    var octave = truncateTowardZero((note - inRoot) / mod);
-    if (((note - inRoot) < 0) && (((note - inRoot) % mod) !== 0)) {
+    var octave = truncateTowardZero((note - inRoot) / inputMod);
+    if (((note - inRoot) < 0) && (((note - inRoot) % inputMod) !== 0)) {
       octave -= 1;
     }
@@
-      var outNote = outPitchClass + outRoot + (octave * mod);
+      var outNote = outPitchClass + outRoot + (octave * outputMod);
@@
           inPitchClass = (notes[index] - inRoot) % mod;
           if (inPitchClass < 0) {
-            inPitchClass += mod;
+            inPitchClass += inputMod;
           }
@@
-            outNote = outPitchClass + outRoot + (octave * mod);
+            outNote = outPitchClass + outRoot + (octave * outputMod);
           }
         }
       }
@@
     notes: new PositionVector(notesData, {
-      mod: inputScale.mod,
+      mod: outputScale.mod,
       rangeUpdate: inputScale.rangeUpdate,
       user: inputScale.user,
       userRange: inputScale.userRange

Also applies to: 65-66, 97-102

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ts-sdk/src/analysis/quantize.ts` around lines 37 - 39, The code
currently keys synthesized outNote and the returned notes vector using
inputScale.mod (var mod = inputScale.mod), causing wrong octave placement when
transposing to a scale with a different modulus; update the logic in the
quantize function to use the destination/outScale modulus (use outScale.mod or
outRoot/outMod where outRoot/outMod are derived) when constructing outNote and
populating the notes array (also adjust occurrences around the blocks at lines
referenced: the initial mod assignment, the outNote creation, and the notes
population at 65-66 and 97-102), or alternatively add an upfront validation that
inputScale.mod === outScale.mod and throw/return an error if they differ; ensure
all references to mod for output synthesis use the destination modulus variable.
packages/ts-sdk/src/analysis/quantize.ts-5-26 (1)

5-26: ⚠️ Potential issue | 🟠 Major

Replace the -1 sentinel in quantize.

-1 is a valid scale value, so this misclassifies legitimate bounds and also makes an empty scale silently return -1.

Suggested fix
 export function quantize(note: number, scale: readonly number[], left: boolean = true): number {
-  var lower = -1;
-  var upper = -1;
+  var lower: number | undefined;
+  var upper: number | undefined;
@@
-  if (lower === -1) {
-    return upper;
+  if (typeof lower === "undefined") {
+    if (typeof upper === "undefined") {
+      throw new Error("Cannot quantize against an empty scale");
+    }
+    return upper;
   }
-  if (upper === -1) {
+  if (typeof upper === "undefined") {
     return lower;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ts-sdk/src/analysis/quantize.ts` around lines 5 - 26, The function
quantize misuses -1 as a sentinel (which can be a valid scale value) and
silently returns -1 for an empty scale; change the sentinel variables lower and
upper in quantize to use undefined (or null) instead of -1, update the
comparisons/tests from === -1 to === undefined, and ensure the function handles
an empty scale explicitly (e.g., return undefined or throw an Error) and the
return type/signature is updated accordingly so callers know quantize may not
always return a number.
packages/ts-sdk/src/core/conversions.ts-73-77 (1)

73-77: ⚠️ Potential issue | 🟠 Major

Fix inconsistent normalization base in positionsToBinary.

At Line 75, normalization uses positionData[0] while returned offset uses minimum. For non-sorted inputs, this produces incorrect bit placement.

💡 Proposed fix
   var minimum = minNumber(positionData);
   for (index = 0; index < positionData.length; index += 1) {
-    var normalizedPosition = positionData[index] - positionData[0];
+    var normalizedPosition = positionData[index] - minimum;
     binaryData[euclideanDivision(normalizedPosition, range).remainder] = 1;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ts-sdk/src/core/conversions.ts` around lines 73 - 77, In
positionsToBinary the normalization step uses positionData[0] but the offset
variable is computed from minNumber(positionData) (minimum), causing incorrect
bit indices for unsorted inputs; change the normalization to subtract minimum
(use minimum instead of positionData[0]) when computing normalizedPosition so
the remainder from euclideanDivision(normalizedPosition, range) aligns with the
returned offset; update the line referencing normalizedPosition to use minimum
and keep euclideanDivision(binaryIndex, range) and binaryData assignment
unchanged.
packages/ts-sdk/test/parity.test.ts-63-71 (1)

63-71: ⚠️ Potential issue | 🟠 Major

Harden scalar parsing in spec() to prevent NaN mismatches from non-numeric strings.

At lines 69-70, the function coerces non-JSON output via Number(output) without validation. This yields NaN for boolean strings ("true", "false") and other non-numeric scalars, causing spurious parity test failures.

Proposed fix
 function spec(command: string, args: Record<string, string>): any {
   const cliArgs: string[] = [command];
   const keys = Object.keys(args);
   for (let index = 0; index < keys.length; index += 1) {
     cliArgs.push(`--${keys[index]}`, args[keys[index]]);
   }
   const output = execFileSync(specRunner, cliArgs, { encoding: "utf8" }).trim();
-  return output.startsWith("{") || output.startsWith("[") ? JSON.parse(output) : Number(output);
+  try {
+    return JSON.parse(output);
+  } catch {
+    const numeric = Number(output);
+    return Number.isNaN(numeric) ? output : numeric;
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ts-sdk/test/parity.test.ts` around lines 63 - 71, The spec function
currently parses non-JSON output by blindly calling Number(output), which turns
non-numeric strings like "true"/"false" into NaN and breaks parity tests; update
spec(command, args) to robustly coerce scalars: after trimming, if output starts
with "{" or "[" keep JSON.parse(output); else if output matches boolean literals
("true" or "false") return the boolean; else if output matches a numeric pattern
(integer or float, optionally with sign/decimal/exponent) or Number(output) is
finite, return Number(output); otherwise return the original string. Ensure you
only change parsing logic inside spec and preserve existing execFileSync usage
and return conventions.
packages/ts-sdk/src/core/distances.ts-48-59 (1)

48-59: ⚠️ Potential issue | 🟠 Major

Don’t drop trailing elements from unequal-length distances.

Using minLength here means euclideanDistance([1], [1, 100]), manhattanDistance([1], [1, 100]), hammingDistance([1], [1, 100]), and difference([1], [1, 100]) all ignore the extra dimension entirely. That can make unequal inputs compare as identical in downstream ranking. Either reject unequal lengths or define how the tail contributes to the metric.

Also applies to: 97-123, 125-136

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ts-sdk/src/core/distances.ts` around lines 48 - 59, The distance
functions (euclideanDistance, manhattanDistance, hammingDistance, difference,
etc.) currently use the shorter length and ignore trailing dimensions, which can
make unequal-length vectors appear identical; add an explicit length check at
the start of each function (compare distanceSource(left).length and
distanceSource(right).length) and throw a clear TypeError (or RangeError) when
lengths differ, so callers must provide equal-length inputs (or handle
padding/aggregation upstream); update euclideanDistance, manhattanDistance,
hammingDistance and the difference helper(s) to perform this validation before
computing the metric.
packages/ts-sdk/src/selection/index.ts-125-134 (1)

125-134: ⚠️ Potential issue | 🟠 Major

Use the rotated criterion when deriving the returned offset.

This branch builds resultIntervals from rotatedPositionCriterion, but selectedOffset still comes from the unrotated positionCriterion. Any non-zero rotation therefore returns correct interval spans with the wrong offset.

Proposed fix
   var selectedOffset = intervalSource.offset;
-  if (positionCriterion.at(0) >= 0) {
-    for (intervalIndex = 0; intervalIndex < positionCriterion.at(0); intervalIndex += 1) {
+  var firstPosition = rotatedPositionCriterion.at(0);
+  if (firstPosition >= 0) {
+    for (intervalIndex = 0; intervalIndex < firstPosition; intervalIndex += 1) {
       selectedOffset += intervalSource.at(intervalIndex);
     }
   } else {
-    for (intervalIndex = positionCriterion.at(0); intervalIndex < 0; intervalIndex += 1) {
+    for (intervalIndex = firstPosition; intervalIndex < 0; intervalIndex += 1) {
       selectedOffset -= intervalSource.at(intervalIndex);
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ts-sdk/src/selection/index.ts` around lines 125 - 134, The computed
selectedOffset is still derived from the unrotated positionCriterion while
resultIntervals were built from rotatedPositionCriterion, so update the offset
calculation to use rotatedPositionCriterion (and its at(0) value) instead of
positionCriterion: replace references to positionCriterion.at(0) in the initial
conditional and both for-loop bounds with rotatedPositionCriterion.at(0), and
iterate against intervalSource the same way but using rotatedPositionCriterion
to compute selectedOffset so offsets align with the rotated intervals (consider
the rotation variable and resultIntervals creation in the same function to
confirm consistency).
packages/ts-sdk/src/core/rhythm.ts-160-161 (1)

160-161: ⚠️ Potential issue | 🟠 Major

Validate steps before constructing a deep rhythm.

deepRhythmValues() uses steps as the modulus, and this wrapper also stores steps as the resulting vector modulus. With steps <= 0, the API currently builds an invalid PositionVector instead of rejecting bad input.

Proposed fix
 export function deepRhythm(steps: number, events: number, multiplicity: number, offset: number = 0): PositionVector {
+  if (steps <= 0) {
+    throw new Error("steps must be positive");
+  }
   return new PositionVector(deepRhythmValues(steps, events, multiplicity), { mod: steps }).addScalar(offset);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ts-sdk/src/core/rhythm.ts` around lines 160 - 161, The wrapper
deepRhythm must validate the steps parameter before constructing a
PositionVector because deepRhythmValues and the PositionVector mod both rely on
a positive modulus; add a guard at the start of deepRhythm that checks if steps
is a positive integer (steps > 0) and throw a clear RangeError or TypeError if
not, then call deepRhythmValues(steps, events, multiplicity) and construct the
PositionVector as before; reference deepRhythm, deepRhythmValues, and
PositionVector when applying the change.
packages/ts-sdk/src/naming/noteNamingSystem.ts-277-278 (1)

277-278: ⚠️ Potential issue | 🟠 Major

Default positionVectorToNoteNames() to the vector’s own modulus.

Right now a PositionVector with mod !== 12 is still interpreted as 12-TET unless the caller repeats moduloValue manually. That breaks non-12 systems and diverges from the C++ oracle path, which defaults from the vector itself.

Proposed fix
   public positionVectorToNoteNames(positionVector: PositionVector, options?: NoteMapperOptions): NoteResult {
-    return this.midiNumbersToNoteNames(positionVector.data, options);
+    return this.midiNumbersToNoteNames(positionVector.data, {
+      ...(options || {}),
+      moduloValue: options && typeof options.moduloValue === "number" ? options.moduloValue : positionVector.mod
+    });
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ts-sdk/src/naming/noteNamingSystem.ts` around lines 277 - 278, The
method positionVectorToNoteNames currently always calls midiNumbersToNoteNames
without defaulting the modulo to the PositionVector's own modulus; update
positionVectorToNoteNames to detect when options is undefined or
options.moduloValue is not set and set options.moduloValue = positionVector.mod
(from the PositionVector object) before calling midiNumbersToNoteNames so
non-12-TET vectors default to their own modulus; reference NoteMapperOptions and
ensure you only mutate/clone options as appropriate before passing to
midiNumbersToNoteNames.
packages/ts-sdk/src/core/internal.ts-174-186 (1)

174-186: ⚠️ Potential issue | 🟠 Major

lcm() still breaks on multiple zero inputs.

If values contains two zeroes, gcd(result, values[index]) becomes 0, so the recurrence computes an invalid result instead of the expected 0. That can propagate a bad modulus into the LCM adapters.

Proposed fix
 export function lcm(values: readonly number[]): number {
   if (values.length === 0) {
     return 1;
   }
   if (values.length === 1) {
     return absoluteInteger(values[0]);
   }
+  if (values.some(function (value) {
+    return value === 0;
+  })) {
+    return 0;
+  }
 
   var result = absoluteInteger(values[0]);
   for (var index = 1; index < values.length; index += 1) {
     var divisor = gcd(result, values[index]);
     result = (result * absoluteInteger(values[index])) / divisor;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ts-sdk/src/core/internal.ts` around lines 174 - 186, The lcm(values)
implementation doesn't handle multiple zero inputs: when gcd(result,
values[index]) is 0 the recurrence produces an invalid value instead of
returning 0. Fix lcm by normalizing each operand with absoluteInteger and if any
normalized operand is 0 immediately return 0 (or if divisor === 0 return 0)
instead of continuing the loop; update the loop in lcm (using variables result,
divisor, values, absoluteInteger, gcd) to check for zero operands/divisor and
return 0 early so the function yields 0 for any set containing more than one
zero.
packages/ts-sdk/src/core/positionVector.ts-408-444 (1)

408-444: ⚠️ Potential issue | 🟠 Major

adaptToLCM missing guard for mod === 0 vectors.

Unlike IntervalVector.adaptToLCM which checks if (vectors[index].mod === 0) and skips scaling, this implementation would cause division by zero at line 429 if any vector has mod === 0.

🐛 Proposed fix
     for (index = 0; index < vectors.length; index += 1) {
+      if (vectors[index].mod === 0) {
+        adapted.push(vectors[index]);
+        continue;
+      }
       var scaleFactor = commonMod / vectors[index].mod;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ts-sdk/src/core/positionVector.ts` around lines 408 - 444,
adaptToLCM currently divides by vectors[index].mod and will throw for vectors
whose mod === 0; update PositionVector.adaptToLCM to guard against mod === 0 by
treating those vectors as unscaled entries (skip scaling and push a copy with
mod 0 into adapted) instead of computing scaleFactor or dividing by zero; mirror
the behavior used in IntervalVector.adaptToLCM: when vectors[index].mod === 0,
push vectors[index].slice/copy (preserving user and userRange) into adapted, and
only compute commonMod and scaleFactor for non-zero mod vectors.
packages/ts-sdk/src/core/binaryVector.ts-290-316 (1)

290-316: ⚠️ Potential issue | 🟠 Major

adaptToLCM may have undefined behavior if all input vectors have mod === 0.

If all vectors have mod === 0, uniquePositive returns an empty array. Then modulos.length <= 1 is true, and the function returns a shallow copy. This is handled correctly.

However, if some vectors have mod === 0 and others have positive mods, uniquePositive filters out zeros, lcm is computed, and scaleFactor = commonMod / vectors[index].mod would cause division by zero for mod === 0 vectors. But line 305 is inside the loop that doesn't check for mod === 0.

Wait - looking more carefully, there's no if (vectors[index].mod === 0) guard in this implementation unlike IntervalVector.adaptToLCM. This could cause division by zero.

🐛 Proposed fix
     for (var index = 0; index < vectors.length; index += 1) {
+      if (vectors[index].mod === 0) {
+        adapted.push(vectors[index]);
+        continue;
+      }
       var scaleFactor = commonMod / vectors[index].mod;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ts-sdk/src/core/binaryVector.ts` around lines 290 - 316, adaptToLCM
in BinaryVector can divide by zero when some vectors have mod === 0 because
uniquePositive drops zeros; update BinaryVector.adaptToLCM to handle zero-mod
vectors like IntervalVector.adaptToLCM: compute modulos = uniquePositive(...),
if modulos.length === 0 return vectors.slice(); compute commonMod = lcm(modulos)
only when modulos non-empty, and inside the loop skip scaling/division for
vectors[index].mod === 0 by either cloning them unchanged (preserve _data and
offset with mod 0) or treating scaleFactor as 1 for mod 0 cases; ensure no
division by zero when calculating scaleFactor and that new BinaryVector(...)
uses the correct mod and offset.
packages/ts-sdk/src/naming/chordAnalysis.ts-178-187 (1)

178-187: ⚠️ Potential issue | 🟠 Major

Sixth intervals should be mutually exclusive using else if.

Lines 178-187 incorrectly use two separate if statements instead of if-else, allowing both hasFlatSixth and hasNaturalSixth to be set to true simultaneously. This contradicts music theory—a chord cannot have both a flat sixth (♭6) and natural sixth (♮6) as they are distinct pitches. All other interval pairs in the function (thirds, fifths, sevenths, ninths, elevenths, thirteenths, seconds, and fourths) correctly use if-else to enforce mutual exclusivity. Change the second if to else if on line 183.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ts-sdk/src/naming/chordAnalysis.ts` around lines 178 - 187, The
sixth-interval checks currently allow both hasFlatSixth and hasNaturalSixth to
be set because they use two separate ifs; update the second check so the
natural-sixth branch is mutually exclusive with the flat-sixth branch by
changing the second condition to an else if (i.e., make the intervals.indexOf(9)
check an else if so only one of analysis.hasFlatSixth or
analysis.hasNaturalSixth can be set and usedIntervals is updated accordingly);
refer to the identifiers analysis.hasSixth, analysis.hasFlatSixth,
analysis.hasNaturalSixth, intervals, and usedIntervals to locate the code to
modify.
packages/ts-sdk/src/core/positionVector.ts-270-284 (1)

270-284: ⚠️ Potential issue | 🟠 Major

rotate() in PositionVector discards sign information using absoluteInteger(), breaking rotation directionality.

Line 276 uses absoluteInteger(rotationAmount), converting negative rotations to positive and making rotate(-2) equivalent to rotate(2). This inconsistently loses information compared to BinaryVector.rotate() which uses euclideanDivision(rotationAmount, length).remainder to preserve sign, and IntervalVector.rotate() which doesn't strip sign at all. The parity test at differential-parity.test.ts:85 expects rotate(-2) to preserve the -2 in the spec, indicating directional rotation semantics are intended.

Use euclideanDivision(rotationAmount, size).remainder instead of absoluteInteger() to preserve rotation direction consistently across all vector types.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ts-sdk/src/core/positionVector.ts` around lines 270 - 284, The
rotate method in PositionVector currently uses absoluteInteger(rotationAmount)
which removes sign info; update PositionVector.rotate to compute the effective
shift with euclideanDivision(rotationAmount, size).remainder (where size =
this._data.length) instead of absoluteInteger so negative rotations are
preserved, then use that remainder when indexing into rotatedData; keep the rest
of the method (rotatedData population and return new PositionVector(...,
asPositionVectorOptions(this))) unchanged.
packages/ts-sdk/src/automation/index.ts-72-76 (1)

72-76: ⚠️ Potential issue | 🟠 Major

Don't swallow every align() failure here.

This fallback converts any exception into center 0, so invalid inputs or regressions inside align() get hidden behind a plausible-but-wrong result. Please only fall back for the expected “no aligned center” case and rethrow everything else.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ts-sdk/src/automation/index.ts` around lines 72 - 76, The
calculateCenter function currently swallows all exceptions from
requireAlignedCenter and returns fallbackCenter for any error; change it to only
catch the specific "no aligned center" condition (e.g., the domain-specific
error thrown by requireAlignedCenter or a sentinel like NoAlignedCenterError / a
specific error.message) and return fallbackCenter in that case, but rethrow any
other errors so real failures in requireAlignedCenter/align() surface; update
the catch in calculateCenter to inspect the caught error and rethrow unless it
matches the expected "no aligned center" indicator.
packages/ts-sdk/src/analysis/measures.ts-423-430 (1)

423-430: ⚠️ Potential issue | 🟠 Major

Normalize the input set before the generated-set comparison.

generate() is treated as order-insensitive here because each candidate is sorted before comparison, but values is compared as-is. That makes isGenerated() return false negatives for the same set in a different order.

Suggested fix
 export function isGenerated(values: readonly number[], n: number): GeneratedResult {
   var size = values.length;
+  var normalized = values.slice().sort(function (left, right) {
+    return left - right;
+  });
 
   for (var multiplier = 1; multiplier < n; multiplier += 1) {
     var generated = generate(multiplier, size, n).slice().sort(function (left, right) {
       return left - right;
     });
-    if (arraysEqual(generated, values)) {
+    if (arraysEqual(generated, normalized)) {
       return {
         found: true,
         multiplier: multiplier
       };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ts-sdk/src/analysis/measures.ts` around lines 423 - 430, The
comparison in isGenerated treats generated candidates as order-insensitive by
sorting them but compares against values as-is, causing false negatives; before
the loop (or at start of isGenerated) normalize values by creating a sorted copy
(e.g., const normalized = values.slice().sort((a,b)=>a-b)) and then use
normalized in the arraysEqual check instead of values so that
arraysEqual(generated, normalized) compares two sorted arrays; ensure you don't
mutate the original values array.
packages/ts-sdk/src/analysis/measures.ts-455-476 (1)

455-476: ⚠️ Potential issue | 🟠 Major

Keep measureSummary() on the same modulus source as the rest of the module.

Line 455 switches to value.range, while the surrounding helpers compute against value.mod. If those two fields diverge, aksakClassification, generated, regressionEvenness, and the returned modulo no longer describe the same circle as the other summary fields.

Suggested fix
-  var modulo = value.range;
+  var modulo = value.mod;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ts-sdk/src/analysis/measures.ts` around lines 455 - 476, The
measureSummary() block currently uses value.range for the local modulo, which
diverges from other helpers that use value.mod; change the modulo source to
value.mod so calls like isGenerated(positions, modulo),
classifyAksakRhythm(modulo), isEuclidean(value, modulo) and
calculateRegressionEvenness(positions, modulo) (and the returned modulo field)
all reference the same circle; update the var modulo assignment to use value.mod
so generated, regressionEvenness, aksakClassification and the returned modulo
remain consistent with the rest of the module.
🟡 Minor comments (5)
README.md-220-228 (1)

220-228: ⚠️ Potential issue | 🟡 Minor

Fix inconsistent numbering in the Contributing checklist.

Lines 226-228 restart numbering, which makes the instructions look broken in raw markdown and review UIs.

📝 Proposed fix
-3. Document with Doxygen-style comments
-4. Rebuild and test: `npm run build && npm run docs`
-5. Update this README if adding major features
+5. Document with Doxygen-style comments
+6. Rebuild and test: `npm run build && npm run docs`
+7. Update this README if adding major features
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 220 - 228, The contributing checklist numbering is
inconsistent (items restart at 3); update the final three entries so the
sequence is continuous (e.g., after "Keep TS outputs aligned with C++ for
mirrored features" change the subsequent "3. Document with Doxygen-style
comments" to "5.", "4. Rebuild and test: `npm run build && npm run docs`" to
"6.", and "5. Update this README if adding major features" to "7.") so the
markdown list reads 1–7 in order and renders correctly; reference the checklist
lines mentioning `packages/cpp-sdk/src/`,
`packages/cpp-sdk/tools/spec_runner.cpp`, and `packages/ts-sdk/test` to locate
the items to renumber.
packages/cpp-sdk/src/scaleDictionary.h-135-136 (1)

135-136: ⚠️ Potential issue | 🟡 Minor

Normalize negative roots before indexing noteNames.

If the first interval is negative, C++ % returns a negative remainder, so this can index noteNames out of bounds.

Suggested fix
-    int root = intervals[0] % 12;
+    int root = ((intervals[0] % 12) + 12) % 12;
     return noteNames[root];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cpp-sdk/src/scaleDictionary.h` around lines 135 - 136, The code
computes int root = intervals[0] % 12 then returns noteNames[root], but C++'s %
can yield a negative remainder and thus root may be negative and index noteNames
out of bounds; change the normalization of root (used in the function in
scaleDictionary.h where variables intervals, root and noteNames appear) to
guarantee a 0..11 result (e.g., normalize by adding 12 then mod 12) before
indexing noteNames so negative intervals are handled safely.
packages/shared-data/scripts/generate-scale-data.mjs-60-64 (1)

60-64: ⚠️ Potential issue | 🟡 Minor

Validate interval values, not just the array shape.

Right now any non-integer entry inside intervals is emitted directly into generated C++/TS source. Failing fast here makes bad catalog edits much easier to diagnose.

Suggested fix
 if (!Array.isArray(scales) || scales.some((scale) => !scale || typeof scale.sheetName !== "string" || typeof scale.scaleName !== "string" || !Array.isArray(scale.intervals))) {
   throw new Error("Invalid scale catalog format");
 }
+if (
+  !Array.isArray(scales) ||
+  scales.some(
+    (scale) =>
+      !scale ||
+      typeof scale.sheetName !== "string" ||
+      typeof scale.scaleName !== "string" ||
+      !Array.isArray(scale.intervals) ||
+      scale.intervals.some((interval) => !Number.isInteger(interval))
+  )
+) {
+  throw new Error("Invalid scale catalog format");
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/shared-data/scripts/generate-scale-data.mjs` around lines 60 - 64,
The current validation only checks that scale.intervals is an array but not the
values; update the validation around the scales variable (the block that throws
"Invalid scale catalog format") to also confirm every entry in each
scale.intervals is a finite integer (e.g., Number.isInteger(value) &&
Number.isFinite(value)), and throw a descriptive Error that includes the
offending scale.sheetName/scale.scaleName and the invalid value(s); perform this
check inline with the existing Array.isArray(scales) guard so malformed interval
entries fail fast before generating C++/TS source.
packages/ts-sdk/src/melody/index.ts-58-73 (1)

58-73: ⚠️ Potential issue | 🟡 Minor

noteInfo does not normalize negative MIDI values.

Line 59: var normalizedNote = inputNote % mod produces negative remainders for negative inputs in JavaScript. For example, -5 % 12 = -5, not 7. This causes the pitch-class lookup at line 64 to fail for negative notes, returning invalidNote().

If negative MIDI notes are valid inputs, use proper modulo:

🐛 Proposed fix
 function noteInfo(inputNote: number, value: readonly number[], mod: number): NoteInfo {
-  var normalizedNote = inputNote % mod;
+  var normalizedNote = ((inputNote % mod) + mod) % mod;
   var baseOctave = Math.floor(inputNote / mod);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ts-sdk/src/melody/index.ts` around lines 58 - 73, The function
noteInfo uses inputNote % mod which yields negative remainders for negative
inputNote; replace the normalization with a true positive modulo like
normalizedNote = ((inputNote % mod) + mod) % mod and recompute baseOctave using
baseOctave = Math.floor((inputNote - normalizedNote) / mod) so the octave and
pitch-class lookup (value array scan and invalidNote() fallback) work correctly
for negative MIDI notes; update references to normalizedNote and baseOctave in
noteInfo accordingly.
packages/ts-sdk/src/core/intervalVector.ts-161-177 (1)

161-177: ⚠️ Potential issue | 🟡 Minor

Inconsistent error handling pattern: divideVector and moduloVector lack explicit zero-divisor checks.

These vector methods rely on euclideanDivision to throw an error if other contains any zero values, unlike divideScalar and moduloScalar (lines 110-132), which explicitly validate the divisor before proceeding. While division by zero is ultimately caught (via euclideanDivision's check on line 142-143), the indirect error handling makes the contract less clear and inconsistent with the scalar variant. Consider adding explicit validation in divideVector and moduloVector for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ts-sdk/src/core/intervalVector.ts` around lines 161 - 177, The
divideVector and moduloVector methods rely indirectly on euclideanDivision to
throw on zeros, creating an inconsistent pattern versus
divideScalar/moduloScalar; update divideVector and moduloVector to explicitly
validate the divisor array returned by extractData(other) for any zero entries
(or zero-equivalent values) before calling componentwiseOperation and throw the
same error type/message used by divideScalar/moduloScalar when a zero divisor is
found; locate checks in divideScalar/moduloScalar and mirror that logic here
(use the same error text/throw behavior) so divideVector, moduloVector,
euclideanDivision, and componentwiseOperation have consistent zero-divisor
handling.
🧹 Nitpick comments (13)
packages/ts-sdk/fixtures/web-smoke/index.html (1)

10-12: Minor: Inconsistent indentation on closing </html> tag.

The closing </html> tag on line 12 has extra leading whitespace compared to the rest of the document structure.

✏️ Suggested fix
     <script type="module" src="./main.ts"></script>
   </body>
-  </html>
+</html>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ts-sdk/fixtures/web-smoke/index.html` around lines 10 - 12, The
closing </html> tag has extra leading whitespace causing inconsistent
indentation; adjust the indentation of the closing </html> tag so it aligns with
the opening <html> and other top-level tags (remove the extra leading spaces
before </html>), ensuring consistent formatting for the file's root tags.
packages/ts-sdk/src/slonimsky/index.ts (1)

1-15: Consider using let/const instead of var.

The module consistently uses var for variable declarations. Modern TypeScript/JavaScript best practices favor let for mutable variables and const for immutable ones, as they provide block scoping and help prevent accidental reassignment.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ts-sdk/src/slonimsky/index.ts` around lines 1 - 15, The file uses
var for local variables; change them to let/const to follow modern TS best
practices: in cloneNumberArray ensure any local variables (none currently) would
use const where immutable; in intervals replace "var result: number[] = [];"
with "const result: number[] = [];" and replace "for (var index = 0; index <
sequence.length - 1; index += 1)" with "for (let index = 0; index <
sequence.length - 1; index += 1)"; update any other var occurrences in this
module (e.g., future locals in cloneNumberArray or other helpers) to let if they
are reassigned or const if not.
packages/cpp-sdk/src/distances.h (1)

19-31: Parameter should be const since it's not modified.

The in parameter is declared as a non-const reference, but the function never modifies it. This should be const vector<int>& in for const-correctness and to allow passing temporaries.

♻️ Suggested fix
-vector<double> normalize(vector<int>& in) {
+vector<double> normalize(const vector<int>& in) {
     double sum = accumulate(in.begin(), in.end(), 0.0);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cpp-sdk/src/distances.h` around lines 19 - 31, The parameter of
normalize should be a const reference because the function does not modify the
input; update the signature of normalize from vector<int>& in to const
vector<int>& in (preserving the function name normalize) so callers can pass
temporaries and to enforce const-correctness, and ensure all uses inside
(accumulate, transform) operate on the const input iterators (no other logic
changes needed).
packages/ts-sdk/fixtures/web-smoke/main.ts (1)

10-10: Handle missing #app container with an explicit error.

Line 10 uses a non-null assertion; if the fixture markup changes, this fails with a generic null dereference instead of a smoke-test-specific message.

♻️ Proposed fix
-document.getElementById("app")!.textContent = intervals.data.join(",");
+const app = document.getElementById("app");
+if (!app) {
+  throw new Error('ESM smoke test failed: missing "#app" container');
+}
+app.textContent = intervals.data.join(",");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ts-sdk/fixtures/web-smoke/main.ts` at line 10, The current use of
document.getElementById("app")! in the fixture can produce an opaque null
dereference if the markup changes; update the code around
document.getElementById("app") (the expression that assigns textContent using
intervals.data.join(",")) to explicitly handle a missing container: first call
document.getElementById("app"), check if the result is null, and if so throw or
log a clear, smoke-test-specific Error (e.g., "Missing `#app` container in
web-smoke fixture") rather than using a non-null assertion, otherwise set
textContent from intervals.data.join(",").
packages/ts-sdk/src/analysis/shared.ts (1)

3-78: Consolidate duplicated numeric helpers to reduce drift.

Several utilities here overlap with core helper primitives. Consider centralizing shared numeric/array helpers in one module and importing them here, to avoid behavior divergence between analysis and core paths.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ts-sdk/src/analysis/shared.ts` around lines 3 - 78, This module
duplicates many basic numeric/array helpers (cloneNumberArray, sortAscending,
arraysEqual, maxNumber, minNumber, truncateTowardZero, cppPrecision,
occurrenceCountForDistance, containsNumber); extract these implementations into
the central shared helper module used by core (or import the existing primitives
if they already exist), replace the local implementations with imports,
re-export any public APIs as needed, ensure function signatures and edge-case
behavior (empty arrays, NaN/Infinity handling, truncate toward zero semantics)
are preserved exactly, and run/update unit tests to confirm there is no
behavioral drift.
packages/ts-sdk/src/core/positionVector.ts (1)

126-130: Redundant cycle calculation.

Line 128 recalculates cycles as (index - division.remainder) / this._data.length, but this is equivalent to division.quotient from the Euclidean division result. Consider using the quotient directly.

♻️ Simplified element access
   public element(index: number): number {
     if (this._data.length === 0) {
       return 0;
     }

     var division = euclideanDivision(index, this._data.length);
-    var cycles = (index - division.remainder) / this._data.length;
-    return this._data[division.remainder] + absoluteInteger(this._range) * cycles;
+    return this._data[division.remainder] + absoluteInteger(this._range) * division.quotient;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ts-sdk/src/core/positionVector.ts` around lines 126 - 130, The
element access in PositionVector (in the method that computes an element by
index) recomputes cycles using (index - division.remainder) / this._data.length
even though euclideanDivision(index, this._data.length) already provides the
quotient; replace the manual cycles calculation with division.quotient and use
that in the returned expression (i.e., return this._data[division.remainder] +
absoluteInteger(this._range) * division.quotient) to avoid redundant arithmetic
and clarify intent.
packages/ts-sdk/src/melody/index.ts (2)

33-40: Magic number -666 used as invalid sentinel.

The sentinel value -666 is used throughout to indicate invalid note info. Consider extracting this as a named constant for clarity and maintainability.

const INVALID_VALUE = -666;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ts-sdk/src/melody/index.ts` around lines 33 - 40, Extract the magic
sentinel -666 into a named constant (e.g., INVALID_VALUE) and replace its
occurrences in the invalidNote function and any other usage sites so the
function invalidNote(): NoteInfo returns fields set to INVALID_VALUE; update
references to the NoteInfo invalid sentinel accordingly to improve clarity and
maintainability.

251-268: Add a clarifying comment explaining the algorithm's intentional behavior.

The algorithm loops repeatedly prepending values to result, then takes a suffix of the repeated pattern. For ornamentLoop([1, 2, 3], 5), this produces [2, 3, 1, 2, 3] (elements 1–5 of the repeated pattern [1, 2, 3, 1, 2, 3, ...]), which matches the C++ reference implementation and is validated by tests.

However, the algorithm is inefficient: repeated concat operations are O(n²). Consider optimizing this, such as by calculating indices into the pattern rather than building intermediate arrays. A comment explaining the intended behavior would also clarify why the suffix is taken rather than a prefix.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ts-sdk/src/melody/index.ts` around lines 251 - 268, The ornamentLoop
function prepends repeated copies of values then takes a trailing slice, which
intentionally returns the first length elements of the infinitely repeated
pattern (e.g., ornamentLoop([1,2,3],5) -> [2,3,1,2,3]); add a concise comment
above ornamentLoop (and mention cloneNumberArray usage) explaining this
intentional suffix-taking behavior and its correspondence to the C++ reference
and tests, and then refactor the implementation to avoid O(n^2) concat by
directly computing each output element with modular index arithmetic (compute
result[i] = values[(i + offset) % values.length] or equivalent) rather than
building intermediate arrays.
packages/ts-sdk/src/matrix/index.ts (1)

306-341: Runtime type detection is fragile and relies on duck typing.

The implementation uses property existence checks (rows !== undefined, matrix !== undefined, transposition !== undefined, modeIndex !== undefined) to determine the matrix type at runtime. This is fragile because:

  1. If a new matrix type is added with overlapping properties, dispatch could be incorrect
  2. The checks assume first element represents all elements (line 327: rows[0].matrix)

Consider using a discriminant property (e.g., readonly kind: "modal" | "transposition" | ...) for safer type discrimination.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ts-sdk/src/matrix/index.ts` around lines 306 - 341, The runtime
dispatch is fragile because it duck-types on properties like rows, matrix,
modeIndex and assumes rows[0] shape; update the matrix type hierarchy to include
a readonly discriminant (e.g., kind: "rototranslation" | "modal" |
"transposition"), then replace the property-existence checks in the function
that inspects the local variable matrix with an explicit switch or if on
matrix.kind (referencing RototranslationMatrix, ModalRototranslationMatrixRow,
and the generic rows handling) and use the discriminant to safely access
rotationMatrix.rows, modalRotations[*].matrix.rows, etc.; also remove the
rows[0].matrix assumption by using the discriminant instead of peeking at the
first element.
packages/ts-sdk/src/naming/chordAnalysis.ts (2)

372-401: Inconsistent spacing in added note suffixes.

The spacing logic introduces inconsistencies. isAddedNote is used to conditionally add a leading space, but each suffix also appends a trailing space (e.g., "2 "). This results in double spacing when multiple notes are added and a trailing space before the (omit X) suffix.

♻️ Suggested refactor for cleaner spacing
   var isAddedNote = false;
   if (analysis.hasNaturalSecond && name.indexOf("sus2") === -1 && name.indexOf(" 9") === -1) {
-    name += isAddedNote ? "" : " ";
-    name += "2 ";
+    name += isAddedNote ? " 2" : " 2";
     isAddedNote = true;
   }
   if (analysis.hasFlatSecond) {
-    name += isAddedNote ? "" : " ";
-    name += "b2 ";
+    name += isAddedNote ? " b2" : " b2";
     isAddedNote = true;
   }
   // ... apply same pattern to other added notes
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ts-sdk/src/naming/chordAnalysis.ts` around lines 372 - 401, The
added-note concatenation logic in chordAnalysis.ts (using variables name and
isAddedNote and checks like analysis.hasNaturalSecond / hasFlatSecond /
hasNaturalFourth / hasSharpFourth / hasNaturalSixth / hasFlatSixth) appends both
conditional leading spaces and trailing spaces (e.g., "2 "), causing double
spaces and a trailing space before the "(omit X)" suffix; fix by making each
note suffix have no trailing space (use "2", "b2", "4", "#4", "6", "b6") and
only prepend a single leading space when isAddedNote is false (then set
isAddedNote = true), and finally ensure you trim or conditionally add a single
space before adding the "(omit ...)" suffix so no extra spaces remain.

69-86: Interval normalization may produce unexpected results for large intervals.

The loop at lines 81-83 reduces intervals >= 24 by subtracting 12, but this only runs once per iteration. An interval of 36 would become 24 on first pass and exit the loop (since 24 >= 24 is false after subtraction). However, the while condition ensures it loops until < 24. The logic is correct, but consider using modulo arithmetic for clarity and efficiency.

♻️ Simplified interval normalization
       var interval = midiNotes[index] - midiNotes[rootIndex];
-      while (interval < 0) {
-        interval += 12;
-      }
-      while (interval >= 24) {
-        interval -= 12;
-      }
+      interval = ((interval % 24) + 24) % 24;
+      if (interval < 0) {
+        interval += 24;
+      }

Or more simply, if intervals should be in range [0, 24):

+      interval = ((interval % 24) + 24) % 24;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ts-sdk/src/naming/chordAnalysis.ts` around lines 69 - 86, The
interval normalization in analyzeChord currently adjusts negative intervals with
a loop and reduces large intervals with a while-subtraction that is hard to
read; replace both loops with a clear modulo operation to normalize interval
into the desired range (e.g., interval = ((interval % 12) + 12) % 12 or into
[0,24) if intended) when computed inside the analyzeChord function where
interval is derived from midiNotes[rootIndex]; update the interval push site
(intervals.push(interval)) to use this normalized value and remove the explicit
while loops for clarity and efficiency.
packages/ts-sdk/src/core/vectors.ts (1)

69-71: Method name inversion is misleading.

inversion delegates to rototranslatePositions, which performs rotation and translation. In music theory and the context of this SDK (where PositionVector.inversion and IntervalVector.inversion perform actual pitch-axis inversions), this naming is confusing.

If this matches the C++ reference implementation for parity, consider adding a clarifying comment. Otherwise, rename to something like rotoTranslate or translateByMode.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ts-sdk/src/core/vectors.ts` around lines 69 - 71, The method
Vectors.inversion is misleading because it delegates to rototranslatePositions
(rotation+translation) rather than performing a pitch-axis inversion; either
rename Vectors.inversion to a clearer name like rotoTranslate or translateByMode
(and update all callers, exports, tests and docs to match), or if it must match
the C++ reference for parity, replace the method body with a concise clarifying
comment referencing rototranslatePositions and the C++ implementation and update
the public API docs to explain that Vectors.inversion performs rototranslation
rather than musical inversion; ensure references to the symbol inversion and
rototranslatePositions are updated consistently.
packages/ts-sdk/src/harmony/index.ts (1)

33-56: Option resolvers are verbose but functional.

The explicit null/undefined checks work correctly. Consider using nullish coalescing (??) for cleaner code, though this may be intentional for older JS runtime compatibility.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ts-sdk/src/harmony/index.ts` around lines 33 - 56, The option
resolvers resolveScaleOptions and resolveChordOptions use verbose typeof checks
for each property; simplify them by replacing the repeated options && typeof ...
ternaries with nullish coalescing (??) to provide defaults (e.g., degree ?? 0,
invert ?? false, etc.) for each field (degree, inversionAxis, invert, mirror,
mirrorAxis, mode, root in resolveScaleOptions and axis, invert,
negativeOrMirror, negativeOrMirrorPos, position, preVoices, rotation, shift in
resolveChordOptions) so the functions return the same Required<...> objects more
concisely while preserving current defaults.

Comment on lines +109 to +110
for (var index = 0; index < targets.length; index += 1) {
result.push(voiceLeadingAutomation(targets[index], references[index], normalized[index]).vector);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Swap the reference and target arguments in this call.

voiceLeadingAutomation() expects (reference, target, complexity). Line 110 currently passes (targets[index], references[index], ...), which inverts the transformation for every pair.

Suggested fix
-    result.push(voiceLeadingAutomation(targets[index], references[index], normalized[index]).vector);
+    result.push(voiceLeadingAutomation(references[index], targets[index], normalized[index]).vector);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (var index = 0; index < targets.length; index += 1) {
result.push(voiceLeadingAutomation(targets[index], references[index], normalized[index]).vector);
for (var index = 0; index < targets.length; index += 1) {
result.push(voiceLeadingAutomation(references[index], targets[index], normalized[index]).vector);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ts-sdk/src/automation/index.ts` around lines 109 - 110, The call to
voiceLeadingAutomation in the loop is passing arguments in the wrong order:
voiceLeadingAutomation expects (reference, target, complexity) but the code
calls voiceLeadingAutomation(targets[index], references[index],
normalized[index]). Update that call to pass references[index] as the first
argument and targets[index] as the second (keeping normalized[index] as the
complexity) so the function receives (reference, target, complexity).

Comment on lines +6 to +9
var referenceDivision = euclideanDivision(reference.at(0), reference.range);
var targetDivision = euclideanDivision(target.at(0), target.range);
var differenceInOctaves = referenceDivision.quotient - targetDivision.remainder;
var index = differenceInOctaves * target.size;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

align is subtracting the wrong division component.

differenceInOctaves should compare quotients. Using targetDivision.remainder mixes octave and pitch-class units, so the starting index is wrong for most non-zero targets.

Suggested fix
   var referenceDivision = euclideanDivision(reference.at(0), reference.range);
   var targetDivision = euclideanDivision(target.at(0), target.range);
-  var differenceInOctaves = referenceDivision.quotient - targetDivision.remainder;
+  var differenceInOctaves = referenceDivision.quotient - targetDivision.quotient;
   var index = differenceInOctaves * target.size;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var referenceDivision = euclideanDivision(reference.at(0), reference.range);
var targetDivision = euclideanDivision(target.at(0), target.range);
var differenceInOctaves = referenceDivision.quotient - targetDivision.remainder;
var index = differenceInOctaves * target.size;
var referenceDivision = euclideanDivision(reference.at(0), reference.range);
var targetDivision = euclideanDivision(target.at(0), target.range);
var differenceInOctaves = referenceDivision.quotient - targetDivision.quotient;
var index = differenceInOctaves * target.size;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ts-sdk/src/core/helpers.ts` around lines 6 - 9, The align logic
subtracts the wrong division component: change the computation of
differenceInOctaves to compare quotients (use targetDivision.quotient) rather
than mixing octave and pitch-class units via targetDivision.remainder; update
the line that sets differenceInOctaves to referenceDivision.quotient -
targetDivision.quotient so the subsequent index = differenceInOctaves *
target.size uses a correct octave delta when aligning (keep uses of
euclideanDivision, reference.at(0)/target.at(0) and range/size as-is).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant