From 003c24453637bcd422ce2d839cc8cd4e66f93e02 Mon Sep 17 00:00:00 2001 From: joshunrau Date: Sun, 4 Oct 2026 10:09:09 -0400 Subject: [PATCH 1/2] fix(instrument-utils): read const measures from their ref in the results summary The results summary looked up a const measure's value and label by the measure's own key instead of its `ref`, which is what the API stores. A measure keyed apart from its field showed another field's value, showed "NA", or threw when the key named no field at all. Co-Authored-By: Claude Opus 5.5 --- .../src/__tests__/measures.test.ts | 9 ++++++++ packages/instrument-utils/src/measures.ts | 21 ++++++++++++------- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/packages/instrument-utils/src/__tests__/measures.test.ts b/packages/instrument-utils/src/__tests__/measures.test.ts index ec1e4fa64..e4eb626d6 100644 --- a/packages/instrument-utils/src/__tests__/measures.test.ts +++ b/packages/instrument-utils/src/__tests__/measures.test.ts @@ -47,6 +47,15 @@ describe('computeInstrumentMeasures', () => { expect(result.favoriteNumber).toEqual({ label: 'Favorite Number', value: 7 }); }); + it("should read a 'const' measure's value and label through its ref, so a measure keyed apart from its field still shows that field", () => { + const instrument = { + ...formInstrument, + measures: { favorite: { kind: 'const', ref: 'favoriteNumber' } } + } as unknown as AnyUnilingualScalarInstrument; + const result = computeInstrumentMeasures(instrument, { favoriteNumber: 7 }); + expect(result.favorite).toEqual({ label: 'Favorite Number', value: 7 }); + }); + it("should use a 'const' measure's own label when it declares one", () => { const instrument = { ...formInstrument, diff --git a/packages/instrument-utils/src/measures.ts b/packages/instrument-utils/src/measures.ts index adcbc14d4..786441c3c 100644 --- a/packages/instrument-utils/src/measures.ts +++ b/packages/instrument-utils/src/measures.ts @@ -1,5 +1,10 @@ import { isPlainObject } from '@douglasneuroinformatics/libjs'; -import type { AnyUnilingualScalarInstrument, InstrumentMeasureValue } from '@opendatacapture/runtime-core'; +import type { + AnyUnilingualScalarInstrument, + InstrumentMeasures, + InstrumentMeasureValue, + Language +} from '@opendatacapture/runtime-core'; import { $InstrumentMeasureValue } from '@opendatacapture/schemas/instrument'; import { match } from 'ts-pattern'; @@ -14,14 +19,16 @@ export function computeInstrumentMeasures(instrument: AnyUnilingualScalarInstrum console.error(`Cannot compute measures from data: ${JSON.stringify(data)} is not an object`); return computedMeasures; } - for (const key in instrument.measures) { - const result = match(instrument.measures[key]!) + // Widened because `data` is untyped: under an interactive instrument's `Json` data, `ConditionalKeys` + // makes `ref` a union of `Json[]` members (numbers, array methods), which cannot index `data`. + const measures: InstrumentMeasures | null = instrument.measures; + for (const key in measures) { + const result = match(measures[key]!) .with({ kind: 'computed' }, (measure) => { - // @ts-expect-error - this is ignored because it is safer than the previous (any) solution return { label: measure.label, value: measure.value(data) }; }) .with({ kind: 'const' }, (measure) => { - const result = $InstrumentMeasureValue.safeParse(data[key]); + const result = $InstrumentMeasureValue.safeParse(data[measure.ref]); if (!result.success) { console.error('Failed to Parse Constant Measure', result.error); return null; @@ -32,10 +39,10 @@ export function computeInstrumentMeasures(instrument: AnyUnilingualScalarInstrum label = measure.label; } else if (isFormInstrument(instrument)) { // @ts-expect-error - this is ignored because it is safer than the previous (any) solution - label = extractFieldLabel(instrument, key, data); + label = extractFieldLabel(instrument, measure.ref, data); } if (!label) { - console.error(`Failed to extract label for key '${key}' from data '${JSON.stringify(data)}'`); + console.error(`Failed to extract label for key '${measure.ref}' from data '${JSON.stringify(data)}'`); return; } return { label, value: result.data }; From 550aa2746ecf97af5618dba13dc3a681314dd7db Mon Sep 17 00:00:00 2001 From: joshunrau Date: Sun, 4 Oct 2026 10:14:35 -0400 Subject: [PATCH 2/2] fix(instrument-utils): leave measures on unrevealed dynamic questions out of the results summary A const measure on a dynamic form field whose render function returned null for the submitted data still appeared in the results summary as "