BhargavMN commited on
Commit
0acfbcd
·
1 Parent(s): 01da2ab

data set for fine tuning

Browse files
.gitignore CHANGED
@@ -137,3 +137,11 @@ scripts/tag/run_generation.py
137
  scripts/tag/test_pipeline.py
138
  scripts/tag/validate_dataset.py
139
  scripts/tag/validator.py
 
 
 
 
 
 
 
 
 
137
  scripts/tag/test_pipeline.py
138
  scripts/tag/validate_dataset.py
139
  scripts/tag/validator.py
140
+ prompts/dataset_generation_context.md
141
+ scripts/common/batching.py
142
+ scripts/common/test_batching.py
143
+ scripts/scavenger_hunt/validator.py
144
+ scripts/scavenger_hunt/test_pipeline.py
145
+ scripts/scavenger_hunt/generator.py
146
+ scripts/common/cli.py
147
+ scripts/.batch_key_state.json
app/data/hide_and_seek/dataset.json CHANGED
The diff for this file is too large to render. See raw diff
 
app/data/scavenger_hunt/dataset.json CHANGED
The diff for this file is too large to render. See raw diff
 
app/data/tag/dataset.json CHANGED
@@ -2789,5 +2789,899 @@
2789
  "estimated_total_time_minutes": 45,
2790
  "quality_score": 4.8
2791
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2792
  }
2793
  ]
 
2789
  "estimated_total_time_minutes": 45,
2790
  "quality_score": 4.8
2791
  }
