sinonym 0.2.7__tar.gz → 0.2.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. {sinonym-0.2.7 → sinonym-0.2.8}/.gitignore +2 -0
  2. {sinonym-0.2.7 → sinonym-0.2.8}/PKG-INFO +54 -12
  3. {sinonym-0.2.7 → sinonym-0.2.8}/README.md +53 -11
  4. {sinonym-0.2.7 → sinonym-0.2.8}/pyproject.toml +6 -2
  5. {sinonym-0.2.7 → sinonym-0.2.8}/scripts/README.md +14 -1
  6. sinonym-0.2.8/scripts/check_test_status.py +511 -0
  7. sinonym-0.2.8/scripts/name_order_routing_rules.py +163 -0
  8. {sinonym-0.2.7 → sinonym-0.2.8}/sinonym/chinese_names_data.py +7 -2
  9. {sinonym-0.2.7 → sinonym-0.2.8}/sinonym/coretypes/__init__.py +2 -0
  10. {sinonym-0.2.7 → sinonym-0.2.8}/sinonym/coretypes/results.py +58 -5
  11. sinonym-0.2.8/sinonym/data/name_order_routing/README.md +29 -0
  12. sinonym-0.2.8/sinonym/data/name_order_routing/pp_abstain_labels.jsonl +750 -0
  13. sinonym-0.2.8/sinonym/data/name_order_routing/pp_vys_abstain_labels.jsonl +1000 -0
  14. {sinonym-0.2.7 → sinonym-0.2.8}/sinonym/detector.py +489 -87
  15. sinonym-0.2.8/sinonym/pipeline/__init__.py +1 -0
  16. sinonym-0.2.8/sinonym/pipeline/name_order_routing.py +1334 -0
  17. {sinonym-0.2.7 → sinonym-0.2.8}/sinonym/services/__init__.py +6 -1
  18. sinonym-0.2.8/sinonym/services/batch_analysis.py +1453 -0
  19. {sinonym-0.2.7 → sinonym-0.2.8}/sinonym/services/ethnicity.py +116 -26
  20. {sinonym-0.2.7 → sinonym-0.2.8}/sinonym/services/formatting.py +14 -7
  21. {sinonym-0.2.7 → sinonym-0.2.8}/sinonym/services/initialization.py +7 -10
  22. sinonym-0.2.8/sinonym/services/non_person.py +134 -0
  23. {sinonym-0.2.7 → sinonym-0.2.8}/sinonym/services/normalization.py +123 -5
  24. sinonym-0.2.8/sinonym/services/order_metadata.py +116 -0
  25. {sinonym-0.2.7 → sinonym-0.2.8}/sinonym/services/parsing.py +136 -34
  26. {sinonym-0.2.7 → sinonym-0.2.8}/sinonym/services/process_pool.py +28 -11
  27. {sinonym-0.2.7 → sinonym-0.2.8}/sinonym/text_processing/text_normalizer.py +26 -3
  28. {sinonym-0.2.7 → sinonym-0.2.8}/sinonym/text_processing/text_preprocessor.py +60 -28
  29. {sinonym-0.2.7 → sinonym-0.2.8}/sinonym/timo/integration_test.py +8 -9
  30. sinonym-0.2.8/sinonym/timo/interface.py +347 -0
  31. sinonym-0.2.8/tests/_case_assertions.py +46 -0
  32. {sinonym-0.2.7 → sinonym-0.2.8}/tests/test_acl.py +23 -82
  33. {sinonym-0.2.7 → sinonym-0.2.8}/tests/test_all_chinese_inputs.py +7 -45
  34. {sinonym-0.2.7 → sinonym-0.2.8}/tests/test_basic_chinese_names.py +5 -37
  35. {sinonym-0.2.7 → sinonym-0.2.8}/tests/test_batch.py +289 -60
  36. sinonym-0.2.8/tests/test_check_test_status.py +237 -0
  37. sinonym-0.2.8/tests/test_compound_names.py +40 -0
  38. {sinonym-0.2.7 → sinonym-0.2.8}/tests/test_compound_surname_formats.py +6 -49
  39. sinonym-0.2.8/tests/test_middle_names.py +298 -0
  40. {sinonym-0.2.7 → sinonym-0.2.8}/tests/test_misc.py +4 -80
  41. {sinonym-0.2.7 → sinonym-0.2.8}/tests/test_mixed_production_cases.py +5 -45
  42. sinonym-0.2.8/tests/test_mixed_scripts.py +43 -0
  43. {sinonym-0.2.7 → sinonym-0.2.8}/tests/test_ml_japanese_detection.py +13 -62
  44. {sinonym-0.2.7 → sinonym-0.2.8}/tests/test_multiprocess_pool.py +28 -4
  45. {sinonym-0.2.7 → sinonym-0.2.8}/tests/test_name_formatting.py +5 -36
  46. sinonym-0.2.8/tests/test_name_order_routing_label_fixtures.py +74 -0
  47. sinonym-0.2.8/tests/test_name_order_routing_rules.py +879 -0
  48. {sinonym-0.2.7 → sinonym-0.2.8}/tests/test_non_chinese_rejection.py +7 -30
  49. {sinonym-0.2.7 → sinonym-0.2.8}/tests/test_regional_variants.py +5 -36
  50. sinonym-0.2.8/tests/test_regression_proposals.py +1215 -0
  51. sinonym-0.2.8/tests/test_timo_interface.py +223 -0
  52. sinonym-0.2.7/sinonym/services/batch_analysis.py +0 -755
  53. sinonym-0.2.7/sinonym/timo/interface.py +0 -244
  54. sinonym-0.2.7/tests/_fail_log.py +0 -24
  55. sinonym-0.2.7/tests/test_compound_names.py +0 -71
  56. sinonym-0.2.7/tests/test_middle_names.py +0 -378
  57. sinonym-0.2.7/tests/test_mixed_scripts.py +0 -74
  58. sinonym-0.2.7/tests/test_regression_proposals.py +0 -561
  59. sinonym-0.2.7/tests/test_timo_interface.py +0 -87
  60. {sinonym-0.2.7 → sinonym-0.2.8}/LICENSE +0 -0
  61. {sinonym-0.2.7 → sinonym-0.2.8}/sinonym/__init__.py +0 -0
  62. {sinonym-0.2.7 → sinonym-0.2.8}/sinonym/coretypes/config.py +0 -0
  63. {sinonym-0.2.7 → sinonym-0.2.8}/sinonym/data/README.md +0 -0
  64. {sinonym-0.2.7 → sinonym-0.2.8}/sinonym/data/acl_2025_authors.txt +0 -0
  65. {sinonym-0.2.7 → sinonym-0.2.8}/sinonym/data/chinese_japanese_classifier.joblib +0 -0
  66. {sinonym-0.2.7 → sinonym-0.2.8}/sinonym/data/chinese_japanese_classifier.skops +0 -0
  67. {sinonym-0.2.7 → sinonym-0.2.8}/sinonym/data/familyname_orcid.csv +0 -0
  68. {sinonym-0.2.7 → sinonym-0.2.8}/sinonym/data/givenname_orcid.csv +0 -0
  69. {sinonym-0.2.7 → sinonym-0.2.8}/sinonym/data/model_features.json +0 -0
  70. {sinonym-0.2.7 → sinonym-0.2.8}/sinonym/ml_model_components.py +0 -0
  71. {sinonym-0.2.7 → sinonym-0.2.8}/sinonym/patterns/__init__.py +0 -0
  72. {sinonym-0.2.7 → sinonym-0.2.8}/sinonym/patterns/compiled_patterns.py +0 -0
  73. {sinonym-0.2.7 → sinonym-0.2.8}/sinonym/patterns/regex_builders.py +0 -0
  74. {sinonym-0.2.7 → sinonym-0.2.8}/sinonym/resources.py +0 -0
  75. {sinonym-0.2.7 → sinonym-0.2.8}/sinonym/services/cache.py +0 -0
  76. {sinonym-0.2.7 → sinonym-0.2.8}/sinonym/text_processing/__init__.py +0 -0
  77. {sinonym-0.2.7 → sinonym-0.2.8}/sinonym/text_processing/compound_detector.py +0 -0
  78. {sinonym-0.2.7 → sinonym-0.2.8}/sinonym/timo/__init__.py +0 -0
  79. {sinonym-0.2.7 → sinonym-0.2.8}/sinonym/timo/config.yaml +0 -0
  80. {sinonym-0.2.7 → sinonym-0.2.8}/sinonym/utils/__init__.py +0 -0
  81. {sinonym-0.2.7 → sinonym-0.2.8}/sinonym/utils/string_manipulation.py +0 -0
  82. {sinonym-0.2.7 → sinonym-0.2.8}/sinonym/utils/thread_cache.py +0 -0
  83. {sinonym-0.2.7 → sinonym-0.2.8}/tests/conftest.py +0 -0
  84. {sinonym-0.2.7 → sinonym-0.2.8}/tests/test_performance.py +0 -0
