@@ -828,14 +828,79 @@ export class MetricsCalculator {
828828 const existingLabels = new Map < string , ButtonMetrics > ( ) ;
829829 buttons . forEach ( ( btn ) => existingLabels . set ( btn . label . toLowerCase ( ) , btn ) ) ;
830830
831+ // Build a map of POS tags from ALL tree buttons, keyed by lowercase label.
832+ // This ensures words on BFS-unreachable pages still contribute POS data.
833+ const treePosMap = new Map < string , string > ( ) ;
834+ const treePredictionsMap = new Map < string , string [ ] > ( ) ;
835+ Object . values ( tree . pages ) . forEach ( ( page : AACPage ) => {
836+ page . grid . forEach ( ( row : ( AACButton | null ) [ ] ) => {
837+ row . forEach ( ( btn : AACButton | null ) => {
838+ if ( ! btn || ! btn . label ) return ;
839+ const lower = btn . label . toLowerCase ( ) ;
840+ if ( btn . pos && btn . pos !== 'Unknown' && btn . pos !== 'Ignore' ) {
841+ treePosMap . set ( lower , btn . pos ) ;
842+ }
843+ if ( btn . predictions && btn . predictions . length > 0 ) {
844+ const existing = treePredictionsMap . get ( lower ) ;
845+ if ( ! existing || btn . predictions . length > existing . length ) {
846+ treePredictionsMap . set ( lower , btn . predictions ) ;
847+ }
848+ }
849+ } ) ;
850+ } ) ;
851+ } ) ;
852+
853+ // For metrics buttons that lack POS but have a tree counterpart with POS,
854+ // propagate the POS tag so it's available in the output.
855+ buttons . forEach ( ( btn ) => {
856+ const lower = btn . label . toLowerCase ( ) ;
857+ if ( ! btn . pos || btn . pos === 'Unknown' || btn . pos === 'Ignore' ) {
858+ const treePos = treePosMap . get ( lower ) ;
859+ if ( treePos ) btn . pos = treePos ;
860+ }
861+ } ) ;
862+
863+ // For POS-tagged tree buttons that have predictions but are NOT in the
864+ // BFS-reachable metrics set, create synthetic metrics entries so their
865+ // word forms can still be generated. This handles words like "run" that
866+ // exist only on topic pages the BFS doesn't reach.
867+ const processedPredictionLabels = new Set < string > ( ) ;
868+ Object . values ( tree . pages ) . forEach ( ( page : AACPage ) => {
869+ page . grid . forEach ( ( row : ( AACButton | null ) [ ] ) => {
870+ row . forEach ( ( btn : AACButton | null ) => {
871+ if ( ! btn || ! btn . label || ! btn . predictions || btn . predictions . length === 0 ) return ;
872+ const lower = btn . label . toLowerCase ( ) ;
873+ if ( existingLabels . has ( lower ) ) return ; // Already in metrics
874+ if ( processedPredictionLabels . has ( lower ) ) return ; // Already synthesized
875+ processedPredictionLabels . add ( lower ) ;
876+
877+ // Create a synthetic metrics entry using a high effort estimate
878+ // (the user would need to navigate to this page to access the button)
879+ const syntheticMetrics : ButtonMetrics = {
880+ id : btn . id || `synthetic_${ lower } ` ,
881+ label : btn . label ,
882+ level : 3 , // Approximate depth for unreachable pages
883+ effort : 20 , // High effort — deep navigation required
884+ count : 1 ,
885+ pos : btn . pos ,
886+ } ;
887+ buttons . push ( syntheticMetrics ) ;
888+ existingLabels . set ( lower , syntheticMetrics ) ;
889+ } ) ;
890+ } ) ;
891+ } ) ;
892+
831893 // Iterate through all pages to find buttons with predictions
832894 Object . values ( tree . pages ) . forEach ( ( page : AACPage ) => {
833895 page . grid . forEach ( ( row : ( AACButton | null ) [ ] ) => {
834896 row . forEach ( ( btn : AACButton | null ) => {
835897 if ( ! btn || ! btn . predictions || btn . predictions . length === 0 ) return ;
836898
837- // Find the parent button's metrics
838- const parentMetrics = buttons . find ( ( b ) => b . id === btn . id ) ;
899+ // Find the parent button's metrics (by id first, then by label)
900+ let parentMetrics = buttons . find ( ( b ) => b . id === btn . id ) ;
901+ if ( ! parentMetrics && btn . label ) {
902+ parentMetrics = existingLabels . get ( btn . label . toLowerCase ( ) ) ;
903+ }
839904 if ( ! parentMetrics ) return ;
840905
841906 // Calculate effort for each word form
0 commit comments