2792
+ },
2793
+ {
2794
+ "id": "TG-LOS-0023",
2795
+ "input": {
2796
+ "game_type": "tag",
2797
+ "location": {
2798
+ "city": "Lagos",
2799
+ "country": "Nigeria",
2800
+ "city_code": "LOS",
2801
+ "landscape_tags": [
2802
+ "market_outdoor",
2803
+ "dense_urban_grid",
2804
+ "coastal_waterfront",
2805
+ "food_market"
2806
+ ],
2807
+ "urban_density": "hyper_dense",
2808
+ "climate_zone": "tropical",
2809
+ "area_type": "waterfront"
2810
+ },
2811
+ "players": {
2812
+ "count": 10,
2813
+ "team_count": 1,
2814
+ "age_group": "mixed_family",
2815
+ "mobility": "standard"
2816
+ },
2817
+ "preferences": {
2818
+ "duration_minutes": 120,
2819
+ "difficulty": "easy",
2820
+ "theme": "urban_exploration",
2821
+ "allow_transport": false
2822
+ }
2823
+ },
2824
+ "output": {
2825
+ "game_title": "The Waterfront Market Chase",
2826
+ "rules": {
2827
+ "objective": "Avoid being tagged by 'It' players by reaching designated safe zones.",
2828
+ "tag_variant": "safe_zone_tag",
2829
+ "it_count": 2,
2830
+ "round_count": 6,
2831
+ "round_time_minutes": 20,
2832
+ "tag_mechanic": "A tag is made by an open-hand touch on any part of the body, accompanied by a clear verbal call-out of 'Tag!'. Grabbing, pushing, or pulling clothing is strictly prohibited.",
2833
+ "unfreeze_or_revival_rule": "Tagged players are out for the current round.",
2834
+ "win_condition": "Be the last player untagged when the round time expires."
2835
+ },
2836
+ "arena": {
2837
+ "boundary_description": "The play area is contained within the bustling outdoor market, bordered by the grid thoroughfares on two sides and the coastal waterfront promenade on another. The food market stalls delineate an inner boundary.",
2838
+ "boundary_landscape_tags_used": [
2839
+ "market_outdoor",
2840
+ "dense_urban_grid",
2841
+ "coastal_waterfront",
2842
+ "food_market"
2843
+ ],
2844
+ "arena_size_tier": "medium",
2845
+ "out_of_bounds": [
2846
+ "Beyond the market boundaries",
2847
+ "into active roadways of the dense urban grid",
2848
+ "any entry into the coastal waterfront or its immediate banks"
2849
+ ]
2850
+ },
2851
+ "safe_zones": [
2852
+ {
2853
+ "zone_id": "S01",
2854
+ "description": "The raised platform near the large market entrance.",
2855
+ "landscape_tags_used": [
2856
+ "market_outdoor"
2857
+ ],
2858
+ "capacity_rule": "Max 3 players, 15-second limit.",
2859
+ "safety_flags": [
2860
+ "watch for tripping hazards"
2861
+ ]
2862
+ },
2863
+ {
2864
+ "zone_id": "S02",
2865
+ "description": "The covered seating area within the food market.",
2866
+ "landscape_tags_used": [
2867
+ "food_market"
2868
+ ],
2869
+ "capacity_rule": "Max 3 players, 15-second limit.",
2870
+ "safety_flags": [
2871
+ "ensure clear path to exit"
2872
+ ]
2873
+ }
2874
+ ],
2875
+ "movement_features": {
2876
+ "chokepoints": [
2877
+ "Narrow aisles between market stalls",
2878
+ "congested pathways within the food market",
2879
+ "pedestrian crossings at the edge of the dense urban grid"
2880
+ ],
2881
+ "open_zones": [
2882
+ "The wide open area at the heart of the outdoor market",
2883
+ "the extended coastal waterfront promenade"
2884
+ ],
2885
+ "hazard_zones": [
2886
+ "Coastal water edges, which may have slippery surfaces due to spray",
2887
+ "active vehicle roadways within the dense urban grid",
2888
+ "crowded market aisles with potential for bumping and tripping hazards from goods"
2889
+ ]
2890
+ },
2891
+ "safety_constraints": {
2892
+ "exclusion_zones": [
2893
+ "private_property",
2894
+ "active_roadway",
2895
+ "construction_sites",
2896
+ "restricted_government_buildings",
2897
+ "water_edge"
2898
+ ],
2899
+ "physical_limits": [
2900
+ "no grabbing or pushing — tag is an open-hand touch only",
2901
+ "no climbing",
2902
+ "no jumping",
2903
+ "no water entry",
2904
+ "no entering buildings",
2905
+ "no running across any roadway"
2906
+ ],
2907
+ "collision_safety_note": "Players must be highly aware of other pedestrians and market stalls in this hyper-dense environment. Exercise caution for sudden stops, blind corners, and potential tripping hazards from goods on the ground in market aisles.",
2908
+ "adult_supervision_required": true,
2909
+ "notes": "Tropical climate: Players must stay well-hydrated and seek shade frequently. Adult supervisors should ensure shade access is available within 10 minutes of any point in the arena. Be aware of slippery surfaces near the waterfront."
2910
+ },
2911
+ "scoring": {
2912
+ "method": "survival_time",
2913
+ "winning_condition_detail": "The player who remains untagged for the longest cumulative time across all rounds wins the game."
2914
+ },
2915
+ "estimated_total_time_minutes": 120,
2916
+ "quality_score": 4.7
2917
+ }
2918
+ },
2919
+ {
2920
+ "id": "TG-BOM-0024",
2921
+ "input": {
2922
+ "game_type": "tag",
2923
+ "location": {
2924
+ "city": "Mumbai",
2925
+ "country": "India",
2926
+ "city_code": "BOM",
2927
+ "landscape_tags": [
2928
+ "market_outdoor",
2929
+ "religious_site_accessible",
2930
+ "dense_urban_grid",
2931
+ "coastal_waterfront",
2932
+ "food_market"
2933
+ ],
2934
+ "urban_density": "hyper_dense",
2935
+ "climate_zone": "tropical",
2936
+ "area_type": "city_center"
2937
+ },
2938
+ "players": {
2939
+ "count": 16,
2940
+ "team_count": 1,
2941
+ "age_group": "teens",
2942
+ "mobility": "standard"
2943
+ },
2944
+ "preferences": {
2945
+ "duration_minutes": 60,
2946
+ "difficulty": "hard",
2947
+ "theme": "urban_exploration",
2948
+ "allow_transport": false
2949
+ }
2950
+ },
2951
+ "output": {
2952
+ "game_title": "Urban Grid Chain Chase",
2953
+ "rules": {
2954
+ "objective": "Avoid being tagged by the 'It' chain; tagged players join the chain to make it longer.",
2955
+ "tag_variant": "chain_tag",
2956
+ "it_count": 2,
2957
+ "round_count": 4,
2958
+ "round_time_minutes": 15,
2959
+ "tag_mechanic": "A tag is made by an open-hand touch on any part of the body, accompanied by a clear verbal call-out of 'Tag!'. Grabbing, pushing, or pulling clothing is strictly prohibited.",
2960
+ "unfreeze_or_revival_rule": "Tagged players immediately join the 'It' chain by holding hands with a player already in the chain. The chain must remain connected.",
2961
+ "win_condition": "The last player(s) not part of the 'It' chain when the round time expires wins the round."
2962
+ },
2963
+ "arena": {
2964
+ "boundary_description": "The expansive play area encompasses a dynamic outdoor market, a publicly accessible religious site, and extends through parts of the dense urban grid, reaching towards the coastal waterfront promenade. The food market sections are within the boundaries.",
2965
+ "boundary_landscape_tags_used": [
2966
+ "market_outdoor",
2967
+ "religious_site_accessible",
2968
+ "dense_urban_grid",
2969
+ "coastal_waterfront",
2970
+ "food_market"
2971
+ ],
2972
+ "arena_size_tier": "wide",
2973
+ "out_of_bounds": [
2974
+ "Beyond the designated market perimeter",
2975
+ "crossing into active vehicle traffic lanes of the dense urban grid",
2976
+ "entering private property",
2977
+ "any entry into the coastal waterfront or its immediate banks",
2978
+ "inside religious building interiors"
2979
+ ]
2980
+ },
2981
+ "safe_zones": [
2982
+ {
2983
+ "zone_id": "S01",
2984
+ "description": "The shaded courtyard of the accessible religious site.",
2985
+ "landscape_tags_used": [
2986
+ "religious_site_accessible"
2987
+ ],
2988
+ "capacity_rule": "Max 4 players, 10-second limit.",
2989
+ "safety_flags": [
2990
+ "respect quiet zones"
2991
+ ]
2992
+ },
2993
+ {
2994
+ "zone_id": "S02",
2995
+ "description": "A designated open space within the outdoor food market.",
2996
+ "landscape_tags_used": [
2997
+ "food_market",
2998
+ "market_outdoor"
2999
+ ],
3000
+ "capacity_rule": "Max 4 players, 10-second limit.",
3001
+ "safety_flags": [
3002
+ "watch for uneven surfaces"
3003
+ ]
3004
+ }
3005
+ ],
3006
+ "movement_features": {
3007
+ "chokepoints": [
3008
+ "Narrow alleyways within the dense urban grid",
3009
+ "congested paths between outdoor market stalls",
3010
+ "archways leading into the religious site's courtyards"
3011
+ ],
3012
+ "open_zones": [
3013
+ "the square area of the outdoor market",
3014
+ "the open plaza near the religious site entrance",
3015
+ "sections of the coastal waterfront promenade"
3016
+ ],
3017
+ "hazard_zones": [
3018
+ "Coastal water edges, which can be slippery due to spray or algae",
3019
+ "active roadways with vehicle traffic within the dense urban grid",
3020
+ "crowded market areas with potential for collisions and uneven ground",
3021
+ "steep steps or ramps within the religious site area"
3022
+ ]
3023
+ },
3024
+ "safety_constraints": {
3025
+ "exclusion_zones": [
3026
+ "private_property",
3027
+ "active_roadway",
3028
+ "construction_sites",
3029
+ "restricted_government_buildings",
3030
+ "water_edge",
3031
+ "religious_interiors"
3032
+ ],
3033
+ "physical_limits": [
3034
+ "no grabbing or pushing — tag is an open-hand touch only",
3035
+ "no climbing",
3036
+ "no jumping",
3037
+ "no water entry",
3038
+ "no entering buildings",
3039
+ "no running across any roadway"
3040
+ ],
3041
+ "collision_safety_note": "Players must be extremely vigilant for other pedestrians, moving market carts, and uneven ground in the hyper-dense market and urban grid. Maintain awareness of blind corners in alleyways and avoid running near the steep drops at the waterfront edge.",
3042
+ "adult_supervision_required": false,
3043
+ "notes": "Tropical climate: Players must stay well-hydrated and seek shade frequently. Require shade access to be available within 10 minutes of any point in the arena. Be aware of slippery surfaces near the waterfront and potential crowded conditions."
3044
+ },
3045
+ "scoring": {
3046
+ "method": "last_untagged_wins",
3047
+ "winning_condition_detail": "The player who is the last one not part of the 'It' chain at the end of the final round wins the game."
3048
+ },
3049
+ "estimated_total_time_minutes": 60,
3050
+ "quality_score": 4.6
3051
+ }
3052
+ },
3053
+ {
3054
+ "id": "TG-BOM-0025",
3055
+ "input": {
3056
+ "game_type": "tag",
3057
+ "location": {
3058
+ "city": "Mumbai",
3059
+ "country": "India",
3060
+ "city_code": "BOM",
3061
+ "landscape_tags": [
3062
+ "religious_site_accessible",
3063
+ "dense_urban_grid",
3064
+ "coastal_waterfront"
3065
+ ],
3066
+ "urban_density": "hyper_dense",
3067
+ "climate_zone": "tropical",
3068
+ "area_type": "waterfront"
3069
+ },
3070
+ "players": {
3071
+ "count": 14,
3072
+ "team_count": 1,
3073
+ "age_group": "mixed_family",
3074
+ "mobility": "standard"
3075
+ },
3076
+ "preferences": {
3077
+ "duration_minutes": 90,
3078
+ "difficulty": "medium",
3079
+ "theme": "nature",
3080
+ "allow_transport": false
3081
+ }
3082
+ },
3083
+ "output": {
3084
+ "game_title": "Waterfront Freeze Pursuit",
3085
+ "rules": {
3086
+ "objective": "Avoid being tagged by 'It' players; unfreeze teammates to keep the game going.",
3087
+ "tag_variant": "freeze_tag",
3088
+ "it_count": 2,
3089
+ "round_count": 5,
3090
+ "round_time_minutes": 18,
3091
+ "tag_mechanic": "A tag is made by an open-hand touch on any part of the body, accompanied by a clear verbal call-out of 'Tag!'. Grabbing, pushing, or pulling clothing is strictly prohibited.",
3092
+ "unfreeze_or_revival_rule": "Frozen players must stand still until unfrozen by a teammate's open-hand touch.",
3093
+ "win_condition": "Be the last player unfrozen when the round time expires."
3094
+ },
3095
+ "arena": {
3096
+ "boundary_description": "The play area spans a section of the dense urban grid, including a publicly accessible religious site and extending along the coastal waterfront promenade.",
3097
+ "boundary_landscape_tags_used": [
3098
+ "religious_site_accessible",
3099
+ "dense_urban_grid",
3100
+ "coastal_waterfront"
3101
+ ],
3102
+ "arena_size_tier": "medium",
3103
+ "out_of_bounds": [
3104
+ "Beyond the urban grid's pedestrian zones",
3105
+ "into active vehicle roadways",
3106
+ "any entry into the coastal waterfront or its immediate banks",
3107
+ "inside religious building interiors"
3108
+ ]
3109
+ },
3110
+ "safe_zones": [
3111
+ {
3112
+ "zone_id": "S01",
3113
+ "description": "The shaded veranda within the accessible religious site.",
3114
+ "landscape_tags_used": [
3115
+ "religious_site_accessible"
3116
+ ],
3117
+ "capacity_rule": "Max 3 players, 10-second limit.",
3118
+ "safety_flags": [
3119
+ "respect quiet zones"
3120
+ ]
3121
+ },
3122
+ {
3123
+ "zone_id": "S02",
3124
+ "description": "A designated seating area on the waterfront promenade.",
3125
+ "landscape_tags_used": [
3126
+ "coastal_waterfront"
3127
+ ],
3128
+ "capacity_rule": "Max 3 players, 10-second limit.",
3129
+ "safety_flags": [
3130
+ "watch for slippery surfaces near water"
3131
+ ]
3132
+ }
3133
+ ],
3134
+ "movement_features": {
3135
+ "chokepoints": [
3136
+ "Narrow passages between buildings in the dense urban grid",
3137
+ "archways leading into the religious site",
3138
+ "gaps between vendors along the waterfront"
3139
+ ],
3140
+ "open_zones": [
3141
+ "The open plaza area of the religious site",
3142
+ "the wide sections of the coastal waterfront promenade"
3143
+ ],
3144
+ "hazard_zones": [
3145
+ "Coastal water edges, which can be slippery due to spray or algae",
3146
+ "active roadways with fast-moving traffic within the dense urban grid",
3147
+ "uneven stone pathways and steps within the religious site"
3148
+ ]
3149
+ },
3150
+ "safety_constraints": {
3151
+ "exclusion_zones": [
3152
+ "private_property",
3153
+ "active_roadway",
3154
+ "construction_sites",
3155
+ "restricted_government_buildings",
3156
+ "water_edge",
3157
+ "religious_interiors"
3158
+ ],
3159
+ "physical_limits": [
3160
+ "no grabbing or pushing — tag is an open-hand touch only",
3161
+ "no climbing",
3162
+ "no jumping",
3163
+ "no water entry",
3164
+ "no entering buildings",
3165
+ "no running across any roadway"
3166
+ ],
3167
+ "collision_safety_note": "Players must maintain constant awareness of other pedestrians and obstacles in the dense urban grid and along the waterfront. Be cautious of blind corners and sudden changes in elevation near the religious site, and slippery surfaces near the water's edge.",
3168
+ "adult_supervision_required": true,
3169
+ "notes": "Tropical climate: Players must stay well-hydrated and seek shade frequently. Adult supervisors should ensure shade access is available within 10 minutes of any point in the arena. Be aware of slippery surfaces near the waterfront."
3170
+ },
3171
+ "scoring": {
3172
+ "method": "survival_time",
3173
+ "winning_condition_detail": "The player who remains unfrozen for the longest cumulative time across all rounds wins the game."
3174
+ },
3175
+ "estimated_total_time_minutes": 90,
3176
+ "quality_score": 4.6
3177
+ }
3178
+ },
3179
+ {
3180
+ "id": "TG-IST-0026",
3181
+ "input": {
3182
+ "game_type": "tag",
3183
+ "location": {
3184
+ "city": "Istanbul",
3185
+ "country": "Turkey",
3186
+ "city_code": "IST",
3187
+ "landscape_tags": [
3188
+ "plaza_or_square",
3189
+ "historic_district",
3190
+ "market_covered",
3191
+ "hill_or_elevation"
3192
+ ],
3193
+ "urban_density": "dense",
3194
+ "climate_zone": "mediterranean",
3195
+ "area_type": "city_center"
3196
+ },
3197
+ "players": {
3198
+ "count": 10,
3199
+ "team_count": 1,
3200
+ "age_group": "adults",
3201
+ "mobility": "limited"
3202
+ },
3203
+ "preferences": {
3204
+ "duration_minutes": 120,
3205
+ "difficulty": "medium",
3206
+ "theme": "urban_exploration",
3207
+ "allow_transport": false
3208
+ }
3209
+ },
3210
+ "output": {
3211
+ "game_title": "Historic District Freeze Out",
3212
+ "rules": {
3213
+ "objective": "Avoid being tagged by 'It' players and unfreeze your teammates.",
3214
+ "tag_variant": "freeze_tag",
3215
+ "it_count": 2,
3216
+ "round_count": 6,
3217
+ "round_time_minutes": 20,
3218
+ "tag_mechanic": "A tag is made by an open-hand touch on any part of the body, accompanied by a clear verbal call-out of 'Tag!'. Grabbing, pushing, or pulling clothing is strictly prohibited.",
3219
+ "unfreeze_or_revival_rule": "Frozen players must stand still until unfrozen by a teammate's open-hand touch.",
3220
+ "win_condition": "Be the last player unfrozen when the round time expires."
3221
+ },
3222
+ "arena": {
3223
+ "boundary_description": "The play area is defined by the pedestrian zones of a historic district, encompassing a grand plaza, a bustling covered market, and the base of a significant elevation.",
3224
+ "boundary_landscape_tags_used": [
3225
+ "plaza_or_square",
3226
+ "historic_district",
3227
+ "market_covered",
3228
+ "hill_or_elevation"
3229
+ ],
3230
+ "arena_size_tier": "medium",
3231
+ "out_of_bounds": [
3232
+ "Beyond the designated pedestrian pathways",
3233
+ "into active vehicle roadways",
3234
+ "inside private establishments within the covered market"
3235
+ ]
3236
+ },
3237
+ "safe_zones": [
3238
+ {
3239
+ "zone_id": "S01",
3240
+ "description": "the fountain area within the large plaza.",
3241
+ "landscape_tags_used": [
3242
+ "plaza_or_square"
3243
+ ],
3244
+ "capacity_rule": "Max 2 players, 10-second limit.",
3245
+ "safety_flags": [
3246
+ "watch for uneven paving"
3247
+ ]
3248
+ },
3249
+ {
3250
+ "zone_id": "S02",
3251
+ "description": "A designated waiting area inside the covered market.",
3252
+ "landscape_tags_used": [
3253
+ "market_covered"
3254
+ ],
3255
+ "capacity_rule": "Max 2 players, 10-second limit.",
3256
+ "safety_flags": [
3257
+ "mind your step on inclines"
3258
+ ]
3259
+ }
3260
+ ],
3261
+ "movement_features": {
3262
+ "chokepoints": [
3263
+ "Narrow alleys within the historic district",
3264
+ "entrances and exits of the covered market",
3265
+ "steep, winding passages leading up the hill or elevation"
3266
+ ],
3267
+ "open_zones": [
3268
+ "The expansive open space of the plaza or square",
3269
+ "the wide corridors within the covered market"
3270
+ ],
3271
+ "hazard_zones": [
3272
+ "Steep inclines and declines with potential for slips",
3273
+ "uneven cobblestone surfaces in the historic district",
3274
+ "crowded passages within the covered market with low visibility"
3275
+ ]
3276
+ },
3277
+ "safety_constraints": {
3278
+ "exclusion_zones": [
3279
+ "private_property",
3280
+ "active_roadway",
3281
+ "construction_sites",
3282
+ "restricted_government_buildings"
3283
+ ],
3284
+ "physical_limits": [
3285
+ "no grabbing or pushing — tag is an open-hand touch only",
3286
+ "no climbing",
3287
+ "no jumping",
3288
+ "no water entry",
3289
+ "no entering buildings",
3290
+ "no running across any roadway"
3291
+ ],
3292
+ "collision_safety_note": "Players must be aware of other pedestrians and market stalls. Exercise caution on uneven historic paving, especially on slopes, and navigate crowded covered market areas carefully, watching for blind corners and low-hanging objects.",
3293
+ "adult_supervision_required": false,
3294
+ "notes": "Mediterranean climate: no special constraints regarding temperature; however, players should stay hydrated. Be mindful of uneven surfaces and potential slips on inclines and declines."
3295
+ },
3296
+ "scoring": {
3297
+ "method": "survival_time",
3298
+ "winning_condition_detail": "The player who remains unfrozen for the longest cumulative time across all rounds wins the game."
3299
+ },
3300
+ "estimated_total_time_minutes": 120,
3301
+ "quality_score": 4.4
3302
+ }
3303
+ },
3304
+ {
3305
+ "id": "TG-CPT-0027",
3306
+ "input": {
3307
+ "game_type": "tag",
3308
+ "location": {
3309
+ "city": "Cape Town",
3310
+ "country": "South Africa",
3311
+ "city_code": "CPT",
3312
+ "landscape_tags": [
3313
+ "historic_district",
3314
+ "market_outdoor",
3315
+ "hill_or_elevation",
3316
+ "coastal_waterfront",
3317
+ "garden_formal"
3318
+ ],
3319
+ "urban_density": "mixed",
3320
+ "climate_zone": "mediterranean",
3321
+ "area_type": "mixed_residential"
3322
+ },
3323
+ "players": {
3324
+ "count": 5,
3325
+ "team_count": 2,
3326
+ "age_group": "mixed_family",
3327
+ "mobility": "standard"
3328
+ },
3329
+ "preferences": {
3330
+ "duration_minutes": 90,
3331
+ "difficulty": "medium",
3332
+ "theme": "social",
3333
+ "allow_transport": true
3334
+ }
3335
+ },
3336
+ "output": {
3337
+ "game_title": "Coastal Garden Freeze",
3338
+ "rules": {
3339
+ "objective": "As a team, avoid being tagged by the 'It' player and work together to unfreeze teammates.",
3340
+ "tag_variant": "freeze_tag",
3341
+ "it_count": 1,
3342
+ "round_count": 5,
3343
+ "round_time_minutes": 18,
3344
+ "tag_mechanic": "A tag is made by an open-hand touch on any part of the body, accompanied by a clear verbal call-out of 'Tag!'. Grabbing, pushing, or pulling clothing is strictly prohibited.",
3345
+ "unfreeze_or_revival_rule": "Frozen players must stand still until unfrozen by a teammate's open-hand touch.",
3346
+ "win_condition": "The team with the most unfrozen players at the end of the round or the last team with players remaining wins the round."
3347
+ },
3348
+ "arena": {
3349
+ "boundary_description": "The wide play area encompasses a historic district, a sprawling outdoor market, a formal garden, and extends to the coastal waterfront, including paths that traverse minor hills or elevations.",
3350
+ "boundary_landscape_tags_used": [
3351
+ "historic_district",
3352
+ "market_outdoor",
3353
+ "hill_or_elevation",
3354
+ "coastal_waterfront",
3355
+ "garden_formal"
3356
+ ],
3357
+ "arena_size_tier": "wide",
3358
+ "out_of_bounds": [
3359
+ "Beyond the formal garden fences",
3360
+ "into active vehicle roadways",
3361
+ "into private residential areas",
3362
+ "any entry into the coastal waterfront or its immediate banks"
3363
+ ]
3364
+ },
3365
+ "safe_zones": [
3366
+ {
3367
+ "zone_id": "S01",
3368
+ "description": "The gazebo structure within the formal garden.",
3369
+ "landscape_tags_used": [
3370
+ "garden_formal"
3371
+ ],
3372
+ "capacity_rule": "Max 2 players, 10-second limit.",
3373
+ "safety_flags": [
3374
+ "watch for slippery steps"
3375
+ ]
3376
+ },
3377
+ {
3378
+ "zone_id": "S02",
3379
+ "description": "A designated shaded area within the outdoor market.",
3380
+ "landscape_tags_used": [
3381
+ "market_outdoor"
3382
+ ],
3383
+ "capacity_rule": "Max 2 players, 10-second limit.",
3384
+ "safety_flags": [
3385
+ "clear path to exit"
3386
+ ]
3387
+ }
3388
+ ],
3389
+ "movement_features": {
3390
+ "chokepoints": [
3391
+ "Narrow pedestrian lanes in the historic district",
3392
+ "paths winding through the market stalls",
3393
+ "steep, winding paths on the hill or elevation",
3394
+ "archways leading into the formal garden"
3395
+ ],
3396
+ "open_zones": [
3397
+ "The wide open plaza within the historic district",
3398
+ "the square of the outdoor market",
3399
+ "the expansive lawns of the formal garden",
3400
+ "the coastal waterfront promenade"
3401
+ ],
3402
+ "hazard_zones": [
3403
+ "Coastal water edges, which can be slippery or have sudden drops",
3404
+ "steep, uneven slopes and steps on the hills",
3405
+ "crowded market pathways with potential for collisions and tripping hazards from goods"
3406
+ ]
3407
+ },
3408
+ "safety_constraints": {
3409
+ "exclusion_zones": [
3410
+ "private_property",
3411
+ "active_roadway",
3412
+ "construction_sites",
3413
+ "restricted_government_buildings",
3414
+ "water_edge"
3415
+ ],
3416
+ "physical_limits": [
3417
+ "no grabbing or pushing — tag is an open-hand touch only",
3418
+ "no climbing",
3419
+ "no jumping",
3420
+ "no water entry",
3421
+ "no entering buildings",
3422
+ "no running across any roadway"
3423
+ ],
3424
+ "collision_safety_note": "Players should be cautious of uneven ground and steep slopes, especially when running downhill. In crowded market areas, be mindful of other players and market structures. Watch for slippery surfaces near the waterfront.",
3425
+ "adult_supervision_required": true,
3426
+ "notes": "Mediterranean climate: no special constraints regarding temperature; however, players should stay hydrated. Be mindful of uneven surfaces on hills and near the waterfront."
3427
+ },
3428
+ "scoring": {
3429
+ "method": "survival_time",
3430
+ "winning_condition_detail": "The team with the highest cumulative unfrozen player time across all rounds wins the game."
3431
+ },
3432
+ "estimated_total_time_minutes": 90,
3433
+ "quality_score": 4.5
3434
+ }
3435
+ },
3436
+ {
3437
+ "id": "TG-CPT-0028",
3438
+ "input": {
3439
+ "game_type": "tag",
3440
+ "location": {
3441
+ "city": "Cape Town",
3442
+ "country": "South Africa",
3443
+ "city_code": "CPT",
3444
+ "landscape_tags": [
3445
+ "hill_or_elevation",
3446
+ "market_outdoor",
3447
+ "coastal_waterfront"
3448
+ ],
3449
+ "urban_density": "mixed",
3450
+ "climate_zone": "mediterranean",
3451
+ "area_type": "waterfront"
3452
+ },
3453
+ "players": {
3454
+ "count": 20,
3455
+ "team_count": 1,
3456
+ "age_group": "mixed_family",
3457
+ "mobility": "limited"
3458
+ },
3459
+ "preferences": {
3460
+ "duration_minutes": 90,
3461
+ "difficulty": "hard",
3462
+ "theme": "urban_exploration",
3463
+ "allow_transport": false
3464
+ }
3465
+ },
3466
+ "output": {
3467
+ "game_title": "Waterfront Hilltop Freeze",
3468
+ "rules": {
3469
+ "objective": "Avoid being tagged by the 'It' players; unfreeze teammates to stay in the game.",
3470
+ "tag_variant": "freeze_tag",
3471
+ "it_count": 3,
3472
+ "round_count": 5,
3473
+ "round_time_minutes": 18,
3474
+ "tag_mechanic": "A tag is made by an open-hand touch on any part of the body, accompanied by a clear verbal call-out of 'Tag!'. Grabbing, pushing, or pulling clothing is strictly prohibited.",
3475
+ "unfreeze_or_revival_rule": "Frozen players must stand still until unfrozen by a teammate's open-hand touch.",
3476
+ "win_condition": "Be the last player unfrozen when the round time expires."
3477
+ },
3478
+ "arena": {
3479
+ "boundary_description": "The wide play area encompasses a large outdoor market, paths traversing significant hills or elevations, and the extensive coastal waterfront promenade.",
3480
+ "boundary_landscape_tags_used": [
3481
+ "hill_or_elevation",
3482
+ "market_outdoor",
3483
+ "coastal_waterfront"
3484
+ ],
3485
+ "arena_size_tier": "wide",
3486
+ "out_of_bounds": [
3487
+ "Beyond the market boundaries",
3488
+ "into active vehicle roadways",
3489
+ "private property",
3490
+ "any entry into the coastal waterfront or its immediate banks"
3491
+ ]
3492
+ },
3493
+ "safe_zones": [
3494
+ {
3495
+ "zone_id": "S01",
3496
+ "description": "A sheltered alcove within the outdoor market.",
3497
+ "landscape_tags_used": [
3498
+ "market_outdoor"
3499
+ ],
3500
+ "capacity_rule": "Max 4 players, 10-second limit.",
3501
+ "safety_flags": [
3502
+ "watch for uneven ground"
3503
+ ]
3504
+ },
3505
+ {
3506
+ "zone_id": "S02",
3507
+ "description": "The flat, paved lookout point on an elevated section.",
3508
+ "landscape_tags_used": [
3509
+ "hill_or_elevation"
3510
+ ],
3511
+ "capacity_rule": "Max 4 players, 10-second limit.",
3512
+ "safety_flags": [
3513
+ "avoid slippery slopes"
3514
+ ]
3515
+ }
3516
+ ],
3517
+ "movement_features": {
3518
+ "chokepoints": [
3519
+ "Narrow staircases and winding paths on the hills or elevations",
3520
+ "congested pathways between market stalls",
3521
+ "pedestrian bridges crossing small inlets along the waterfront"
3522
+ ],
3523
+ "open_zones": [
3524
+ "The expansive open area of the outdoor market square",
3525
+ "the wide coastal waterfront promenade",
3526
+ "flat plateau areas on the hills or elevations"
3527
+ ],
3528
+ "hazard_zones": [
3529
+ "Coastal water edges, which can be slippery with sudden drops",
3530
+ "steep, uneven slopes and steps with loose gravel",
3531
+ "blind corners on winding paths",
3532
+ "crowded market aisles with potential for collisions and hidden obstacles"
3533
+ ]
3534
+ },
3535
+ "safety_constraints": {
3536
+ "exclusion_zones": [
3537
+ "private_property",
3538
+ "active_roadway",
3539
+ "construction_sites",
3540
+ "restricted_government_buildings",
3541
+ "water_edge"
3542
+ ],
3543
+ "physical_limits": [
3544
+ "no grabbing or pushing — tag is an open-hand touch only",
3545
+ "no climbing",
3546
+ "no jumping",
3547
+ "no water entry",
3548
+ "no entering buildings",
3549
+ "no running across any roadway"
3550
+ ],
3551
+ "collision_safety_note": "Players with limited mobility should exercise extreme caution on steep and uneven terrain, watching for loose footing and blind corners. In crowded market areas, maintain awareness of others to avoid collisions. Be mindful of slippery surfaces and sudden drops near the waterfront.",
3552
+ "adult_supervision_required": true,
3553
+ "notes": "Mediterranean climate: no special constraints regarding temperature; however, players should stay hydrated. Be highly aware of uneven surfaces, steep slopes, and slippery areas near the waterfront due to the 'limited' mobility group and 'hard' difficulty."
3554
+ },
3555
+ "scoring": {
3556
+ "method": "survival_time",
3557
+ "winning_condition_detail": "The player who remains unfrozen for the longest cumulative time across all rounds wins the game."
3558
+ },
3559
+ "estimated_total_time_minutes": 90,
3560
+ "quality_score": 4.7
3561
+ }
3562
+ },
3563
+ {
3564
+ "id": "TG-BOG-0029",
3565
+ "input": {
3566
+ "game_type": "tag",
3567
+ "location": {
3568
+ "city": "Bogota",
3569
+ "country": "Colombia",
3570
+ "city_code": "BOG",
3571
+ "landscape_tags": [
3572
+ "street_art_district",
3573
+ "historic_district",
3574
+ "plaza_or_square"
3575
+ ],
3576
+ "urban_density": "dense",
3577
+ "climate_zone": "temperate",
3578
+ "area_type": "waterfront"
3579
+ },
3580
+ "players": {
3581
+ "count": 4,
3582
+ "team_count": 5,
3583
+ "age_group": "mixed_family",
3584
+ "mobility": "standard"
3585
+ },
3586
+ "preferences": {
3587
+ "duration_minutes": 60,
3588
+ "difficulty": "hard",
3589
+ "theme": "logic",
3590
+ "allow_transport": false
3591
+ }
3592
+ },
3593
+ "output": {
3594
+ "game_title": "Historic Plaza Freeze",
3595
+ "rules": {
3596
+ "objective": "Avoid being tagged by the 'It' players; unfreeze teammates to stay in the game.",
3597
+ "tag_variant": "freeze_tag",
3598
+ "it_count": 2,
3599
+ "round_count": 4,
3600
+ "round_time_minutes": 15,
3601
+ "tag_mechanic": "A tag is made by an open-hand touch on any part of the body, accompanied by a clear verbal call-out of 'Tag!'. Grabbing, pushing, or pulling clothing is strictly prohibited.",
3602
+ "unfreeze_or_revival_rule": "Frozen players must stand still until unfrozen by a teammate's open-hand touch.",
3603
+ "win_condition": "Be the last player unfrozen when the round time expires."
3604
+ },
3605
+ "arena": {
3606
+ "boundary_description": "The compact play area is defined by the pedestrian zones of a historic district, including a vibrant street art district and a the plaza or square.",
3607
+ "boundary_landscape_tags_used": [
3608
+ "street_art_district",
3609
+ "historic_district",
3610
+ "plaza_or_square"
3611
+ ],
3612
+ "arena_size_tier": "compact",
3613
+ "out_of_bounds": [
3614
+ "Beyond the designated pedestrian paths",
3615
+ "into active vehicle roadways",
3616
+ "inside private buildings"
3617
+ ]
3618
+ },
3619
+ "safe_zones": [
3620
+ {
3621
+ "zone_id": "S01",
3622
+ "description": "The raised platform in the center of the plaza.",
3623
+ "landscape_tags_used": [
3624
+ "plaza_or_square"
3625
+ ],
3626
+ "capacity_rule": "Max 1 player, 10-second limit.",
3627
+ "safety_flags": [
3628
+ "watch for uneven paving"
3629
+ ]
3630
+ },
3631
+ {
3632
+ "zone_id": "S02",
3633
+ "description": "A designated alcove within the historic district, adorned with street art.",
3634
+ "landscape_tags_used": [
3635
+ "historic_district",
3636
+ "street_art_district"
3637
+ ],
3638
+ "capacity_rule": "Max 1 player, 10-second limit.",
3639
+ "safety_flags": [
3640
+ "mind your step near obstacles"
3641
+ ]
3642
+ }
3643
+ ],
3644
+ "movement_features": {
3645
+ "chokepoints": [
3646
+ "Narrow passages and archways within the historic district",
3647
+ "alleyways decorated with street art",
3648
+ "entrances/exits to the plaza"
3649
+ ],
3650
+ "open_zones": [
3651
+ "The wide open expanse of the plaza or square",
3652
+ "broader pedestrian streets in the street art district"
3653
+ ],
3654
+ "hazard_zones": [
3655
+ "Uneven cobblestone paths and unexpected steps within the historic district",
3656
+ "blind corners in narrow alleys",
3657
+ "crowded areas near popular street art installations"
3658
+ ]
3659
+ },
3660
+ "safety_constraints": {
3661
+ "exclusion_zones": [
3662
+ "private_property",
3663
+ "active_roadway",
3664
+ "construction_sites",
3665
+ "restricted_government_buildings"
3666
+ ],
3667
+ "physical_limits": [
3668
+ "no grabbing or pushing — tag is an open-hand touch only",
3669
+ "no climbing",
3670
+ "no jumping",
3671
+ "no water entry",
3672
+ "no entering buildings",
3673
+ "no running across any roadway"
3674
+ ],
3675
+ "collision_safety_note": "Players must be aware of other pedestrians and obstacles in the dense historic district. Exercise caution on uneven paving and steps, and navigate narrow alleys carefully due to blind corners and potential crowded sections.",
3676
+ "adult_supervision_required": true,
3677
+ "notes": "Temperate climate: no special constraints regarding temperature; however, players should stay hydrated. Be mindful of uneven surfaces and potential tripping hazards in the historic district."
3678
+ },
3679
+ "scoring": {
3680
+ "method": "survival_time",
3681
+ "winning_condition_detail": "The player who remains unfrozen for the longest cumulative time across all rounds wins the game."
3682
+ },
3683
+ "estimated_total_time_minutes": 60,
3684
+ "quality_score": 4.4
3685
+ }
3686
  }