@@ -5,6 +5,8 @@ __pycache__/
5
5
  .claude
6
6
  claude_scripts/
7
7
  data_backup/
8
+ scratch/
9
+ .pytest_failures.txt
8
10
 
9
11
  # C extensions
10
12
  *.so
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sinonym
3
- Version: 0.2.7
3
+ Version: 0.2.8
4
4
  Summary: Chinese Name Detection and Normalization Module
5
5
  Project-URL: Homepage, https://github.com/allenai/sinonym
6
6
  Project-URL: Repository, https://github.com/allenai/sinonym
@@ -246,7 +246,11 @@ author_list = ["Zhang Wei", "Li Ming", "Wang Xiaoli", "Liu Jiaming", "Feng Cha"]
246
246
  batch_result = detector.analyze_name_batch(author_list)
247
247
  print(f"Format detected: {batch_result.format_pattern.dominant_format}")
248
248
  print(f"Confidence: {batch_result.format_pattern.confidence:.1%}")
249
- # Expected Output: Format detected: NameFormat.SURNAME_FIRST, Confidence: 94%
249
+ print(f"Decision confidence: {batch_result.format_pattern.decision_confidence:.1%}")
250
+ # Expected Output:
251
+ # Format detected: NameFormat.SURNAME_FIRST
252
+ # Confidence: 80.0%
253
+ # Decision confidence: 80.0%
250
254
 
