Skip to content

fix(FAGERSTRÖM): correct option ordering, add smoker gate, bump edition - #18

Merged
gdevenyi merged 6 commits into
DouglasNeuroInformatics:mainfrom
david-roper:edit-fagerstrom
Jul 31, 2026
Merged

fix(FAGERSTRÖM): correct option ordering, add smoker gate, bump edition#18
gdevenyi merged 6 commits into
DouglasNeuroInformatics:mainfrom
david-roper:edit-fagerstrom

Conversation

@david-roper

@david-roper david-roper commented May 20, 2026

Copy link
Copy Markdown
Collaborator

Change the scale and total score calculation to fix order of options within multiple questions.

Questions appear as negative values, get converted to numbers and multiplied by -1

https://github.com/DouglasNeuroInformatics/CPP/issues/90

form can be viewed here

Summary by CodeRabbit

Release Notes

  • Bug Fixes
    • Improved scoring for the Nicotine Dependence assessment to ensure total scores are calculated correctly.
    • Updated response validation rules to match the revised scoring ranges.
    • Refined wording/labels for select answer options to improve clarity.

@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1a820aae-5561-48ae-aa8d-35cf4e66b5f3

📥 Commits

Reviewing files that changed from the base of the PR and between 85035ec and c08e266.

📒 Files selected for processing (1)
  • lib/forms/FAGERSTRÖM_NICOTINE_DEPENDENCE/index.ts

Walkthrough

The Fagerstrom Nicotine Dependence form updates scoring keys for several radio questions, revises the cigarette amount labels, and changes score computation and validation to match the new negative-value scheme.

Changes

Fagerstrom Nicotine Dependence Form Scoring Revision

Layer / File(s) Summary
Option score key scheme migration
lib/forms/FAGERSTRÖM_NICOTINE_DEPENDENCE/index.ts
The shared yesNoOptions mapping switches from {1/0} to {-1/0}, and the smokeTime and cigaretteHateToGiveup question options use negative string keys (-3..0 and -1) to match the new convention.
Question label and text updates
lib/forms/FAGERSTRÖM_NICOTINE_DEPENDENCE/index.ts
The cigaretteAmount question text and option labels are updated while preserving the 0–3 score range with string keys.
Score calculation and validation schema updates
lib/forms/FAGERSTRÖM_NICOTINE_DEPENDENCE/index.ts
The total-score measure now sums absolute values of responses, and validationSchema bounds are updated so several fields accept negative-to-zero ranges while cigaretteAmount remains 0..3.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: joshunrau

Poem

Scores dip low, then rise in tune,
Labels shift beneath the moon,
Negatives now carry weight,
Absolute sums set the gate.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title is related to the Fagerström form changes, but it is too generic to convey the main update clearly. Use a more specific title that mentions the scoring/order changes in the Fagerström form, such as updating option values and validation.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gdevenyi

gdevenyi commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

What's holding this up?

@david-roper

Copy link
Copy Markdown
Collaborator Author

@gdevenyi we use a hacky way of reordering the question options by making the scale negative instead (1 -> -1, 2 -> -2, ...). If this is ok then the pr is good to go.

@gdevenyi

gdevenyi commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Why is this needed? Can you explain the underlying issue?

@david-roper

david-roper commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator Author

Anne requests that the ordering of the questions options to be changed from lowest to highest score value to highest to lowest score value as seen in issue https://github.com/DouglasNeuroInformatics/CPP/issues/90. The design of ODC form does not allow for this ordering to be possible as it always sorts the options by numerical value, so the current makeshift solution is to make the values negative. A possible solution for this in the future is maybe a autoSortedOptions tag within the form field to stop this behaviour.

@david-roper

Copy link
Copy Markdown
Collaborator Author

another workaround that could be done is inversing the scale values like done in this form here however this could be confusing comparing it the official form and its scoring method.

@gdevenyi

gdevenyi commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

I think its a bit silly to auto-sort these things when the display is based on this. @joshunrau can we look into adding the sort control feature and defaulting it to off?

@joshunrau

joshunrau commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

This is not a design in ODC sorting. In the ECMAScript spec, non-negative integer object keys are automatically sorted when iterating over them. An alternative here could be to use something like "1.0".

Object.keys() returns keys in the following deterministic order (specified in ES2015+):

1. Integer indices, ascending numeric order
Keys that are valid array indices (non-negative integers as strings): "0", "1", "2", etc.

2. String keys, in insertion order
All other string keys, in the order they were added to the object.

3. Symbol keys are NOT included
Object.keys() ignores symbols entirely. (Use Object.getOwnPropertySymbols() for those.)