3687
  ]
scripts/scavenger_hunt/generator.py CHANGED
@@ -21,10 +21,14 @@ Flags:
21
  --seed INT fix random seed for sampling (optional)
22
  --mock skip Gemini, use built-in stub (pipeline smoke-test)
23
  """
24
- import os, re, json, time, random, logging, argparse
25
  from sampler import sample_input, build_prompt
26
  from validator import validate_example, auto_fix
27
 
 
 
 
 
28
  logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
29
  log = logging.getLogger("cq")
30
 
@@ -100,10 +104,12 @@ def call_mock(prompt: str) -> str:
100
  tasks = []
101
  for i in range(n_tasks):
102
  tag = tags[i % len(tags)]
 
 
103
  tasks.append({
104
  "task_id": f"T{i+1:02d}",
105
  "title": f"Zone Study {i+1}",
106
- "description": f"Within the zone characterised by {tag.replace('_',' ')}, locate and document the most structurally distinct feature visible from a public pedestrian path.",
107
  "landscape_tags_used": [tag],
108
  "task_type": types_cycle[i % len(types_cycle)],
109
  "difficulty_contribution": diffs[i],
@@ -111,11 +117,11 @@ def call_mock(prompt: str) -> str:
111
  "completion_proof": "photograph submitted via app",
112
  "estimated_time_minutes": per_task,
113
  "hints": {
114
- "hint_1": "Scan the area from the nearest open space.",
115
- "hint_2": f"Move toward where {tag.replace('_',' ')} activity is densest and compare individual instances.",
116
- "hint_3": f"Walk the main pedestrian route through the {tag.replace('_',' ')} zone; at the midpoint, turn to face the most varied cluster and document the single feature that differs most from its neighbours in scale or colour.",
117
  },
118
- "safety_flags": [],
119
  })
120
 
121
  bonus = (
@@ -163,138 +169,9 @@ def call_mock(prompt: str) -> str:
163
  return json.dumps(out)
164
 
165
 
166
- # ── single example loop ───────────────────────────────────────────────────────
167
- def generate_one(record: dict, backend, max_retries: int = 2):
168
- """
169
- Try up to (max_retries+1) times.
170
- On retry the validation errors are appended to the prompt so the model
171
- knows exactly what to fix.
172
- Returns (example_dict, None) on success, (None, [errors]) on failure.
173
- """
174
- prompt = build_prompt(record)
175
- last_errors = []
176
-
177
- for attempt in range(max_retries + 1):
178
- full_prompt = prompt
179
- if attempt > 0 and last_errors:
180
- full_prompt += (
181
- "\n\nYOUR PREVIOUS ATTEMPT FAILED THESE VALIDATION CHECKS:\n"
182
- + "\n".join(f" • {e}" for e in last_errors[:8])
183
- + "\nPlease fix every item listed above. Return ONLY the corrected JSON."
184
- )
185
-
186
- try:
187
- raw = backend(full_prompt)
188
- output_json = extract_json(raw)
189
-
190
- # the model sometimes wraps the output inside {"output": {...}}
191
- if "output" in output_json and isinstance(output_json["output"], dict):
192
- output_json = output_json["output"]
193
-
194
- ex = {**record, "output": output_json}
195
- ex = auto_fix(ex) # repair arithmetic first
196
- ok, errors = validate_example(ex)
197
-
198
- if ok:
199
- return ex, None
200
- last_errors = errors
201
- log.warning(" attempt %d/%d — %d error(s): %s",
202
- attempt + 1, max_retries + 1,
203
- len(errors), errors[0])
204
-
205
- except Exception as exc:
206
- last_errors = [f"PARSE/API error: {exc}"]
207
- log.warning(" attempt %d/%d — %s", attempt + 1, max_retries + 1, exc)
208
-
209
- return None, last_errors
210
-
211
-
212
- # ── main ──────────────────────────────────────────────────────────────────────
213
- def main():
214
- ap = argparse.ArgumentParser(description="CityQuest-AI dataset generator")
215
- ap.add_argument("--n", type=int, default=10, help="examples to generate this run")
216
- ap.add_argument("--output", default=DEFAULT_OUTPUT, help="output JSON file (resumes if exists)")
217
- ap.add_argument("--sleep", type=float, default=8.0, help="seconds between Gemini calls")
218
- ap.add_argument("--seed", type=int, default=None, help="random seed for reproducibility")
219
- ap.add_argument("--mock", action="store_true", help="use stub backend (no API key needed)")
220
- args = ap.parse_args()
221
-
222
- if args.seed is not None:
223
- random.seed(args.seed)
224
-
225
- if not args.mock and not os.environ.get("GEMINI_API_KEY"):
226
- log.error("GEMINI_API_KEY is not set.")
227
- log.error(" Windows (PowerShell): $env:GEMINI_API_KEY = 'your-key'")
228
- log.error(" Mac/Linux: export GEMINI_API_KEY='your-key'")
229
- return
230
-
231
- backend = call_mock if args.mock else call_gemini
232
-
233
- out_dir = os.path.dirname(args.output)
234
- if out_dir:
235
- os.makedirs(out_dir, exist_ok=True)
236
-
237
- # ── resume ────────────────────────────────────────────────────────────────
238
- dataset = []
239
- if os.path.exists(args.output):
240
- try:
241
- dataset = json.load(open(args.output, encoding="utf-8"))
242
- log.info("Resuming — loaded %d existing examples from %s", len(dataset), args.output)
243
- except json.JSONDecodeError:
244
- log.warning("Existing output file unreadable; starting fresh.")
245
-
246
- errors_path = args.output.replace(".json", "_errors.jsonl")
247
- start_idx = len(dataset) + 1
248
- kept = skipped = 0
249
- quota_exhausted = False
250
-
251
- # ── generation loop ───────────────────────────────────────────────────────
252
- for i in range(start_idx, start_idx + args.n):
253
- record = sample_input(i)
254
- city = record["input"]["location"]["city"]
255
- diff = record["input"]["preferences"]["difficulty"]
256
- dur = record["input"]["preferences"]["duration_minutes"]
257
- age = record["input"]["players"]["age_group"]
258
-
259
- log.info("[%d/%d] %s | %-12s | %-6s | %3d min | %s",
260
- i - start_idx + 1, args.n,
261
- record["id"], city, diff, dur, age)
262
-
263
- ex, errors = generate_one(record, backend)
264
-
265
- if ex:
266
- dataset.append(ex)
267
- kept += 1
268
- # incremental save — safe to interrupt at any point
269
- with open(args.output, "w", encoding="utf-8") as f:
270
- json.dump(dataset, f, indent=1, ensure_ascii=False)
271
- log.info(" ✓ saved (dataset total: %d)", len(dataset))
272
- else:
273
- skipped += 1
274
- with open(errors_path, "a", encoding="utf-8") as f:
275
- f.write(json.dumps({"id": record["id"], "errors": errors}) + "\n")
276
- log.info(" ✗ skipped after retries — logged to %s", errors_path)
277
-
278
- if not args.mock and any("RESOURCE_EXHAUSTED" in e or "429" in e for e in errors):
279
- log.warning(" quota exhausted on this API key — stopping run early")
280
- quota_exhausted = True
281
- break
282
-
283
- if not args.mock:
284
- time.sleep(args.sleep)
285
-
286
- # ── summary ───────────────────────────────────────────────────────────────
287
- log.info("")
288
- log.info("══ DONE ══════════════════════════════════")
289
- log.info(" Kept : %d", kept)
290
- log.info(" Skipped : %d", skipped)
291
- log.info(" Total : %d → %s", len(dataset), args.output)
292
- if skipped:
293
- log.info(" Failures: %s", errors_path)
294
-
295
- if quota_exhausted:
296
- raise SystemExit(2)
297
 
298
 
299
  if __name__ == "__main__":
300
- main()
 
 
21
  --seed INT fix random seed for sampling (optional)
22
  --mock skip Gemini, use built-in stub (pipeline smoke-test)
23
  """