251
255
  for i, result in enumerate(batch_result.results):
252
256
  if result.success:
@@ -258,7 +262,7 @@ unknown_format_list = ["Wei Zhang", "Ming Li", "Xiaoli Wang"]
258
262
  pattern = detector.detect_batch_format(unknown_format_list)
259
263
  if pattern.threshold_met:
260
264
  print(f"Consistent {pattern.dominant_format} formatting detected")
261
- print(f"Safe to process as batch with {pattern.confidence:.1%} confidence")
265
+ print(f"Safe to process as batch with {pattern.decision_confidence:.1%} decision confidence")
262
266
  else:
263
267
  print("Mixed formatting detected - process individually")
264
268
 
@@ -282,7 +286,9 @@ When you call `normalize_name`, you get a `ParseResult` with helpful structured
282
286
  - `surname_tokens`, `given_tokens`: normalized, capitalized tokens used to form components
283
287
  - `middle_tokens`: trailing single-letter initials extracted from given name, if present
284
288
  - `order`: component order descriptor, typically `["given", "middle", "surname"]`
285
- - `parsed_original_order`: A `ParsedName` aligned to the input’s original order. In this view, the labels are positional: `given` corresponds to the first component(s) in the original input, and `surname` to the last component(s), regardless of the semantic roles used for normalization.
289
+ - `parsed_original_order`: A `ParsedName` with the same semantic `surname` and
290
+ `given_name` labels as `parsed`, plus an `order` list that records how those
291
+ components appeared in the input.
286
292
 
287
293
  Notes:
288
294
  - The tokens in `parsed` and `parsed_original_order` are the same normalized tokens; only the conceptual ordering differs via the `order` list.
@@ -295,8 +301,8 @@ res = detector.normalize_name("Li Wei")
295
301
  # res.result == "Wei Li"
296
302
  # res.parsed.order == ["given", "middle", "surname"]
297
303
  # res.parsed_original_order.order == ["surname", "given"]
