Skip to content

fix(metadata): non-mutating getters, ingestion crash fixes, faster levels/description handling#134

Merged
jodeleeuw merged 6 commits into
mainfrom
claude/metadata-correctness-perf
Jul 20, 2026
Merged

fix(metadata): non-mutating getters, ingestion crash fixes, faster levels/description handling#134
jodeleeuw merged 6 commits into
mainfrom
claude/metadata-correctness-perf

Conversation

@jodeleeuw

Copy link
Copy Markdown
Member

Summary

Implements the core-library workstream of the repository improvement plan (docs/dev/improvement-plan.md, Phases 1a/1b/2). Every bug below was verified by executing a reproduction before fixing; every fix ships with the regression test that would have caught it.

The destructive-getter cluster (silent loss of user-edited descriptions)

  • collapseDescription now lets a user-edited default description win; it is only dropped when it is the "unknown" placeholder. Previously, editing a description for any plugin-documented or system variable in the web wizard showed the edit in the UI but shipped the plugin text in the downloaded JSON.
  • getList() collapses descriptions on copies — it no longer mutates stored variables, so getMetadata() followed by another generate() (the CLI's multi-file flow) no longer corrupts/duplicates description text.
  • getMetadata() / getMetadataFields() assemble and return fresh copies; author/variableMeasured no longer leak into internal state.

Ingestion crashes / corruption (all previously reproduce as thrown errors or corrupt output)

  • Extension columns no longer crash generate() for CSV input ('["mouse-tracking"]' string), missing extension_version, or single-string extension_type.
  • A plain-string description in metadata options (generate(data, { variables: { rt: { description: "Reaction time" } } })) is treated as { default: text } instead of being spread character-by-character into {"0":"R","1":"e",…}.
  • null rows in JSON/JSONL input are skipped with a warning instead of crashing.
  • objectsToCSV escapes header names (a column key containing a comma previously produced a structurally corrupt sidecar CSV).
  • Numeric coercion only applies when the value round-trips (String(Number(v)) === v.trim()), so "007", 17+-digit IDs, and "1e5" stay strings and the metadata no longer disagrees with the verbatim-written CSV.
  • updateMinMax no longer clobbers a single pre-set bound; updateName refuses renames onto existing/empty names instead of silently deleting variables; updateLevels stringifies primitives.
  • PluginCache is keyed on (name, version, extension flag) so mixed-version datasets get the right descriptions.

Performance (no default output changes)

  • Levels dedup via an internal Set — was Array.includes per value, O(n²); a 94k-distinct-value column was ~4×10⁹ comparisons. New opt-in levelsCap generate option (default off; the no-cap default behavior is unchanged and still covered by the scale stress test).
  • Description fetch+merge memoized per (plugin, version, variable) pair per generate() call — previously every cell awaited the plugin cache and re-merged.
  • Removed dead JSON2CSV, fixed stale JSDoc, defaulted constructor(verbose = false).

Testing

  • New tests/correctness-perf-fixes.test.ts (20 tests) covering every fix, plus a frontend integration test (packages/frontend/tests/descriptionSurvival.test.tsx) that drives the real library the way Variables.tsx does and asserts an edited description reaches the output JSON.
  • Updated tests that codified the old buggy behavior (called out per-test in commit messages): #getList non-mutation, two/three-key default-description survival, numeric-level stringification, scientific-notation strings staying strings.
  • Full suite: 678 passed (656 baseline + net new). tsc --noEmit clean; package build succeeds.

Notes for review

🤖 Generated with Claude Code

https://claude.ai/code/session_01QMjKvi9vHT3LfikGfW84Kg


Generated by Claude Code

claude added 5 commits July 6, 2026 20:39
… & description memoization

Getter cluster (A): getMetadata/getMetadataFields assemble on a fresh object
instead of mutating the live private metadata (author/variableMeasured no longer
leak into internal state); VariablesMap.getList and collapseDescription operate
on copies so a second generate() after getMetadata() can't corrupt the stored
per-plugin description maps. collapseDescription now lets a user-edited "default"
win (leads the joined Text) and only drops it when it is the "unknown"
placeholder — the frontend's user-edited descriptions survive.

Ingestion crashes (B): extension_type/extension_version normalized to arrays
(JSON-array strings from CSV are parsed, single strings wrapped, missing values
tolerated) so generate() no longer crashes on `.map`/undefined indexing; string
cells are only coerced to numbers when they round-trip (String(Number(v))===v),
keeping "007", 17-digit ids and "1e5" as string levels; non-object observations
(a null row) are skipped with a warning. updateDescription accepts a plain
string as { default: text } (no more char-by-char spread) and the empty-object
guard now compares to real undefined, not the string "undefined". updateMinMax
initializes only the missing bound; updateName refuses collisions/empty names;
updateLevels stringifies primitive levels.

Perf (C): per-(variable, plugin) description fetch+merge is memoized per
generate() call; levels dedup is O(1) via an internal Set while the serialized
field stays an insertion-ordered string[]; the extension loop no longer
re-runs updateFields on a value already folded in; opt-in levelsCap (default
off). Also default the verbose constructor param and fix the displayMetadata
JSDoc. Existing tests that pinned the old destructive-getter / dropped-default /
raw-numeric-level behavior are updated to the corrected behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QMjKvi9vHT3LfikGfW84Kg
- objectsToCSV now escapes header names (a column key with a comma or quote no
  longer produces a structurally corrupt sidecar CSV).
- parseJsonData filters null observations out of its JSONL output so a `null`
  line can't emit [null] and crash downstream Object.keys(null).
- Remove the unused JSON2CSV helper (superseded by objectsToCSV).
- Update the CSV stress test: scientific-notation strings ("1e3") now stay as
  string levels under the round-trip coercion rule instead of being coerced to
  numeric ranges.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QMjKvi9vHT3LfikGfW84Kg
Keying the plugin-description cache on plugin name alone served the first-seen
version's descriptions to every later version in a mixed-version dataset, and
made a plugin and an extension that share a name collide on one entry. Key on
(isExtension, name, version) so each is cached independently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QMjKvi9vHT3LfikGfW84Kg
Adds packages/metadata/tests/correctness-perf-fixes.test.ts (extension column
normalization, plain-string descriptions, user-edited default survival,
non-mutating getMetadata interleaved with generate, null observation handling,
objectsToCSV header escaping, numeric round-trip, updateMinMax/updateName/
updateLevels guards, version-keyed PluginCache, and description-fetch
memoization) and a frontend integration test that drives the REAL @jspsych/
metadata library the way Variables.tsx does and renders the real PreviewDrawer,
proving an edited description reaches the output JSON.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QMjKvi9vHT3LfikGfW84Kg
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QMjKvi9vHT3LfikGfW84Kg
@changeset-bot

changeset-bot Bot commented Jul 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2b83d04

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@jspsych/metadata Minor
frontend Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

…y-string description defaults

Two review follow-ups:

- The strict round-trip numeric test reclassified non-canonical decimals
  ("450.0", "5.50", "1.0") as categorical string levels — silently
  de-numericizing whole float columns from R/pandas exports. Accept
  decimal-fraction literals (no leading zeros) alongside the round-trip;
  identifier protection is unaffected since those risks ("007", 17-digit
  ids, "1e5") are all integer/exponent-shaped.
- collapseDescription treated a cleared (empty-string) user default as real
  user text, producing a leading " | " before plugin descriptions. Treat
  it like the "unknown" placeholder.

Tests for both, changeset updated to document the exact rules.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jodeleeuw
jodeleeuw merged commit b240e8b into main Jul 20, 2026
2 checks passed
@jodeleeuw
jodeleeuw deleted the claude/metadata-correctness-perf branch July 20, 2026 21:44
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.

2 participants