24
+ import os, re, json, time, random, logging, argparse, sys
25
  from sampler import sample_input, build_prompt
26
  from validator import validate_example, auto_fix
27
 
28
+ HERE = os.path.dirname(os.path.abspath(__file__))
29
+ sys.path.insert(0, os.path.join(HERE, "..", "common"))
30
+ from cq_common import has_risk_indicators
31
+
32
  logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
33
  log = logging.getLogger("cq")
34
 
 
104
  tasks = []
105
  for i in range(n_tasks):
106
  tag = tags[i % len(tags)]
107
+ description = f"Within the zone characterised by {tag.replace('_',' ')}, locate and document the most structurally distinct feature visible from a public pedestrian path."
108
+ flags = ["uneven paving may be slippery when wet", "watch for cyclists sharing the path"] if has_risk_indicators(description) else []
109
  tasks.append({
110
  "task_id": f"T{i+1:02d}",
111
  "title": f"Zone Study {i+1}",
112
+ "description": description,
113
  "landscape_tags_used": [tag],
114
  "task_type": types_cycle[i % len(types_cycle)],
115
  "difficulty_contribution": diffs[i],
 
117
  "completion_proof": "photograph submitted via app",
118
  "estimated_time_minutes": per_task,
119
  "hints": {
120
+ "hint_1": "Scan the area from open space.",
121
+ "hint_2": f"Move toward the {tag.replace('_',' ')} zone and compare features.",
122
+ "hint_3": f"Within the {tag.replace('_',' ')} zone, find the feature that differs most from its neighbours in scale or colour.",
123
  },
124
+ "safety_flags": flags,
125
  })