298
- # res.parsed_original_order.given_name == "Li" # first in input
299
- # res.parsed_original_order.surname == "Wei" # last in input
304
+ # res.parsed_original_order.given_name == "Wei"
305
+ # res.parsed_original_order.surname == "Li"
300
306
 
301
307
  res = detector.normalize_name("Chi-Ying F. Huang")
302
308
  # res.result == "Chi-Ying F Huang"
@@ -318,7 +324,7 @@ When processing multiple names together, Sinonym:
318
324
 
319
325
  1. **Detects Format Patterns**: Analyzes the entire batch to identify whether names follow a surname-first (e.g., "Zhang Wei") or given-first (e.g., "Wei Zhang") pattern
320
326
  2. **Aggregates Evidence**: Uses frequency statistics across all names to build confidence in the detected pattern
321
- 3. **Applies Consistent Formatting**: When confidence exceeds 67%, applies the detected pattern to improve parsing of ambiguous individual names
327
+ 3. **Applies Consistent Formatting**: When `decision_confidence` exceeds the configured threshold, applies the detected pattern to improve parsing of ambiguous individual names
322
328
  4. **Tracks Improvements**: Identifies which names benefit from batch context vs. individual processing
323
329
 
324
330
  ### Key Benefits
@@ -341,7 +347,10 @@ result = detector.analyze_name_batch([
341
347
  ])
342
348
  print(f"Format detected: {result.format_pattern.dominant_format}")
343
349
  print(f"Confidence: {result.format_pattern.confidence:.1%}")
350
+ print(f"Decision confidence: {result.format_pattern.decision_confidence:.1%}")
351
+ print(f"Vote margin: {result.format_pattern.vote_margin:.1%}")
344
352
  print(f"Improved names: {len(result.improvements)}")
353
+ print(result.name_order_evidence[0].selected_surname_position)
345
354
 
346
355
  # Quick format detection without full processing
