Skip to content

Implement custom reliability flags#41

Merged
richford merged 19 commits into
mainfrom
enh/allow-custom-reliability-measures
Jun 10, 2026
Merged

Implement custom reliability flags#41
richford merged 19 commits into
mainfrom
enh/allow-custom-reliability-measures

Conversation

@amyyeung17

@amyyeung17 amyyeung17 commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

This PR introduces the new argument customValidations to createEvaluateValidity. customValidations allows apps to pass in new flags using existing or new conditions defined in the conditions array. Each customValidations item must contain the new flag name, conditions array, and logicalOperation to use with conditions.

  • conditions callback function parameters: { responseTimes, responses, correct, completed, existingFlags }. responseTimes, responses, correct, and completed are existing values provided by the parent function baseEvaluateValidity (which is the function returned when calling createEvaluateValidity).
  • Existing flags can be verified using the callback argument data.existingFlags
  • Existing flags can be used to mark as reliability measure by itself even if it's used as a custom validation rule by passing the flag name to includedReliabilityFlags

Unrelated edit: Set fixed date for getAgeData test. The test was being flaky because the function relied on new Date().

@amyyeung17 amyyeung17 marked this pull request as ready for review June 3, 2026 15:40
@amyyeung17 amyyeung17 requested a review from a team as a code owner June 3, 2026 15:40
Emily-ejag
Emily-ejag previously approved these changes Jun 4, 2026

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

Thanks @amyyeung17. A few change requests below. Also, please run npm run format (i.e. prettier/eslint).

Comment thread test/utils.spec.js Outdated
});

// Mock date to avoid timezone-related flakiness
jest.useFakeTimers();

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.

jest.useFakeTimers() sits at the top level, so it activates at module load and applies to every test in the file until jest.useRealTimers() is called at the end of the getAgeData test body. This causes two issues: (1) unrelated tests registered earlier in the file run under fake timers unintentionally; (2) if that test throws before the restore line, fake timers leak into subsequent tests. Can you move setup/teardown into beforeAll/afterAll (or beforeEach/afterEach) scoped to that test/describe, restoring real timers in afterAll so a failure can't leak. The suite passes today, but it's a fragility trap.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hm, what are your thoughts about moving the data and fakeTimers into the test case? Or do we prefer defining a top-level variable so we can reuse it for other future tests?

Comment thread src/utils.js Outdated
}

isReliable = flags.filter((x) => includedReliabilityFlags.includes(x)).length === 0;
flags = flags.filter((x) => includedReliabilityFlags.includes(x));

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.

This is pre-existing but it looks like no "informational-only" flag is possible. The closing flags = flags.filter(x => includedReliabilityFlags.includes(x)) removes every flag not in includedReliabilityFlags from the returned output and from the reliability decision. So a custom flag you compute but don't include is invisible and you can't surface a flag purely for reporting without also making it count toward unreliability. This is pre-existing behavior but can you add a comment or something to the JsDoc documenting the known limitation. And maybe also a test for this.

Comment thread src/utils.js Outdated
Comment on lines +405 to +409
if (logicalOperation.toLowerCase() === 'and') {
conditionMet = conditions.every((condition) => condition(data));
} else if (logicalOperation.toLowerCase() === 'or') {
conditionMet = conditions.some((condition) => condition(data));
}

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.

Unsupported logicalOperation values silently no-op here. Only 'and'/'or' are handled. Anything else ('only', a typo, 'xor') leaves conditionMet = false and the flag silently never fires, with no warning. Please add an else { throw } with console.warn.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Gotcha. Now that you've brought up 'only' and 'xor', should we just implement them now?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Implemented 'xor'. 'Only' can be handled by just passing a single condition.

Comment thread src/utils.js Outdated
Comment on lines +405 to +409
if (logicalOperation.toLowerCase() === 'and') {
conditionMet = conditions.every((condition) => condition(data));
} else if (logicalOperation.toLowerCase() === 'or') {
conditionMet = conditions.some((condition) => condition(data));
}

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.

Also, a malformed rule throws and aborts the whole evaluation here. logicalOperation.toLowerCase() has no guard, so a rule missing logicalOperation (or with a non-string value) throws a TypeError, which propagates out of baseEvaluateValidity and takes the engagement-flag handling down with it. Real-world likelihood is low (rules are app-defined constants), but can you add a default value (logicalOperation = 'and') and also an explicit guard against non-string values?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added error handling during setup. I thought it might make sense to allow one-off custom rules that don't require logicalOperations. As a result, the validation only checks for valid logicalOperations when there are two or more conditions.

Comment thread src/utils.js Outdated

// Evaluate custom validations alongside default checks
if (customValidations.length > 0) {
const data = { responseTimes, responses, correct, completed, existingFlags: flags };

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.

existingFlags is a live reference shared across rules leading to order dependence. data is constructed once and existingFlags: flags aliases the same array that the forEach mutates later. So a later rule's condition can observe flags pushed by earlier custom rules, not just the default flags, which makes rule ordering important. Can you either snapshot it (existingFlags: [...flags]) so every rule sees the same baseline, or document that custom rules chain to each other. This also isn't exercised by tests (the multi-rule test only references base flags).

@amyyeung17 amyyeung17 Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Documented. I explored allowing customValidations rules to depend on one another, but the implementation became too unwieldy and introduced too many edge cases (e.g., circular dependencies).

Comment thread test/utils.spec.js
Emily-ejag
Emily-ejag previously approved these changes Jun 10, 2026
- Snapshot built-in flags once before the custom-validation loop so a custom
  rule cannot observe another custom rule's flag (enforces the documented
  independence contract)
- Only evaluate custom validations when responseTimes.length >=
  minResponsesRequired, so they are skipped on insufficient-response runs
- Clarify JSDoc: logicalOperation semantics (and=all, or=any, xor=exactly one),
  note the min-responses gating, and give the example reduce an initial value
- Add tests for custom-flag independence and skip-on-insufficient-responses
@richford richford merged commit 9c014a5 into main Jun 10, 2026
5 checks passed
@richford richford deleted the enh/allow-custom-reliability-measures branch June 10, 2026 17:08
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.

3 participants