126
 
127
  bonus = (
 
169
  return json.dumps(out)
170
 
171
 
172
+ from cli import run_cli # noqa: E402
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
 
174
 
175
  if __name__ == "__main__":
176
+ run_cli("scavenger_hunt", sample_input, build_prompt, auto_fix,
177
+ validate_example, DEFAULT_OUTPUT, call_mock)
scripts/scavenger_hunt/sampler.py CHANGED
@@ -145,20 +145,28 @@ climate note : {climate_note_txt}
145
 
146
  ═══ ABSOLUTE RULES ═══
147
  1. NO proper nouns anywhere — no city names, landmark names, street names, brand names.
148
- Tasks must work purely from the landscape_tags.
149
- GOOD: "Find the tallest structure visible from the main open square and photograph its base."
150
- BAD: "Find the Eiffel Tower."
 
 
 
 
151
 
152
  2. Each task's landscape_tags_used must be a strict subset of: {json.dumps(loc["landscape_tags"])}
153
 
154
  3. Use at least 3 different task_type values from:
155
  {json.dumps(TASK_TYPES)}
156
 
157
- 4. Hints per task:
158
- hint_1 = 5–15 words, directional nudge only
159
- hint_2 = 15–30 words, describes the feature to look for (longer than hint_1)
160
- hint_3 = 30–50 words, near-explicit walkthrough (longer than hint_2)
161
  All three hints must be meaningfully distinct from each other.
 
 
 
 
162
 
163
  5. No task may require: climbing, jumping, entering water, entering any building
164
  (exception: social_interaction tasks may enter public-facing shops).
@@ -167,6 +175,16 @@ climate note : {climate_note_txt}
167
 
168
  7. {bonus_instruction}
169
 
 
 
 
 
 
 
 
 
 
 
170
  ═══ OUTPUT SHAPE ═══
171
  {{
172
  "game_title": "string",
 
145
 
146
  ═══ ABSOLUTE RULES ═══
147
  1. NO proper nouns anywhere — no city names, landmark names, street names, brand names.
148
+ Tasks must work purely from the landscape_tags.
149
+ ALSO BANNED everywhere (description, title, hints, completion_proof these leak
150
+ city-specific knowledge the player shouldn't need): "central", "main", "largest",
151
+ "most famous", "the famous", "best-known", "best known", "downtown", and "iconic"
152
+ (unless followed by the word "landmark", e.g. "iconic landmark" is fine).
153
+ ✓ GOOD: "Find the tallest structure visible from the open square and photograph its base."
154
+ ✗ BAD: "Find the Eiffel Tower." / "...the main square..." / "...the most famous fountain..."
155
 
156
  2. Each task's landscape_tags_used must be a strict subset of: {json.dumps(loc["landscape_tags"])}
157
 
158
  3. Use at least 3 different task_type values from:
159
  {json.dumps(TASK_TYPES)}
160
 
161
+ 4. Hints per task — HARD character limits (count characters, not words):
162
+ hint_1 = a short directional nudge, AT MOST 50 characters total.
163
+ hint_2 = describes the feature to look for, AT MOST 80 characters total, longer than hint_1.
164
+ hint_3 = a near-explicit walkthrough, AT MOST 120 characters total, longer than hint_2.
165
  All three hints must be meaningfully distinct from each other.
166
+ hint_3 must NOT use spoiler phrases that give away exact spatial position relative to
167
+ another object, e.g. avoid "just before", "next to", "to the left of", "to the right of",
168
+ "near the corner of", "across from", "adjacent to", "directly behind", "right after",
169
+ "just past". Describe the feature itself instead of its position relative to something else.
170
 
171
  5. No task may require: climbing, jumping, entering water, entering any building
172
  (exception: social_interaction tasks may enter public-facing shops).
 
175
 
176
  7. {bonus_instruction}
177
 
178
+ 8. social_interaction tasks: completion_proof must require a photo, receipt, or other
179
+ physical/digital artifact — NEVER a verbal-only response (no "tell us", "ask them",
180
+ "verbal answer", etc.).
181
+
182
+ 9. If a task's landscape_tags_used or description touches water, elevation/slopes/stairs,
183
+ alleys/narrow passages, crowded areas, or dark/low-light areas, that task's safety_flags
184
+ must contain AT LEAST 2 specific hazard mentions (e.g. naming the slippery surface, the
185
+ blind corner, the steep step, the low visibility) — not generic phrases like "be careful"
186
+ or "stay safe".
187
+
188
  ═══ OUTPUT SHAPE ═══
189
  {{
190
  "game_title": "string",
scripts/scavenger_hunt/test_pipeline.py CHANGED
@@ -49,43 +49,43 @@ GOOD = {
49
  "tasks": [
50
  {
51
  "task_id": "T01", "title": "Square Census",
52
- "description": "Stand at the center of the largest open civic square and count the benches visible without moving.",
53
  "landscape_tags_used": ["plaza_or_square"],
54
  "task_type": "observe_and_answer",
55
  "difficulty_contribution": "easy", "points": 10,
56
- "completion_proof": "verbal answer", "estimated_time_minutes": 10,
57
  "hints": {
58
- "hint_1": "Find the biggest open space nearby.",
59
- "hint_2": "Open civic squares have no buildings inside and multiple pedestrian entry points leading to a central area.",
60
- "hint_3": "Walk to where the most pedestrian paths converge, stand at the geometric centre, rotate slowly, and count every fixed seating unit you can see without moving your feet.",
61
  },
62
  "safety_flags": [],
63
  },
64
  {
65
  "task_id": "T02", "title": "Vendor Colours",
66
- "description": "At an open-air market, photograph three vendor stalls whose canopies are three different colours.",
67
  "landscape_tags_used": ["market_outdoor"],
68
  "task_type": "find_and_photograph",
69
  "difficulty_contribution": "easy", "points": 10,
70
  "completion_proof": "photo", "estimated_time_minutes": 12,
71
  "hints": {
72
- "hint_1": "Follow the sounds of commerce.",
73
- "hint_2": "Open-air markets cluster along wide pedestrian streets; look for rows of temporary stalls with overhead canopies.",
74
- "hint_3": "Enter the market from its main entrance, walk one full aisle from start to finish, and photograph the first red, the first blue, and the first white canopy you pass.",
75
  },
76
  "safety_flags": [],
77
  },
78
  {
79
  "task_id": "T03", "title": "Tallest Sighting",
80
- "description": "Find a spot where the tallest structure in the area is fully visible. Photograph it with at least one other person in frame.",
81
  "landscape_tags_used": ["iconic_landmark"],
82
  "task_type": "reach_and_verify",
83
  "difficulty_contribution": "easy", "points": 10,
84
  "completion_proof": "photo", "estimated_time_minutes": 14,
85
  "hints": {
86
- "hint_1": "Look up from the most open ground you can find.",
87
- "hint_2": "Tall structures are easiest to see from squares or wide streets with no overhead obstruction — position yourself so the full height is unblocked.",
88
- "hint_3": "Return to the open square, face the direction where rooftops are at their lowest, and the tallest structure will dominate your skyline; frame it with a teammate visible in the foreground.",
89
  },
90
  "safety_flags": [],
91
  },
@@ -154,6 +154,29 @@ bad = auto_fix(bad)
154
  ok, errs = validate_example(bad)
155
  check("T6: time overrun caught", not ok and any("TIME" in e for e in errs))
156
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  # ── 7: sampler variety ────────────────────────────────────────────────────────
158
  print("\n── Sampler tests ─────────────────────────────────────")
159
  samples = [sample_input(i, rng=random.Random(i)) for i in range(1, 21)]
 
49
  "tasks": [
50
  {
51
  "task_id": "T01", "title": "Square Census",
52
+ "description": "Within the plaza area, count the benches visible while standing in one spot without moving.",
53
  "landscape_tags_used": ["plaza_or_square"],
54
  "task_type": "observe_and_answer",
55
  "difficulty_contribution": "easy", "points": 10,
56
+ "completion_proof": "photo of the bench count tally", "estimated_time_minutes": 10,
57
  "hints": {
58
+ "hint_1": "Find an open paved square.",
59
+ "hint_2": "Open squares have multiple pedestrian paths converging toward open paving.",
60
+ "hint_3": "Stand where several pedestrian paths meet in the open paving, rotate slowly, and count every fixed seat in view.",
61
  },
62
  "safety_flags": [],
63
  },
64
  {
65
  "task_id": "T02", "title": "Vendor Colours",
66
+ "description": "At the open-air market area, photograph three stalls whose canopies show three different colours.",
67
  "landscape_tags_used": ["market_outdoor"],
68
  "task_type": "find_and_photograph",
69
  "difficulty_contribution": "easy", "points": 10,
70
  "completion_proof": "photo", "estimated_time_minutes": 12,
71
  "hints": {
72
+ "hint_1": "Follow sounds of trade and stalls.",
73
+ "hint_2": "Open-air markets line pedestrian streets with rows of canopied stalls.",
74
+ "hint_3": "Walk one full row of stalls end to end and photograph the first red, blue, and white canopy seen.",
75
  },
76
  "safety_flags": [],
77
  },
78
  {
79
  "task_id": "T03", "title": "Tallest Sighting",
80
+ "description": "Find a spot where a tall structure is fully visible and photograph it with a teammate in frame.",
81
  "landscape_tags_used": ["iconic_landmark"],
82
  "task_type": "reach_and_verify",
83
  "difficulty_contribution": "easy", "points": 10,
84
  "completion_proof": "photo", "estimated_time_minutes": 14,
85
  "hints": {
86
+ "hint_1": "Look up from open ground nearby.",
87
+ "hint_2": "Tall structures are visible from open areas with no overhead blockage.",
88
+ "hint_3": "Stand in open paving facing where rooftops are lowest, then photograph the tall structure with a teammate in view.",
89
  },
90
  "safety_flags": [],
91
  },
 
154
  ok, errs = validate_example(bad)
155
  check("T6: time overrun caught", not ok and any("TIME" in e for e in errs))
156
 
157
+ # ── audit-fix failing fixtures ─────────────────────────────────────────────────
158
+ bad = copy.deepcopy(GOOD)
159
+ bad["output"]["tasks"][0]["description"] = "Within the main plaza area, count the benches visible while standing in one spot without moving."
160
+ ok, errs = validate_example(bad)
161
+ check("T11: soft city-leakage word 'main' caught", not ok and any("LEAK" in e for e in errs), errs)
162
+
163
+ bad = copy.deepcopy(GOOD)
164
+ bad["output"]["tasks"][0]["task_type"] = "social_interaction"
165
+ bad["output"]["tasks"][0]["completion_proof"] = "ask a vendor and tell us their answer"
166
+ ok, errs = validate_example(bad)
167
+ check("T12: verbal-only social_interaction proof caught", not ok and any("PROOF" in e for e in errs), errs)
168
+
169
+ bad = copy.deepcopy(GOOD)
170
+ bad["output"]["tasks"][0]["description"] = "Walk along the water's edge near the alley and count the boats moored there."
171
+ bad["output"]["tasks"][0]["safety_flags"] = []
172
+ ok, errs = validate_example(bad)
173
+ check("T13: water/alley task with <2 safety_flags caught", not ok and any("SAFETY" in e for e in errs), errs)
174
+
175
+ bad = copy.deepcopy(GOOD)
176
+ bad["output"]["tasks"][0]["hints"]["hint_3"] = "Walk to the plaza entrance, and the bench cluster is just before the fountain, next to the lamppost on the left side path."
177
+ ok, errs = validate_example(bad)
178
+ check("T14: hint_3 spoiler/length violation caught", not ok and any("HINTSPOILER" in e or "HINTLEN" in e for e in errs), errs)
179
+
180
  # ── 7: sampler variety ────────────────────────────────────────────────────────
181
  print("\n── Sampler tests ─────────────────────────────────────")
182
  samples = [sample_input(i, rng=random.Random(i)) for i in range(1, 21)]
scripts/scavenger_hunt/validator.py CHANGED
@@ -4,12 +4,17 @@ Validates one training example. Returns (is_valid: bool, errors: list[str]).
4
  auto_fix() recomputes arithmetic fields in-place — never reject for math the model shouldn't be trusted with.
5
  """
6
  import os
 
7
  import sys
8
  from Levenshtein import distance as lev
9
 
10
  HERE = os.path.dirname(os.path.abspath(__file__))
11
  sys.path.insert(0, os.path.join(HERE, "..", "common"))
12
- from cq_common import VALID_TAGS, nouns_in as _nouns_in
 
 
 
 
13
 
14
  POINTS = {"easy": 10, "medium": 20, "hard": 30}
15
 
@@ -52,6 +57,34 @@ def validate_example(ex: dict) -> tuple[bool, list[str]]:
52
  if found:
53
  errors.append(f"NOUN: {tid} {hk} contains {set(found)}")
54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  # tags used must be subset of input tags
56
  used = set(t.get("landscape_tags_used",[]))
57
  extra = used - input_tags
@@ -126,14 +159,91 @@ def validate_example(ex: dict) -> tuple[bool, list[str]]:
126
  return len(errors) == 0, errors
127
 
128
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  def auto_fix(ex: dict) -> dict:
130
  """Recompute all arithmetic output fields in-place. Call before validate_example."""
131
  try:
 
 
132
  tasks = ex["output"]["tasks"]
133
  for t in tasks:
134
  d = t.get("difficulty_contribution")
135
  if d in POINTS:
136
  t["points"] = POINTS[d]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  base = sum(t["points"] for t in tasks)
138
  bonus_pts = (ex["output"].get("bonus_task") or {}).get("points") or 0
139
  ex["output"]["task_count"] = len(tasks)
 
4
  auto_fix() recomputes arithmetic fields in-place — never reject for math the model shouldn't be trusted with.
5
  """
6
  import os
7
+ import re as _re
8
  import sys
9
  from Levenshtein import distance as lev
10
 
11
  HERE = os.path.dirname(os.path.abspath(__file__))
12
  sys.path.insert(0, os.path.join(HERE, "..", "common"))
13
+ from cq_common import (
14
+ VALID_TAGS, nouns_in as _nouns_in, soft_leaks_in,
15
+ is_verbal_only_proof, has_risk_indicators, specific_flag_count,
16
+ HINT_LIMITS, hint_spoilers_in, fix_text_leaks,
17
+ )
18
 
19
  POINTS = {"easy": 10, "medium": 20, "hard": 30}
20
 
 
57
  if found:
58
  errors.append(f"NOUN: {tid} {hk} contains {set(found)}")
59
 
60
+ # soft city-leakage words (audit §1) — description + hints
61
+ leaks = soft_leaks_in(t.get("description",""))
62
+ if leaks:
63
+ errors.append(f"LEAK: {tid} description contains city-knowledge words {set(leaks)}")
64
+ for hk in ("hint_1","hint_2","hint_3"):
65
+ leaks = soft_leaks_in(t.get("hints",{}).get(hk,""))
66
+ if leaks:
67
+ errors.append(f"LEAK: {tid} {hk} contains city-knowledge words {set(leaks)}")
68
+
69
+ # hint length limits + anti-spoiler check (audit §4)
70
+ for hk, limit in HINT_LIMITS.items():
71
+ h = t.get("hints",{}).get(hk,"")
72
+ if len(h) > limit:
73
+ errors.append(f"HINTLEN: {tid} {hk} is {len(h)} chars (max {limit})")
74
+ spoilers = hint_spoilers_in(t.get("hints",{}).get("hint_3",""))
75
+ if spoilers:
76
+ errors.append(f"HINTSPOILER: {tid} hint_3 contains direction give-aways {spoilers}")
77
+
78
+ # verifiable proof for social_interaction tasks (audit §2)
79
+ if t.get("task_type") == "social_interaction" and is_verbal_only_proof(t.get("completion_proof","")):
80
+ errors.append(f"PROOF: {tid} social_interaction completion_proof is verbal-only, needs photo/receipt")
81
+
82
+ # safety flag specificity (audit §3)
83
+ risky_text = " ".join([t.get("description",""), t.get("title","")])
84
+ if has_risk_indicators(risky_text):
85
+ if specific_flag_count(t.get("safety_flags")) < 2:
86
+ errors.append(f"SAFETY: {tid} touches water/elevation/alley/crowd/dark but has <2 specific safety_flags")
87
+
88
  # tags used must be subset of input tags
89
  used = set(t.get("landscape_tags_used",[]))
90
  extra = used - input_tags
 
159
  return len(errors) == 0, errors
160
 
161
 
162
+ _LEAK_REPLACEMENTS = {
163
+ "most famous": "most distinctive",
164
+ "the famous": "the notable",
165
+ "best-known": "most noticeable",
166
+ "best known": "most noticeable",
167
+ "central": "the",
168
+ "downtown": "this area",
169
+ "largest": "biggest",
170
+ "main": "the",
171
+ }
172
+
173
+ _SPOILER_PHRASES = [
174
+ "just before", "next to", "to the left of", "to the right of",
175
+ "near the corner of", "across from", "adjacent to",
176
+ "directly behind", "right after", "just past",
177
+ ]
178
+
179
+
180
+ def _strip_leaks(text: str) -> str:
181
+ if not text:
182
+ return text
183
+ for phrase, repl in _LEAK_REPLACEMENTS.items():
184
+ text = _re.sub(r"\b" + _re.escape(phrase) + r"\b", repl, text, flags=_re.IGNORECASE)
185
+ text = _re.sub(r"\biconic\b(?!\s+landmark)", "notable", text, flags=_re.IGNORECASE)
186
+ text = _re.sub(r"\bthe the\b", "the", text, flags=_re.IGNORECASE)
187
+ return _re.sub(r"\s{2,}", " ", text).strip()
188
+
189
+
190
+ def _strip_spoilers(text: str) -> str:
191
+ if not text:
192
+ return text
193
+ for phrase in _SPOILER_PHRASES:
194
+ text = _re.sub(_re.escape(phrase), "near", text, flags=_re.IGNORECASE)
195
+ return _re.sub(r"\s{2,}", " ", text).strip()
196
+
197
+
198
+ def _truncate(text: str, limit: int) -> str:
199
+ if not text or len(text) <= limit:
200
+ return text
201
+ cut = text[:limit]
202
+ if " " in cut:
203
+ cut = cut[: cut.rfind(" ")]
204
+ return cut.rstrip(" ,.;:")
205
+
206
+
207
  def auto_fix(ex: dict) -> dict:
208
  """Recompute all arithmetic output fields in-place. Call before validate_example."""
209
  try:
210
+ loc = ex["input"]["location"]
211
+ ex["output"]["game_title"] = fix_text_leaks(ex["output"].get("game_title", ""), [loc.get("city"), loc.get("country")])
212
  tasks = ex["output"]["tasks"]
213
  for t in tasks:
214
  d = t.get("difficulty_contribution")
215
  if d in POINTS:
216
  t["points"] = POINTS[d]
217
+
218
+ # strip soft city-leakage words from description (audit §1)
219
+ t["description"] = _strip_leaks(t.get("description", ""))
220
+
221
+ # clean + enforce hint limits / anti-spoiler (audit §4)
222
+ hints = t.get("hints")
223
+ if isinstance(hints, dict):
224
+ for hk in ("hint_1", "hint_2", "hint_3"):
225
+ if hk in hints and isinstance(hints[hk], str):
226
+ hints[hk] = _strip_leaks(hints[hk])
227
+ if "hint_3" in hints and isinstance(hints["hint_3"], str):
228
+ hints["hint_3"] = _strip_spoilers(hints["hint_3"])
229
+ for hk, limit in HINT_LIMITS.items():
230
+ if hk in hints and isinstance(hints[hk], str):
231
+ hints[hk] = _truncate(hints[hk], limit)
232
+
233
+ # ensure ≥2 specific safety_flags for risky tasks (audit §3)
234
+ risky_text = " ".join([t.get("description", ""), t.get("title", "")])
235
+ if has_risk_indicators(risky_text):
236
+ flags = t.get("safety_flags") or []
237
+ if specific_flag_count(flags) < 2:
238
+ extra = [
239
+ "watch your footing — surfaces near here can be uneven or slippery",
240
+ "stay alert for blind corners and limited visibility around this spot",
241
+ ]
242
+ for e in extra:
243
+ if specific_flag_count(flags) >= 2:
244
+ break
245
+ flags.append(e)
246
+ t["safety_flags"] = flags
247
  base = sum(t["points"] for t in tasks)
248
  bonus_pts = (ex["output"].get("bonus_task") or {}).get("points") or 0
249
  ex["output"]["task_count"] = len(tasks)