Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
bc5dcb0
Stub the arbitration flow pages
edwardhorsford Aug 4, 2026
7b4fad8
Arbitration rides the reading workflow
edwardhorsford Aug 4, 2026
d382d3a
Arbitration cases join workflow navigation; status bar says arbitration
edwardhorsford Aug 4, 2026
3ad9d36
Increase arbitration seed data and fix modal breakout on review page
edwardhorsford Aug 5, 2026
35aa53f
Allow panel arbitrators to read cases they originally read
edwardhorsford Aug 5, 2026
a640503
Fix start page: pass both backlogCount and soloCount correctly
edwardhorsford Aug 5, 2026
239457d
Trim arbitration start page hints
edwardhorsford Aug 6, 2026
b7ab77c
Record panel members on the arbitration read and show them
edwardhorsford Aug 6, 2026
5878f30
Model arbitration as a group decision, and make arbitration sessions …
edwardhorsford Aug 6, 2026
c350269
Returning to an arbitrated case shows what was recorded
edwardhorsford Aug 6, 2026
112eeef
Fix returning to an arbitrated case: navigation, prefill and labels
edwardhorsford Aug 6, 2026
25447cd
Fix arbitration sessions running out of cases after a few decisions
edwardhorsford Aug 6, 2026
c8b2514
Fix arbitration status bar count and skipping to the next case
edwardhorsford Aug 6, 2026
6ec7fd9
Route session navigation through session-aware helpers
edwardhorsford Aug 6, 2026
1648ca3
Keep the skipped list honest, and stop skipping past the end of a ses…
edwardhorsford Aug 6, 2026
73afe12
Give arbitration its own session overview, and make finalisation real
edwardhorsford Aug 6, 2026
7ed529d
Update tag colour
edwardhorsford Aug 6, 2026
a107396
Regenerate the utils and filters reference
edwardhorsford Aug 6, 2026
e8381f1
Give arbitration its own settings section, with configurable confirm …
edwardhorsford Aug 7, 2026
c58c72c
Show placeholder rows on the lazy arbitration session overview
edwardhorsford Aug 7, 2026
7260aa7
Unlink the participant name on the arbitration session overview
edwardhorsford Aug 7, 2026
6bf8b49
Resume arbitration by case state, not per-user readability
edwardhorsford Aug 7, 2026
f1fa218
Widen the arbitration overview and unlink names on the reading overview
edwardhorsford Aug 7, 2026
ca3928f
Hide original opinions on the arbitration overview until a case is ar…
edwardhorsford Aug 7, 2026
1dd97bc
Move the symptoms tag below the participant name on session overviews
edwardhorsford Aug 7, 2026
366bb13
Left-align the session overview action column
edwardhorsford Aug 7, 2026
d3c08a2
Remove margins making rows too tall
edwardhorsford Aug 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# TODO

- style-guide/modal.html references `layout-fragment.html`, which doesn't exist — real layout is `_templates/layout-modal-form.html`
1 change: 0 additions & 1 deletion app/assets/sass/components/_reading.scss
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@

.app-placeholder-action-cell {
padding-right: 15px;
text-align: right;
white-space: nowrap;
}

Expand Down
11 changes: 9 additions & 2 deletions app/data/session-data-defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ const defaultSettings = {
}
},
reading: {
indexLayout: 'complex', // 'simple' | 'complex'
blindReading: config.reading.blindReading,
confirmNormal: 'false',
confirmNormalWithDetails: 'false',
Expand All @@ -115,10 +116,16 @@ const defaultSettings = {
annotationsMode: 'with-images-simple', // 'without-images' | 'with-images-simple' | 'with-images' | 'with-images-progressive'
secondReaderComparison: 'off', // 'early' | 'late' | 'off'
compareWhen: 'non_normal', // 'non_normal' | 'discordant_only'
arbitrationPolicy: 'discordant_only', // 'discordant_only' | 'all_recalls' | 'all_non_normal'
finalisationDelay: '60', // minutes before reads auto-finalise; '0' immediate | 'never' manual only
lazySessions: 'true',
defaultSessionSize: '25'
defaultSessionSize: '25',
arbitration: {
policy: 'discordant_only', // 'discordant_only' | 'all_recalls' | 'all_non_normal'
flow: 'compare_first', // 'compare_first' | 'opinion_first' - what an arbitration case opens on
confirmDecision: 'true', // show the review page before saving an arbitration decision
hideReadsUntilArbitrated: 'true', // overview hides the original opinions until the case is arbitrated
lazySessions: 'true'
}
}
}