Example

const obj = {};
obj["b"] = 1;
obj["2"] = 2;
obj["a"] = 3;
obj["0"] = 4;
obj["1"] = 5;

Object.keys(obj); // ["0", "1", "2", "b", "a"]

Integer indices sort numerically first (0, 1, 2), then string keys follow in insertion order (b, a).


Important nuances

  • "Integer index" is strict: "3" qualifies, but "3.0", "-1", and "3.5" do not — those fall into insertion-order bucket.
  • This order is guaranteed by the spec ([[OwnPropertyKeys]] internal method, ES2015+). It's not implementation-defined anymore.
  • for...in follows the same order but also traverses the prototype chain.
  • Object.entries() and Object.values() use the identical ordering.
  • Negative integers and floats masquerading as keys ("-1", "1.5") are treated as regular strings and go in bucket 2 (insertion order).

The practical implication: if you're using an object as a map with mixed numeric-string keys, your iteration order may surprise you if you added string keys before numeric ones.

@david-roper
david-roper marked this pull request as ready for review June 3, 2026 20:34
@gdevenyi
gdevenyi requested a review from Copilot June 3, 2026 20:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Updates the Fagerström Nicotine Dependence (FTND) instrument definition to address option ordering issues by changing option value encodings and adjusting the computed total score logic accordingly.

Changes:

  • Updated several radio option value keys (including introducing negative values) to correct ordering behavior.
  • Adjusted total score computation to account for negative-coded answers.
  • Updated validation schema bounds to match the new value ranges and tweaked one English question label.
Comments suppressed due to low confidence (5)

lib/forms/FAGERSTRÖM_NICOTINE_DEPENDENCE/index.ts:101

  • The English label has an extra "a" ("How many cigarettes a do you smoke…"), which reads as a typo in the user-facing question text.
    lib/forms/FAGERSTRÖM_NICOTINE_DEPENDENCE/index.ts:115
  • The French option text "31 ou plus 3" appears to have an extraneous trailing "3" in the user-facing label.
    lib/forms/FAGERSTRÖM_NICOTINE_DEPENDENCE/index.ts:150
  • The total score calculation currently special-cases cigaretteAmount and multiplies the remaining answers by -1. Since the schema now constrains the other answers to be <= 0, computing the score as the sum of absolute values is simpler and avoids depending on a specific field name.
    lib/forms/FAGERSTRÖM_NICOTINE_DEPENDENCE/index.ts:150
  • This computed measure is named auditCScore, which appears to be a copy/paste artifact from the AUDIT-C instrument and is misleading in the Fagerström instrument. Consider renaming it to something FTND-specific (or adding a new correctly-named measure while keeping auditCScore as a backwards-compatible alias if consumers rely on it).
    lib/forms/FAGERSTRÖM_NICOTINE_DEPENDENCE/index.ts:62
  • This French option label has a trailing space, which can show up in UI rendering and makes the string inconsistent with the other options.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
lib/forms/FAGERSTRÖM_NICOTINE_DEPENDENCE/index.ts (1)

142-149: ⚡ Quick win

Measure name auditCScore is misleading.

This is the Fagerström Nicotine Dependence test, not AUDIT-C. Consider renaming to ftndScore or totalScore for clarity.

