@@ -141,16 +141,18 @@ function computeMeasures(trackResult, visits, confirmedROI, fps, eventOverrides)
141141 const nonTargetVisits = realVisits . filter ( v => v . holeIndex !== targetHoleIndex ) . sort ( ( a , b ) => a . startFrame - b . startFrame ) ;
142142
143143 const primaryLatency = targetVisits . length > 0
144- ? { status : 'ok' , value : ( targetVisits [ 0 ] . startFrame - trialStartFrame ) / fps , audited : anyOverrideBefore ( targetVisits [ 0 ] . startFrame + 1 ) }
145- : { status : 'not-reached' , value : null , audited : anyOverrideBefore ( Infinity ) } ;
144+ ? { status : 'ok' , value : ( targetVisits [ 0 ] . startFrame - trialStartFrame ) / fps , audited : anyOverrideBefore ( targetVisits [ 0 ] . startFrame + 1 ) , frame : targetVisits [ 0 ] . startFrame }
145+ : { status : 'not-reached' , value : null , audited : anyOverrideBefore ( Infinity ) , frame : null } ;
146146 const primaryErrorFrame = targetVisits . length > 0 ? targetVisits [ 0 ] . startFrame : Infinity ;
147- const primaryErrors = { value : nonTargetVisits . filter ( v => v . startFrame < primaryErrorFrame ) . length , audited : anyOverrideBefore ( primaryErrorFrame ) } ;
147+ const primaryErrorVisits = nonTargetVisits . filter ( v => v . startFrame < primaryErrorFrame ) ;
148+ const primaryErrors = { value : primaryErrorVisits . length , audited : anyOverrideBefore ( primaryErrorFrame ) , frames : primaryErrorVisits . map ( v => v . startFrame ) } ;
148149
149150 const totalLatencyResolved = resolveTotalLatency ( classified , targetHoleIndex ) ;
150151 const totalLatency = {
151152 status : totalLatencyResolved . status ,
152153 value : totalLatencyResolved . status === 'ok' ? ( totalLatencyResolved . frame - trialStartFrame ) / fps : null ,
153154 audited : totalLatencyResolved . audited ,
155+ frame : totalLatencyResolved . frame ,
154156 } ;
155157 // No defined total-latency frame (not reached, or undetermined) ->
156158 // whole-trial fallback, same reasoning as primary's never-reached case.
@@ -161,33 +163,34 @@ function computeMeasures(trackResult, visits, confirmedROI, fps, eventOverrides)
161163 // overridden (e.g. excluded as thigmotaxis, which changes the count
162164 // without moving the boundary).
163165 const totalAudited = totalLatencyResolved . audited || anyOverrideBefore ( totalErrorFrame ) ;
164- const totalErrors = { value : totalErrorVisits . length , audited : totalAudited } ;
166+ const totalErrors = { value : totalErrorVisits . length , audited : totalAudited , frames : totalErrorVisits . map ( v => v . startFrame ) } ;
165167
166168 // Working memory error (Gawel et al. 2019, Table 1, citing O'Leary &
167169 // Brown): re-checking a wrong hole already visited this trial, ANY
168170 // number of other holes in between allowed - this is what was
169171 // originally built here under the more confusing name "perseverative."
170172 const visitCountByHole = new Map ( ) ;
173+ const workingMemoryFrames = [ ] ;
171174 for ( const v of totalErrorVisits ) {
172- visitCountByHole . set ( v . holeIndex , ( visitCountByHole . get ( v . holeIndex ) || 0 ) + 1 ) ;
175+ const seen = ( visitCountByHole . get ( v . holeIndex ) || 0 ) + 1 ;
176+ visitCountByHole . set ( v . holeIndex , seen ) ;
177+ if ( seen > 1 ) workingMemoryFrames . push ( v . startFrame ) ; // the re-check itself, not the original visit
173178 }
174- let workingMemoryCount = 0 ;
175- for ( const count of visitCountByHole . values ( ) ) workingMemoryCount += Math . max ( 0 , count - 1 ) ;
176- const workingMemoryErrors = { value : workingMemoryCount , audited : totalAudited } ;
179+ const workingMemoryErrors = { value : workingMemoryFrames . length , audited : totalAudited , frames : workingMemoryFrames } ;
177180
178181 // Perseverative error, strict definition (same source): re-checking
179182 // the SAME wrong hole with NO other hole - target included - visited
180183 // in between. A back-to-back fixation, not just "forgot eventually."
181184 // Needs the full ordered sequence (target visits count as "another
182185 // hole" for breaking the streak), not just non-target visits.
183186 const fullSequence = realVisits . filter ( v => v . startFrame < totalErrorFrame ) . sort ( ( a , b ) => a . startFrame - b . startFrame ) ;
184- let strictPerseverativeCount = 0 ;
187+ const perseverativeFrames = [ ] ;
185188 for ( let i = 1 ; i < fullSequence . length ; i ++ ) {
186189 if ( fullSequence [ i ] . holeIndex === fullSequence [ i - 1 ] . holeIndex && fullSequence [ i ] . holeIndex !== targetHoleIndex ) {
187- strictPerseverativeCount ++ ;
190+ perseverativeFrames . push ( fullSequence [ i ] . startFrame ) ; // the repeat check itself
188191 }
189192 }
190- const perseverativeErrors = { value : strictPerseverativeCount , audited : totalAudited } ;
193+ const perseverativeErrors = { value : perseverativeFrames . length , audited : totalAudited , frames : perseverativeFrames } ;
191194
192195 // Hole deviation score (Gawel et al. 2019, Table 1): circular distance
193196 // from the target to whichever hole was checked FIRST in the trial -
@@ -207,8 +210,8 @@ function computeMeasures(trackResult, visits, confirmedROI, fps, eventOverrides)
207210 // independent correctness audit, see
208211 // audits/2026-09-04-correctness-audit.md finding #5.
209212 const holeDeviationScore = firstVisitOverall
210- ? { value : circularHoleDistance ( firstVisitOverall . holeIndex , targetHoleIndex , holes . length ) , audited : anyOverrideBefore ( firstVisitOverall . startFrame + 1 ) }
211- : { value : null , audited : false } ;
213+ ? { value : circularHoleDistance ( firstVisitOverall . holeIndex , targetHoleIndex , holes . length ) , audited : anyOverrideBefore ( firstVisitOverall . startFrame + 1 ) , frame : firstVisitOverall . startFrame }
214+ : { value : null , audited : false , frame : null } ;
212215
213216 // Post-arrival "commitment" - her own observation from test53's real
214217 // event trace (early pokes at the target hole, then a later one that
@@ -225,21 +228,22 @@ function computeMeasures(trackResult, visits, confirmedROI, fps, eventOverrides)
225228 // AND entered) - not-applicable otherwise, same "say so honestly"
226229 // posture as every other status here, rather than a misleading 0/null.
227230 const commitmentDelay = ( primaryLatency . status === 'ok' && totalLatency . status === 'ok' )
228- ? { status : 'ok' , value : totalLatency . value - primaryLatency . value , audited : primaryLatency . audited || totalLatency . audited }
229- : { status : 'not-applicable' , value : null , audited : primaryLatency . audited || totalLatency . audited } ;
231+ ? { status : 'ok' , value : totalLatency . value - primaryLatency . value , audited : primaryLatency . audited || totalLatency . audited , frames : [ primaryLatency . frame , totalLatency . frame ] }
232+ : { status : 'not-applicable' , value : null , audited : primaryLatency . audited || totalLatency . audited , frames : [ ] } ;
230233
231234 // How many separate visits to the target hole happened before the one
232235 // that finally resolved to entry - reuses totalErrorFrame (already the
233236 // entry visit's own frame, or Infinity if never entered) so this can
234237 // never disagree with total latency/errors about which window counts.
238+ const targetRevisitFrames = targetVisits . filter ( v => v . startFrame < totalErrorFrame ) . map ( v => v . startFrame ) ;
235239 const targetRevisitCount = totalLatencyResolved . status === 'ok'
236- ? { value : targetVisits . filter ( v => v . startFrame < totalErrorFrame ) . length , audited : totalAudited }
237- : { value : null , audited : totalAudited } ;
240+ ? { value : targetRevisitFrames . length , audited : totalAudited , frames : targetRevisitFrames }
241+ : { value : null , audited : totalAudited , frames : [ ] } ;
238242
239243 return {
240244 available : true ,
241245 primaryLatency, totalLatency, primaryErrors, totalErrors, workingMemoryErrors, perseverativeErrors, holeDeviationScore,
242246 commitmentDelay, targetRevisitCount,
243- pathLengthCm, avgSpeedCmS, quadrantPct,
247+ pathLengthCm, avgSpeedCmS, quadrantPct, trialStartFrame ,
244248 } ;
245249}
0 commit comments