Expand Down
8 changes: 7 additions & 1 deletion app/lib/generators/episode-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -781,7 +781,13 @@ const checkEpisodes = (episodes, appointmentsById) => {
)
}
})
if (new Set(reads.map((read) => read.readerId)).size !== reads.length) {
// Nobody reads a case twice. An arbitration read is the group's rather
// than one reader's, so it is exempt - its authors may include the
// original readers.
const readerIds = reads
.filter((read) => read.readType !== 'arbitration')
.map((read) => read.readerId)
if (new Set(readerIds).size !== readerIds.length) {
problems.push(
`reading case ${readingCase.id} has the same reader twice`
)
Expand Down
67 changes: 54 additions & 13 deletions app/lib/generators/reading-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -780,29 +780,28 @@ const generateReadingData = (appointments, users, episodes, seedProfile = {}) =>
}

// A FEW VERY RECENT SECOND READS: timestamped inside the finalisation delay,
// so the backlog carries live awaiting_finalisation cases - alternating
// concordant (finalising towards a conclusion) and forced discordant
// (finalising, then arbitration). Drawn from clinics[7], the first of the
// partial-first-read pair above - its first reads are recent, so the
// oldest-first session ordering the e2e journeys lean on is untouched.
// so the backlog carries live awaiting_finalisation cases - mostly forced
// discordant (for arbitration), with every 4th concordant. Drawn from
// clinics[7] and clinics[8]'s read appointments.
if (clinics.length >= 9) {
const clinic = clinics[7]
const candidates = clinic.appointments
const candidates = [clinics[7], clinics[8]]
.flatMap((clinic) => clinic.appointments)
.filter((appointment) => readAppointmentIds.has(appointment.id))
.slice(0, 8)
.slice(0, 20)

let minutesAgo = 50
let minutesAgo = 100
let count = 0

candidates.forEach((appointment, index) => {
const readingCase = casesByAppointmentId.get(appointment.id)
const firstRead = readingCase?.reads?.[0]
if (!firstRead) return

// Most cases forced discordant (arbitration), every 5th concordant
const opinion =
index % 2 === 0
? pickSecondOpinion(firstRead, 0) // force disagreement
: firstRead.opinion
index % 5 === 4
? firstRead.opinion
: pickSecondOpinion(firstRead, 0) // force disagreement

addRead(
readingCase,
Expand All @@ -812,7 +811,7 @@ const generateReadingData = (appointments, users, episodes, seedProfile = {}) =>
{ forceOpinion: opinion, alignmentProbability }
)

minutesAgo -= 5
minutesAgo -= 3
count++
})

Expand All @@ -821,6 +820,48 @@ const generateReadingData = (appointments, users, episodes, seedProfile = {}) =>
)
}

// MORE DISCORDANT SECOND READS: drawn from clinics[5] and clinics[6] (first
// read by secondReader), with thirdReader as the disagreeing second reader.
// Neither reader is the current user, so all are arbitrable by Jane Hitchin.
if (clinics.length >= 7) {
let minutesAgo = 95
let count = 0

const candidates = [clinics[5], clinics[6]]
.flatMap((clinic) => clinic.appointments)
.filter((appointment) => readAppointmentIds.has(appointment.id))
.filter((appointment) => {
const rc = casesByAppointmentId.get(appointment.id)
return rc && rc.reads?.length === 1
})
.slice(0, 20)

candidates.forEach((appointment, index) => {
const readingCase = casesByAppointmentId.get(appointment.id)
const firstRead = readingCase.reads[0]

const opinion =
index % 5 === 4
? firstRead.opinion
: pickSecondOpinion(firstRead, 0)

addRead(
readingCase,
appointment,
thirdReader,
dayjs().subtract(minutesAgo, 'minute').toISOString(),
{ forceOpinion: opinion, alignmentProbability }
)

minutesAgo -= 3
count++
})

console.log(
`Added ${count} discordant second reads from clinics 5-6 for arbitration`
)
}

// A COUPLE OF DEFERRED CASES: deferred by a reader with a reason, so the
// deferred list and the case view's deferral detail have real data behind
// them. Drawn from clinics[8]'s unread remainder, matching how deferral
Expand Down
Loading