Proposed fix
   measures: {
-    auditCScore: {
+    ftndScore: {
       kind: 'computed',
       label: {
         en: 'Total Score:',
         fr: 'Score total:'
       },
       value: (data) => sum(Object.values(data).map((v) => Math.abs(v ?? 0)))
     }
   },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/forms/FAGERSTRÖM_NICOTINE_DEPENDENCE/index.ts` around lines 142 - 149,
Rename the misleading computed field auditCScore to a clear name like ftndScore
(or totalScore) in the form definition: change the property key auditCScore to
ftndScore (and keep its kind, label, and value implementation
sum(Object.values(data).map((v) => Math.abs(v ?? 0)))); then update all
references/usages, imports, exports and any tests or type interfaces that expect
auditCScore to use ftndScore instead so callers read the new property name
consistently (ensure no runtime references remain to auditCScore).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/forms/FAGERSTRÖM_NICOTINE_DEPENDENCE/index.ts`:
- Line 115: The French label for option key '3' in the
FAGERSTRÖM_NICOTINE_DEPENDENCE options map contains a stray "3" ("31 ou plus
3"); locate the options object in
lib/forms/FAGERSTRÖM_NICOTINE_DEPENDENCE/index.ts and change the value for key
'3' from "31 ou plus 3" to "31 ou plus" so the label reads correctly.
- Line 101: Fix the typo in the FAGERSTRÖM_NICOTINE_DEPENDENCE question text:
replace the string "4. How many cigarettes a do you smoke per day?" (the en
value for that question) with "4. How many cigarettes do you smoke per day?" so
the English prompt reads correctly.

---

Nitpick comments:
In `@lib/forms/FAGERSTRÖM_NICOTINE_DEPENDENCE/index.ts`:
- Around line 142-149: Rename the misleading computed field auditCScore to a
clear name like ftndScore (or totalScore) in the form definition: change the
property key auditCScore to ftndScore (and keep its kind, label, and value
implementation sum(Object.values(data).map((v) => Math.abs(v ?? 0)))); then
update all references/usages, imports, exports and any tests or type interfaces
that expect auditCScore to use ftndScore instead so callers read the new
property name consistently (ensure no runtime references remain to auditCScore).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f3f15aea-a056-4264-8e77-264f165eecec

📥 Commits

Reviewing files that changed from the base of the PR and between 0c7c76c and 85035ec.

📒 Files selected for processing (1)
  • lib/forms/FAGERSTRÖM_NICOTINE_DEPENDENCE/index.ts

Comment thread lib/forms/FAGERSTRÖM_NICOTINE_DEPENDENCE/index.ts Outdated
Comment thread lib/forms/FAGERSTRÖM_NICOTINE_DEPENDENCE/index.ts Outdated
@david-roper

Copy link
Copy Markdown
Collaborator Author

will also close issue https://github.com/DouglasNeuroInformatics/CPP/issues/115

@gdevenyi gdevenyi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Comprehensive review — typos, auditCScore rename, and earlier CodeRabbit/Copilot findings are already addressed, so focusing on what's still open.

Blocking (address before merge)

  1. Edition bump (line 34, unchanged context) — schema-shape change breaks compat with edition-1 records: new required smokes field, all 6 scoring fields now .optional() with negative value ranges, measure renamed auditCScorenicotineDependenceScore. Any record captured against edition 1 (positive 0–3 values, no smokes) will fail validation or mis-score under the new schema. Bump internal.edition to 2 (CLAUDE.md §1.1, ScalarInstrumentInternal).
  2. Validation brittleness (line 205) — magic === 6, root-level error. Use superRefine + per-field path.
  3. Helper typing (line 20) — render's data param should be PartialData<TData>, not { smokes?: unknown }.
  4. Non-smoker score ambiguity (line 181) — return undefined, not 0, to distinguish N/A from zero dependence.

Non-blocking

  1. smokes gate deviates from canonical FTND — needs explicit sign-off and a note in description.
  2. Math.abs scoring works by coincidence — document the invariant or drop abs.
  3. Deprecated details.estimatedDuration / instructions (lines 45–49, unchanged context) — migrate to clientDetails.estimatedDuration / clientDetails.instructions. PR is already rewriting this region.
  4. Negative-key hack — track for cleanup once sort-control lands, or use joshunrau's "1.0" string-key idea.
  5. Missing referenceUrl; verify license string (line 50, unchanged context) — FTND has a canonical citation (Heatherton et al. 1991, Br J Addict 86:1116–26). Adding details.referenceUrl is in scope for a scoring-correctness PR. Also confirm PUBLIC-DOMAIN is an accepted SPDX value in the ODC approved-license list — closest SPDX strings are PublicDomain (deprecated) or a LicenseRef-.

Process

  • Title "Apply fagerstrom form suggestions" is vague — suggest fix(FAGERSTRÖM): correct option ordering, add smoker gate, bump edition.
  • 7 commits with merge commits + "fix typo" noise — squash-merge.

Lint passes. Details in inline comments (blocking items 2–4 and non-blocking 5, 6, 8 are anchored below; items 1, 7, 9 sit on unchanged lines so are listed here only).

Comment on lines +16 to +27
function createDependentField<const T>(field: T) {
return {
kind: 'dynamic' as const,
deps: ['smokes'] as const,
render: (data: { smokes?: unknown }) => {
if (data.smokes === true) {
return field;
}
return null;
}
};
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛑 Helper typing defeats the type system.

render: (data: { smokes?: unknown }) => ... should be PartialData<TData>. As written:

  • no inference of the full form shape
  • typos in dependency keys won't be caught
  • field: T is unconstrained — should be T extends StaticField<...>

Import the runtime-core dynamic-field / PartialData types and let TData flow through defineInstrument.

Comment on lines +201 to +206
.refine(({ smokes, ...data }) => {
if (!smokes) {
return true;
}
return Object.values(data).length === 6 && Object.values(data).every((arg) => typeof arg === 'number');
}, 'Error: Please fill out all the questions / Erreur: Veuillez répondre à toutes les questions')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛑 .refine is brittle and emits a single root-level error.

Object.values(data).length === 6  // magic number

If a 7th scoring field is added later, this passes silently for incomplete submissions (length never equals 6). Derive the expected keys from the schema, or enumerate them explicitly.

Also, .refine produces one path-less error — the user sees a generic banner instead of per-field "required" markers. Use .superRefine with ctx.addIssue({ path: [fieldName], ... }) so each missing field highlights individually.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't know what this means @joshunrau

Comment on lines 179 to 188
value: (data) => {
return sum(Object.values(data));
if (!data.smokes) {
return 0;
}
return sum(
Object.values(data)
.filter((v): v is number => typeof v === 'number')
.map((v) => Math.abs(v))
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛑 Non-smoker returns 0 — conflates "N/A" with "zero dependence".

A real FTND total of 0 means "low-dependence smoker." Returning 0 for non-smokers is ambiguous. InstrumentMeasureValue permits undefined — return that so downstream consumers can distinguish:

if (!data.smokes) return undefined;

(Also a nit on this block: Math.abs at line 186 is coincidentally correct — it works only because every negated field scores abs(key) AND cigaretteAmount happens to already score 0..3 directly. If a future field has positive keys but inverse scoring, abs silently breaks. Either drop abs and store keys at their true score values, or document the invariant inline.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Comment on lines +58 to +75
smokes: {
kind: 'boolean',
label: {
en: 'Do you smoke?',
fr: 'Fumez-vous?'
},
options: {
en: {
true: 'Yes',
false: 'No'
},
fr: {
true: 'Oui',
false: 'Non'
}
},
variant: 'radio'
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ smokes gate deviates from canonical FTND.

Standard FTND is 6 items with no gate. Per CLAUDE.md guideline #2 (faithful source implementation), adding a 7th gating question is a protocol change. If accepted (CPP #115), mention it in details.description so clinicians know this isn't vanilla FTND, and confirm explicit maintainer sign-off.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@david-roper this is a good idea

Comment on lines 83 to 96
options: {
en: {
3: 'Within 5 minutes',
2: '6-30 minutes',
1: '31-60 minutes',
0: 'More than 60 minutes'
'-3': 'Within 5 minutes',
'-2': '6-30 minutes',
'-1': '31-60 minutes',
'0': 'More than 60 minutes'
},
fr: {
3: 'Dans les 5 minutes',
2: '6 à 30 minutes',
1: '31 à 60 minutes ',
0: 'Plus de 60 minutes'
'-3': 'Dans les 5 minutes',
'-2': '6 à 30 minutes',
'-1': '31 à 60 minutes',
'0': 'Plus de 60 minutes'
}
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ℹ️ Negative-key hack pollutes the schema.

(Already discussed at length above — flagging on the code for traceability.) The negation forces odd validation bounds (min(-3).max(0) at line 194) and disableAutoPrefix: true everywhere (otherwise users see "-3" prefixes). Two cleaner paths:

  • joshunrau's "1.0" string-key idea — keeps scores readable, avoids negative bounds
  • wait for the platform-level sort-control feature gdevenyi proposed and revert to clean integer keys

Acceptable as a stop-gap, but please track for cleanup.

@gdevenyi gdevenyi changed the title Apply fagerstrom form suggestions fix(FAGERSTRÖM): correct option ordering, add smoker gate, bump edition Jul 14, 2026
@CMonnin

CMonnin commented Jul 31, 2026

Copy link
Copy Markdown
Member

Heads up @david-roper#38 also edits lib/forms/FAGERSTRÖM_NICOTINE_DEPENDENCE/index.ts and also bumps internal.edition from 1 to 2, as part of the generic-instructions work on DouglasNeuroInformatics/CPPQ#74.

The substantive changes don't overlap (this PR: option ordering, scoring, validation; #38: details.instructions only), but the edition line does. Whichever merges second should go to edition: 3 so the two versions don't ship under the same edition number — ODC keys instruments on hash(name-edition).

No preference on ordering from my side; #38 can rebase onto this and take edition 3 if that's easier. Flagging so it isn't missed.

@CMonnin

CMonnin commented Jul 31, 2026

Copy link
Copy Markdown
Member

Following up on my note above — we've settled the order. #38 keeps edition: 2, so this PR needs edition: 3.

@gdevenyi

Copy link
Copy Markdown
Contributor

@CMonnin I'll merge this and you can do a followup PR

@gdevenyi
gdevenyi merged commit dce7ef7 into DouglasNeuroInformatics:main Jul 31, 2026
1 check passed
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.

5 participants