347
356
  pattern = detector.detect_batch_format([
@@ -358,6 +367,37 @@ for result in results:
358
367
  print(f"Processed: {result.result}")
359
368
  ```
360
369
 
370
+ Use `analyze_name_batch()` when you need to choose between two batch contexts
371
+ such as paper-level and source/venue/year-level runs. `process_name_batch()`
372
+ returns only the final `ParseResult` list and intentionally drops the routing
373
+ evidence.
374
+
375
+ `BatchFormatPattern` exposes batch-level convention evidence:
376
+
377
+ - `dominant_format`, `confidence`, and `threshold_met`
378
+ - `decision_confidence`, the score used by the batch-application gate
379
+ - `surname_first_count`, `given_first_count`, and `total_count`
380
+ - `voting_count`, `vote_margin_count`, and `vote_margin`
381
+
382
+ `BatchParseResult.name_order_evidence` is aligned with `names` and `results`.
383
+ Each `NameOrderEvidence` contains stable evidence for external context-routing
384
+ rules:
385
+
386
+ - token shape: `raw_tokens`, `raw_token_count`, `has_all_caps_token`,
387
+ `all_caps_tokens`
388
+ - batch behavior: `script_representation`, `batch_participant`,
389
+ `batch_applied`, `batch_changed_format`
390
+ - order choices: `individual_format`, `selected_format`,
391
+ `selected_surname_position`
392
+ - endpoint frequency evidence: `first_token_surname_frequency`,
393
+ `last_token_surname_frequency`, `selected_surname_frequency`,
394
+ `alternate_endpoint_surname_frequency`,
395
+ `selected_over_alternate_surname_frequency_ratio`
396
+
397
+ Caller-owned metadata such as source, venue, and year is not inferred by
398
+ sinonym. Keep that metadata beside the PP/VYS batch calls and combine it with
399
+ the emitted evidence in the external router.
400
+
361
401
  ### Persistent Multi-Process Processing
362
402
 
363
403
  For high-throughput workloads, you can keep a persistent process pool alive and
@@ -377,7 +417,7 @@ def main():
377
417
  results_a = pool.normalize_names(names_a)
378
418
  results_b = pool.normalize_names(names_b)
379
419
 
380
- # One-off convenience wrapper (creates and closes a temporary pool)
420
+ # One-off compatibility wrapper with full batch-context semantics
381
421
  single_batch = detector.process_name_batch_multiprocess(names_a, max_workers=6, chunk_size=64)
382
422
  return results_a, results_b, single_batch
383
423
 
@@ -400,7 +440,9 @@ Batch processing requires a minimum of 2 names and works best with 5+ names for
400
440
 
401
441
  **Unambiguous Names**: Some names have only one possible parsing format (e.g., compound given names like "Wei‑Qi Wang"). Batch processing never forces such names into the detected pattern and never raises. These names keep their best individual parse while other Chinese names benefit from the jointly detected order.
402
442
 
403
- **Confidence (Advisory Only)**: Batch detection computes a dominant format and a confidence value, but there is no confidence threshold gating. Results are always returned. The confidence is informational (e.g., for logging/UX) and is not used to raise errors.
443
+ **Batch Application Threshold**: Batch detection keeps count-based evidence (`confidence`, counts, and vote margin) separate from the application decision (`decision_confidence`). Batch formatting is applied only when the direction is confident, at least two vote-eligible Latin-only Chinese names participate, and `decision_confidence` clears the configured threshold. Latin rows with all-caps source-token cues are exposed in `name_order_evidence` but do not vote in, or receive, Latin batch formatting.
444
+
445
+ **Script Cohorts**: Vote-eligible Latin-only names vote in and receive Latin batch formatting. Han-only, explicitly aligned Han/Roman, and other mixed-script names are parsed from their own script evidence so a Latin batch convention does not flip their order.
404
446
 
405
447
  ### Batch Processing with Mixed Name Types
406
448
 
@@ -423,7 +465,7 @@ mixed_names = [
423
465
 
424
466
  result = detector.analyze_name_batch(mixed_names)
425
467
 
426
- # Format detection uses only the 8 Chinese names
468
+ # Format detection uses only the 8 Latin-only Chinese names
427
469
  # If 7 prefer GIVEN_FIRST vs 1 SURNAME_FIRST = 87.5% confidence
428
470
  # GIVEN_FIRST pattern is applied to Chinese names; non‑Chinese names return clear failures
429
471
 
@@ -449,8 +491,8 @@ for i, (name, result_obj) in enumerate(zip(mixed_names, result.results)):
449
491
 
450
492
  **Key Benefits:**
451
493
  - **Maintains input-output correspondence**: Results array matches input array length and order
452
- - **Robust format detection**: Only valid Chinese names contribute to pattern detection
453
- - **Consistent formatting**: All Chinese names get the same detected format applied
494
+ - **Robust format detection**: Only valid Latin-only Chinese names contribute to Latin batch pattern detection
495
+ - **Consistent formatting**: Latin-only Chinese names get the detected format applied when the batch signal is strong enough
454
496
  - **Clear failure reporting**: Non-Chinese names are clearly marked as failed with error messages
455
497
 
456
498
  ## Development
@@ -208,7 +208,11 @@ author_list = ["Zhang Wei", "Li Ming", "Wang Xiaoli", "Liu Jiaming", "Feng Cha"]
208
208
  batch_result = detector.analyze_name_batch(author_list)
209
209
  print(f"Format detected: {batch_result.format_pattern.dominant_format}")
210
210
  print(f"Confidence: {batch_result.format_pattern.confidence:.1%}")
211
- # Expected Output: Format detected: NameFormat.SURNAME_FIRST, Confidence: 94%
211
+ print(f"Decision confidence: {batch_result.format_pattern.decision_confidence:.1%}")
212
+ # Expected Output:
213
+ # Format detected: NameFormat.SURNAME_FIRST
214
+ # Confidence: 80.0%
215
+ # Decision confidence: 80.0%
212
216
 
213
217
  for i, result in enumerate(batch_result.results):
214
218
  if result.success:
@@ -220,7 +224,7 @@ unknown_format_list = ["Wei Zhang", "Ming Li", "Xiaoli Wang"]
220
224
  pattern = detector.detect_batch_format(unknown_format_list)
221
225
  if pattern.threshold_met:
222
226
  print(f"Consistent {pattern.dominant_format} formatting detected")
223
- print(f"Safe to process as batch with {pattern.confidence:.1%} confidence")
227
+ print(f"Safe to process as batch with {pattern.decision_confidence:.1%} decision confidence")
224
228
  else:
225
229
  print("Mixed formatting detected - process individually")
226
230
 
@@ -244,7 +248,9 @@ When you call `normalize_name`, you get a `ParseResult` with helpful structured
244
248
  - `surname_tokens`, `given_tokens`: normalized, capitalized tokens used to form components
245
249
  - `middle_tokens`: trailing single-letter initials extracted from given name, if present
246
250
  - `order`: component order descriptor, typically `["given", "middle", "surname"]`
247
- - `parsed_original_order`: A `ParsedName` aligned to the input’s original order. In this view, the labels are positional: `given` corresponds to the first component(s) in the original input, and `surname` to the last component(s), regardless of the semantic roles used for normalization.
251
+ - `parsed_original_order`: A `ParsedName` with the same semantic `surname` and
252
+ `given_name` labels as `parsed`, plus an `order` list that records how those
253
+ components appeared in the input.
248
254
 
249
255
  Notes:
250
256
  - The tokens in `parsed` and `parsed_original_order` are the same normalized tokens; only the conceptual ordering differs via the `order` list.
@@ -257,8 +263,8 @@ res = detector.normalize_name("Li Wei")
257
263
  # res.result == "Wei Li"
258
264
  # res.parsed.order == ["given", "middle", "surname"]
259
265
  # res.parsed_original_order.order == ["surname", "given"]
260
- # res.parsed_original_order.given_name == "Li" # first in input
261
- # res.parsed_original_order.surname == "Wei" # last in input
266
+ # res.parsed_original_order.given_name == "Wei"
267
+ # res.parsed_original_order.surname == "Li"
262
268
 
263
269
  res = detector.normalize_name("Chi-Ying F. Huang")
264
270
  # res.result == "Chi-Ying F Huang"
@@ -280,7 +286,7 @@ When processing multiple names together, Sinonym:
280
286
 
281
287
  1. **Detects Format Patterns**: Analyzes the entire batch to identify whether names follow a surname-first (e.g., "Zhang Wei") or given-first (e.g., "Wei Zhang") pattern
282
288
  2. **Aggregates Evidence**: Uses frequency statistics across all names to build confidence in the detected pattern
283
- 3. **Applies Consistent Formatting**: When confidence exceeds 67%, applies the detected pattern to improve parsing of ambiguous individual names
289
+ 3. **Applies Consistent Formatting**: When `decision_confidence` exceeds the configured threshold, applies the detected pattern to improve parsing of ambiguous individual names
284
290
  4. **Tracks Improvements**: Identifies which names benefit from batch context vs. individual processing
285
291
 
286
292
  ### Key Benefits
@@ -303,7 +309,10 @@ result = detector.analyze_name_batch([
303
309
  ])
304
310
  print(f"Format detected: {result.format_pattern.dominant_format}")
305
311
  print(f"Confidence: {result.format_pattern.confidence:.1%}")
312
+ print(f"Decision confidence: {result.format_pattern.decision_confidence:.1%}")
313
+ print(f"Vote margin: {result.format_pattern.vote_margin:.1%}")
306
314
  print(f"Improved names: {len(result.improvements)}")
315
+ print(result.name_order_evidence[0].selected_surname_position)
307
316
 
308
317
  # Quick format detection without full processing
309
318
  pattern = detector.detect_batch_format([
@@ -320,6 +329,37 @@ for result in results:
320
329
  print(f"Processed: {result.result}")
321
330
  ```
322
331
 
332
+ Use `analyze_name_batch()` when you need to choose between two batch contexts
333
+ such as paper-level and source/venue/year-level runs. `process_name_batch()`
334
+ returns only the final `ParseResult` list and intentionally drops the routing
335
+ evidence.
336
+
337
+ `BatchFormatPattern` exposes batch-level convention evidence:
338
+
339
+ - `dominant_format`, `confidence`, and `threshold_met`
340
+ - `decision_confidence`, the score used by the batch-application gate
341
+ - `surname_first_count`, `given_first_count`, and `total_count`
342
+ - `voting_count`, `vote_margin_count`, and `vote_margin`
343
+
344
+ `BatchParseResult.name_order_evidence` is aligned with `names` and `results`.
345
+ Each `NameOrderEvidence` contains stable evidence for external context-routing
346
+ rules:
347
+
348
+ - token shape: `raw_tokens`, `raw_token_count`, `has_all_caps_token`,
349
+ `all_caps_tokens`
350
+ - batch behavior: `script_representation`, `batch_participant`,
351
+ `batch_applied`, `batch_changed_format`
352
+ - order choices: `individual_format`, `selected_format`,
353
+ `selected_surname_position`
354
+ - endpoint frequency evidence: `first_token_surname_frequency`,
355
+ `last_token_surname_frequency`, `selected_surname_frequency`,
356
+ `alternate_endpoint_surname_frequency`,
357
+ `selected_over_alternate_surname_frequency_ratio`
358
+
359
+ Caller-owned metadata such as source, venue, and year is not inferred by
360
+ sinonym. Keep that metadata beside the PP/VYS batch calls and combine it with
361
+ the emitted evidence in the external router.
362
+
323
363
  ### Persistent Multi-Process Processing
324
364
 
325
365
  For high-throughput workloads, you can keep a persistent process pool alive and
@@ -339,7 +379,7 @@ def main():
339
379
  results_a = pool.normalize_names(names_a)
340
380
  results_b = pool.normalize_names(names_b)
341
381
 
342
- # One-off convenience wrapper (creates and closes a temporary pool)
382
+ # One-off compatibility wrapper with full batch-context semantics
343
383
  single_batch = detector.process_name_batch_multiprocess(names_a, max_workers=6, chunk_size=64)
344
384
  return results_a, results_b, single_batch
345
385
 
@@ -362,7 +402,9 @@ Batch processing requires a minimum of 2 names and works best with 5+ names for
362
402
 
363
403
  **Unambiguous Names**: Some names have only one possible parsing format (e.g., compound given names like "Wei‑Qi Wang"). Batch processing never forces such names into the detected pattern and never raises. These names keep their best individual parse while other Chinese names benefit from the jointly detected order.
364
404
 
365
- **Confidence (Advisory Only)**: Batch detection computes a dominant format and a confidence value, but there is no confidence threshold gating. Results are always returned. The confidence is informational (e.g., for logging/UX) and is not used to raise errors.
405
+ **Batch Application Threshold**: Batch detection keeps count-based evidence (`confidence`, counts, and vote margin) separate from the application decision (`decision_confidence`). Batch formatting is applied only when the direction is confident, at least two vote-eligible Latin-only Chinese names participate, and `decision_confidence` clears the configured threshold. Latin rows with all-caps source-token cues are exposed in `name_order_evidence` but do not vote in, or receive, Latin batch formatting.
406
+
407
+ **Script Cohorts**: Vote-eligible Latin-only names vote in and receive Latin batch formatting. Han-only, explicitly aligned Han/Roman, and other mixed-script names are parsed from their own script evidence so a Latin batch convention does not flip their order.
366
408
 
367
409
  ### Batch Processing with Mixed Name Types
368
410
 
@@ -385,7 +427,7 @@ mixed_names = [
385
427
 
386
428
  result = detector.analyze_name_batch(mixed_names)
387
429
 
388
- # Format detection uses only the 8 Chinese names
430
+ # Format detection uses only the 8 Latin-only Chinese names
389
431
  # If 7 prefer GIVEN_FIRST vs 1 SURNAME_FIRST = 87.5% confidence
390
432
  # GIVEN_FIRST pattern is applied to Chinese names; non‑Chinese names return clear failures
391
433
 
@@ -411,8 +453,8 @@ for i, (name, result_obj) in enumerate(zip(mixed_names, result.results)):
411
453
 
412
454
  **Key Benefits:**
413
455
  - **Maintains input-output correspondence**: Results array matches input array length and order
414
- - **Robust format detection**: Only valid Chinese names contribute to pattern detection
415
- - **Consistent formatting**: All Chinese names get the same detected format applied
456
+ - **Robust format detection**: Only valid Latin-only Chinese names contribute to Latin batch pattern detection
457
+ - **Consistent formatting**: Latin-only Chinese names get the detected format applied when the batch signal is strong enough
416
458
  - **Clear failure reporting**: Non-Chinese names are clearly marked as failed with error messages
417
459
 
418
460
  ## Development
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sinonym"
7
- version = "0.2.7"
7
+ version = "0.2.8"
8
8
  description = "Chinese Name Detection and Normalization Module"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -61,6 +61,8 @@ include = [
61
61
  include = [
62
62
  "sinonym",
63
63
  "tests",
64
+ "scripts/check_test_status.py",
65
+ "scripts/name_order_routing_rules.py",
64
66
  "README.md",
65
67
  "pyproject.toml",
66
68
  ]
@@ -80,7 +82,9 @@ select = ["E", "F", "W", "C90", "I", "N", "UP", "YTT", "S", "BLE", "FBT", "B", "
80
82
  ignore = ["S101", "T201"] # Allow assert statements and print statements
81
83
 
82
84
  [tool.ruff.lint.per-file-ignores]
83
- "tests/*" = ["S101", "T201", "ARG", "FBT"]
85
+ "tests/*" = ["S101", "T201", "ARG", "FBT", "INP001"]
86
+ "tests/test_mixed_scripts.py" = ["RUF001"]
87
+ "tests/test_non_chinese_rejection.py" = ["RUF001"]
84
88
 
85
89
  [tool.mypy]
86
90
  python_version = "3.10"
@@ -5,7 +5,7 @@ Utility scripts for benchmarking, profiling, testing, and model training.
5
5
  ## Active Scripts
6
6
 
7
7
  ### `check_test_status.py`
8
- Runs the full test suite and reports individual test case failures with detailed diagnostics. Runs performance tests separately, then exits 0/1 based on whether the failure count matches the expected baseline configured in `scripts/check_test_status.py`. Improvements (fewer failures) also pass; regressions fail.
8
+ Runs the full test suite and reports individual test case failures with detailed diagnostics. Runs performance tests separately, then exits 0/1 based on whether the failure count and failure signatures match the expected baseline configured in `scripts/check_test_status.py`. Improvements (fewer failures) also pass when the remaining failures are all from the known baseline; regressions or unexpected failure signatures fail.
9
9
 
10
10
  ```bash
11
11
  uv run python scripts/check_test_status.py
@@ -54,6 +54,19 @@ Trains the Chinese-vs-Japanese name classifier used in production. Downloads Chi
54
54
  uv run python scripts/train_ml_classifier_for_chinese_vs_japanese.py
55
55
  ```
56
56
 
57
+ ### `name_order_routing_rules.py`
58
+ Applies the external routing rules for context runs that compare paper-level PP, VYS, input-order abstain, and terminal non-person outputs. The routing policy lives in `sinonym.pipeline.name_order_routing`; this script is the file-format CLI wrapper for already-expanded routing rows and adds `router_prediction` plus `router_reason`.
59
+ `router_prediction` can be `pp`, `vys`, `abstain`, or `not_person`; `not_person` means no person parse should be emitted.
60
+ For PP/VYS/abstain, input rows contain parser evidence from aligned PP and VYS runs. The router derives `old_prediction`, `old_reason`, `new_prediction`, and `new_reason` as audit output; those fields are not policy inputs.
61
+ For direct PP/VYS `BatchParseResult` usage, call `build_pp_vys_abstain_rows` or `route_pp_vys_abstain_batches`; those functions use the same evidence-row policy path as the file CLI.
62
+ The PP/VYS/abstain row regime requires raw `name` so the narrow validated name-prior guards can run.
63
+ CSV and JSONL use only the standard library; Parquet inputs/outputs require pandas plus a Parquet engine in the runtime environment.
64
+
65
+ ```bash
66
+ uv run python scripts/name_order_routing_rules.py pp-vys-abstain --input pp_vys_features.parquet --output routed.parquet
67
+ uv run python scripts/name_order_routing_rules.py pp-abstain --input pp_only_features.parquet --output routed.parquet
68
+ ```
69
+
57
70
  ## Abandoned Scripts
58
71
 
59
72
  These remain for historical reference but are not used by the library. The rule-based parser in `sinonym.services.parsing` replaced the ML approach.