@@ -3,6 +3,7 @@ import toEnName from "arabic-name-to-en";
33import { writeJson } from "../../utils/fs" ;
44import { ApiResource } from "../../../types/api.types" ;
55import { readCsv } from "../../utils/csv" ;
6+ import { knownDuplicates } from "../common/killed-in-gaza/duplicates" ;
67
78const jsonFileName = "killed-in-gaza.json" ;
89
@@ -27,7 +28,7 @@ interface MappedRecord extends Record<string, string | number> {
2728
2829const sexMapping = {
2930 M : "m" ,
30- F : "f"
31+ F : "f" ,
3132} ;
3233
3334const namesFallbackTranslated = new Map < string , number > ( ) ;
@@ -176,18 +177,111 @@ const validateJson = (json: Array<Record<string, number | string>>) => {
176177 }
177178} ;
178179
180+ /**
181+ * Build a set of all IDs that are known duplicates (values in knownDuplicates map)
182+ * so we can skip them during consolidation and unknown duplicate detection.
183+ */
184+ const allKnownDuplicateIds = new Set < string > ( ) ;
185+ for ( const dupIds of Object . values ( knownDuplicates ) ) {
186+ for ( const dupId of dupIds ) {
187+ allKnownDuplicateIds . add ( dupId ) ;
188+ }
189+ }
190+
191+ /**
192+ * Detect records that share the same name+dob+sex but have different IDs.
193+ * These are potential duplicates that should be resolved in the duplicates config.
194+ * Known duplicates (from the config) are excluded from this check.
195+ */
196+ const detectUnknownDuplicates = (
197+ json : MappedRecord [ ]
198+ ) : Map < string , MappedRecord [ ] > => {
199+ const keyMap = new Map < string , MappedRecord [ ] > ( ) ;
200+
201+ for ( const record of json ) {
202+ // Skip records that are known duplicates (to be consolidated)
203+ if ( allKnownDuplicateIds . has ( record . id ) ) {
204+ continue ;
205+ }
206+ const key = `${ record . name } |${ record . dob } |${ record . sex } ` ;
207+ if ( ! keyMap . has ( key ) ) {
208+ keyMap . set ( key , [ ] ) ;
209+ }
210+ keyMap . get ( key ) ! . push ( record ) ;
211+ }
212+
213+ const unknownDupes = new Map < string , MappedRecord [ ] > ( ) ;
214+ for ( const [ key , records ] of keyMap ) {
215+ if ( records . length > 1 ) {
216+ unknownDupes . set ( key , records ) ;
217+ }
218+ }
219+ return unknownDupes ;
220+ } ;
221+
222+ /**
223+ * Consolidate known duplicates: remove duplicate records and add duplicate_ids
224+ * to the canonical record.
225+ */
226+ const consolidateKnownDuplicates = ( json : MappedRecord [ ] ) : MappedRecord [ ] => {
227+ const result : MappedRecord [ ] = [ ] ;
228+
229+ for ( const record of json ) {
230+ // Skip records that are known duplicate IDs (non-canonical)
231+ if ( allKnownDuplicateIds . has ( record . id ) ) {
232+ continue ;
233+ }
234+
235+ // If this is a canonical record with known duplicates, add the field
236+ if ( knownDuplicates [ record . id ] ) {
237+ ( record as Record < string , unknown > ) . duplicate_ids =
238+ knownDuplicates [ record . id ] ;
239+ }
240+
241+ result . push ( record ) ;
242+ }
243+
244+ return result ;
245+ } ;
246+
179247const generateJsonFromTranslatedCsv = async ( ) => {
180248 const [ headerKeys , ...rows ] = readCsv (
181249 "scripts/data/common/killed-in-gaza/output/result.csv"
182250 ) ;
183251 const jsonArray = formatToJson ( headerKeys , rows ) ;
184252 validateJson ( jsonArray ) ;
253+
254+ // Detect unknown duplicates (same name+dob+sex, different IDs) and error
255+ const unknownDupes = detectUnknownDuplicates ( jsonArray ) ;
256+ if ( unknownDupes . size > 0 ) {
257+ const dupeDetails = Array . from ( unknownDupes . entries ( ) )
258+ . map ( ( [ key , records ] ) => {
259+ const ids = records . map ( ( r ) => r . id ) . join ( ", " ) ;
260+ return ` ${ records [ 0 ] . en_name } (${ key } ): [${ ids } ]` ;
261+ } )
262+ . join ( "\n" ) ;
263+ throw new Error (
264+ `Found ${ unknownDupes . size } unknown duplicate group(s) with same name+dob+sex but different IDs.\n` +
265+ `Add them to scripts/data/common/killed-in-gaza/duplicates.ts and remove duplicates from raw.csv.\n` +
266+ `Duplicate groups:\n${ dupeDetails } `
267+ ) ;
268+ }
269+
270+ // Consolidate known duplicates
271+ const consolidated = consolidateKnownDuplicates ( jsonArray ) ;
272+ const removedCount = jsonArray . length - consolidated . length ;
273+ if ( removedCount > 0 ) {
274+ console . log (
275+ `Consolidated ${ removedCount } known duplicate record(s) from ${ jsonArray . length } total`
276+ ) ;
277+ }
278+
185279 // sort by descending ID
186- jsonArray . sort ( ( a , b ) => b . id . localeCompare ( a . id ) ) ;
187- writeJson ( ApiResource . KilledInGazaV2 , jsonFileName , jsonArray ) ;
280+ consolidated . sort ( ( a , b ) => b . id . localeCompare ( a . id ) ) ;
281+ writeJson ( ApiResource . KilledInGazaV2 , jsonFileName , consolidated ) ;
188282
189283 console . log (
190- `generated JSON file with ${ jsonArray . length } records: ${ jsonFileName } `
284+ `generated JSON file with ${ consolidated . length } records: ${ jsonFileName } `
191285 ) ;
192286
193287 const logLines : string [ ] = [ ] ;
0 commit comments