@@ -25,7 +25,7 @@ import { aggregateResults, type AggregationPipelineOptions, createAggregationOut
2525import { createClaimSummary , evaluateClaims } from "../experiments/framework/claims/index.js" ;
2626import { getClaimsByTag , getCoreClaims , THESIS_CLAIMS } from "../experiments/framework/claims/registry.js" ;
2727// Framework imports
28- import { createExecutor , type ExecutorConfig } from "../experiments/framework/executor/index.js" ;
28+ import { createCheckpointManager , createExecutor , type ExecutorConfig , getGitCommit } from "../experiments/framework/executor/index.js" ;
2929import { CaseRegistry } from "../experiments/framework/registry/case-registry.js" ;
3030import { type GraphCaseRegistry , registerCases } from "../experiments/framework/registry/register-cases.js" ;
3131import { type ExpansionSutRegistry , registerExpansionSuts } from "../experiments/framework/registry/register-suts.js" ;
@@ -65,6 +65,9 @@ export interface EvaluateOptions {
6565 /** Collect provenance information */
6666 collectProvenance : boolean ;
6767
68+ /** Checkpoint mode: "file", "git", or "auto" */
69+ checkpointMode : string ;
70+
6871 /** Filter claims by tag */
6972 tags ?: string [ ] ;
7073
@@ -90,6 +93,7 @@ export const parseEvaluateArgs = (arguments_: ParsedArguments): EvaluateOptions
9093 const continueOnError = getBoolean ( arguments_ , "continue-on-error" , true ) ;
9194 const timeoutMs = getNumber ( arguments_ , "timeout" , 0 ) ;
9295 const collectProvenance = getBoolean ( arguments_ , "provenance" , true ) ;
96+ const checkpointMode = getOptional < string > ( arguments_ , "checkpoint-mode" , "auto" ) ;
9397
9498 // Claim filtering
9599 const tagsArgument = getOptional < string > ( arguments_ , "tags" ) ;
@@ -112,6 +116,7 @@ export const parseEvaluateArgs = (arguments_: ParsedArguments): EvaluateOptions
112116 continueOnError,
113117 timeoutMs,
114118 collectProvenance,
119+ checkpointMode,
115120 tags,
116121 claim,
117122 table,
@@ -178,14 +183,37 @@ const extractMetrics = (result: DegreePrioritisedExpansionResult): Record<string
178183 metrics [ "max-path-length" ] = Math . max ( ...pathLengths ) ;
179184 }
180185
181- // Hub traversal: percentage of nodes expanded that are high-degree
186+ // Hub traversal: percentage of nodes expanded that are high-degree (51+)
182187 const highDegreeCount = ( stats . degreeDistribution . get ( "51-100" ) ?? 0 )
183188 + ( stats . degreeDistribution . get ( "101-500" ) ?? 0 )
184189 + ( stats . degreeDistribution . get ( "501-1000" ) ?? 0 )
185190 + ( stats . degreeDistribution . get ( "1000+" ) ?? 0 ) ;
186191 const hubTraversal = stats . nodesExpanded > 0 ? highDegreeCount / stats . nodesExpanded : 0 ;
187192 metrics [ "hub-traversal" ] = hubTraversal ;
188193
194+ // Hub-Avoidance Metrics
195+ // Hub avoidance rate: proportion of expanded nodes that are hubs (50+)
196+ // Using 50+ threshold to capture hubs in academic networks (Cora, CiteSeer)
197+ const hubCount = ( stats . degreeDistribution . get ( "51-100" ) ?? 0 )
198+ + ( stats . degreeDistribution . get ( "101-500" ) ?? 0 )
199+ + ( stats . degreeDistribution . get ( "501-1000" ) ?? 0 )
200+ + ( stats . degreeDistribution . get ( "1000+" ) ?? 0 ) ;
201+ const hubAvoidanceRate = stats . nodesExpanded > 0 ? hubCount / stats . nodesExpanded : 0 ;
202+ metrics [ "hub-avoidance-rate" ] = hubAvoidanceRate ;
203+
204+ // Also track 100+ hub rate separately for large graphs
205+ const hub100Count = ( stats . degreeDistribution . get ( "101-500" ) ?? 0 )
206+ + ( stats . degreeDistribution . get ( "501-1000" ) ?? 0 )
207+ + ( stats . degreeDistribution . get ( "1000+" ) ?? 0 ) ;
208+ const hub100AvoidanceRate = stats . nodesExpanded > 0 ? hub100Count / stats . nodesExpanded : 0 ;
209+ metrics [ "hub-avoidance-rate-100" ] = hub100AvoidanceRate ;
210+
211+ // Peripheral coverage ratio: ratio of peripheral nodes (1-10) to hub nodes (50+)
212+ const peripheralCount = ( stats . degreeDistribution . get ( "1-5" ) ?? 0 )
213+ + ( stats . degreeDistribution . get ( "6-10" ) ?? 0 ) ;
214+ const peripheralCoverageRatio = hubCount > 0 ? peripheralCount / hubCount : peripheralCount ;
215+ metrics [ "peripheral-coverage-ratio" ] = peripheralCoverageRatio ;
216+
189217 // Node coverage: ratio of sampled nodes to total possible
190218 // (This is a placeholder - actual total would come from graph metadata)
191219 metrics [ "node-coverage" ] = result . sampledNodes . size / Math . max ( 1 , result . sampledNodes . size * 2 ) ;
@@ -227,13 +255,63 @@ const runExecutePhase = async (options: EvaluateOptions, sutRegistry: ExpansionS
227255 console . log ( ` - ${ case_ . case . name } (${ case_ . case . caseClass } )` ) ;
228256 }
229257
230- // Configure executor
258+ // Create checkpoint manager for resumable execution
259+ const executeDir = resolve ( options . outputDir , "execute" ) ;
260+ if ( ! existsSync ( executeDir ) ) {
261+ mkdirSync ( executeDir , { recursive : true } ) ;
262+ }
263+
264+ // Create checkpoint based on mode
265+ // For file mode, use full path to checkpoint.json
266+ // For git mode, use namespace string
267+ const checkpointMode = options . checkpointMode as "file" | "git" | "auto" ;
268+ const checkpoint = createCheckpointManager (
269+ checkpointMode === "git" || checkpointMode === "auto"
270+ ? { mode : checkpointMode , pathOrNamespace : "results-execute" }
271+ : { mode : "file" , pathOrNamespace : resolve ( executeDir , "checkpoint.json" ) }
272+ ) ;
273+ await checkpoint . load ( ) ;
274+
275+ // Get git commit for reproducibility
276+ const gitCommit = await getGitCommit ( ) ;
277+
278+ // Calculate total planned runs
279+ const totalPlanned = suts . length * cases . length * options . repetitions ;
280+
281+ // Check if checkpoint is stale (configuration changed)
231282 const executorConfig : Partial < ExecutorConfig > = {
232283 repetitions : options . repetitions ,
233284 seedBase : options . seedBase ,
234285 continueOnError : options . continueOnError ,
235286 timeoutMs : options . timeoutMs ,
236287 collectProvenance : options . collectProvenance ,
288+ } ;
289+
290+ const isStale = checkpoint . isStale ( suts , cases , executorConfig , totalPlanned ) ;
291+
292+ if ( checkpoint . exists ( ) ) {
293+ if ( isStale ) {
294+ console . log ( "\n⚠ Checkpoint exists but configuration has changed." ) ;
295+ console . log ( " Invalidating checkpoint and starting fresh." ) ;
296+ checkpoint . invalidate ( ) ;
297+ } else {
298+ const progress = checkpoint . getProgress ( ) ;
299+ console . log ( `\n✓ Checkpoint found: ${ progress . completed } /${ progress . total } runs completed (${ progress . percent } %)` ) ;
300+ console . log ( " Resuming from checkpoint..." ) ;
301+ }
302+ } else {
303+ // Initialize new checkpoint
304+ checkpoint . initializeEmpty ( suts , cases , executorConfig , totalPlanned , gitCommit ) ;
305+ await checkpoint . save ( ) ;
306+ console . log ( "\n Created new checkpoint (will save incrementally)" ) ;
307+ }
308+
309+ // Collect completed results from checkpoint
310+ const completedResults = checkpoint . getResults ( ) ;
311+
312+ // Configure executor with checkpoint callback
313+ const executorConfigWithCallbacks : Partial < ExecutorConfig > = {
314+ ...executorConfig ,
237315 onProgress : ( progress ) => {
238316 if ( options . verbose ) {
239317 console . log (
@@ -243,38 +321,46 @@ const runExecutePhase = async (options: EvaluateOptions, sutRegistry: ExpansionS
243321 process . stdout . write ( `\r [${ progress . completed } /${ progress . total } ] Complete...` ) ;
244322 }
245323 } ,
324+ onResult : async ( result ) => {
325+ // Save checkpoint incrementally after each result
326+ await checkpoint . saveIncremental ( result ) ;
327+ } ,
246328 } ;
247329
248- const executor = createExecutor < BenchmarkGraphExpander , DegreePrioritisedExpansionResult > ( executorConfig ) ;
330+ const executor = createExecutor < BenchmarkGraphExpander , DegreePrioritisedExpansionResult > ( executorConfigWithCallbacks ) ;
249331
250- // Execute experiments
251- console . log ( "\nRunning experiments..." ) ;
252- const summary = await executor . execute ( suts , cases , extractMetrics ) ;
332+ // Plan all runs and filter out completed ones
333+ const allPlanned = executor . plan ( suts , cases ) ;
334+ const remainingRuns = checkpoint . filterRemaining ( allPlanned ) ;
253335
254- // Write results to file
255- const executeDir = resolve ( options . outputDir , "execute" ) ;
256- if ( ! existsSync ( executeDir ) ) {
257- mkdirSync ( executeDir , { recursive : true } ) ;
336+ if ( remainingRuns . length === 0 ) {
337+ console . log ( "\n\nAll runs already completed. Skipping execution." ) ;
338+ } else {
339+ console . log ( `\nRunning experiments... (${ remainingRuns . length } remaining, ${ completedResults . length } cached)` ) ;
340+
341+ // Execute all runs - checkpoint will track which are already done
342+ await executor . execute ( suts , cases , extractMetrics ) ;
258343 }
259344
345+ // Merge all results (completed from checkpoint + new)
346+ const allResults = [ ...checkpoint . getResults ( ) ] ;
347+
348+ // Write results to file
260349 const resultsFile = resolve ( executeDir , "evaluation-results.json" ) ;
261- writeFileSync ( resultsFile , JSON . stringify ( summary , null , 2 ) , "utf-8" ) ;
350+ const finalSummary = {
351+ totalRuns : allResults . length ,
352+ successfulRuns : allResults . length ,
353+ failedRuns : 0 ,
354+ elapsedMs : 0 , // Would need to track across runs
355+ results : allResults ,
356+ } ;
357+ writeFileSync ( resultsFile , JSON . stringify ( finalSummary , null , 2 ) , "utf-8" ) ;
262358
263359 console . log ( `\n\nResults written to: ${ resultsFile } ` ) ;
264- console . log ( ` Total runs: ${ summary . totalRuns } ` ) ;
265- console . log ( ` Successful: ${ summary . successfulRuns } ` ) ;
266- console . log ( ` Failed: ${ summary . failedRuns } ` ) ;
267- console . log ( ` Elapsed: ${ ( summary . elapsedMs / 1000 ) . toFixed ( 2 ) } s` ) ;
268-
269- if ( summary . errors . length > 0 ) {
270- console . log ( "\nErrors encountered:" ) ;
271- for ( const error of summary . errors . slice ( 0 , 5 ) ) {
272- console . log ( ` - ${ error . runId } : ${ error . error } ` ) ;
273- }
274- if ( summary . errors . length > 5 ) {
275- console . log ( ` ... and ${ summary . errors . length - 5 } more` ) ;
276- }
277- }
360+ console . log ( ` Total runs: ${ allResults . length } ` ) ;
361+ console . log ( ` From checkpoint: ${ completedResults . length } ` ) ;
362+ console . log ( ` New this run: ${ allResults . length - completedResults . length } ` ) ;
363+ console . log ( `\n${ checkpoint . getSummary ( ) } ` ) ;
278364} ;
279365
280366/**
0 commit comments