sinonym 0.2.5__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 (82) hide show
  1. {sinonym-0.2.5 → sinonym-0.2.8}/.gitignore +2 -0
  2. {sinonym-0.2.5 → sinonym-0.2.8}/PKG-INFO +55 -12
  3. {sinonym-0.2.5 → sinonym-0.2.8}/README.md +53 -11
  4. {sinonym-0.2.5 → sinonym-0.2.8}/pyproject.toml +7 -2
  5. {sinonym-0.2.5 → 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.5 → sinonym-0.2.8}/sinonym/chinese_names_data.py +7 -2
  9. {sinonym-0.2.5 → sinonym-0.2.8}/sinonym/coretypes/__init__.py +2 -0
  10. {sinonym-0.2.5 → 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.5 → 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.5 → 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.5 → sinonym-0.2.8}/sinonym/services/ethnicity.py +116 -26
  20. {sinonym-0.2.5 → sinonym-0.2.8}/sinonym/services/formatting.py +14 -7
  21. {sinonym-0.2.5 → 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.5 → 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.5 → sinonym-0.2.8}/sinonym/services/parsing.py +136 -34
  26. {sinonym-0.2.5 → sinonym-0.2.8}/sinonym/services/process_pool.py +28 -11
  27. {sinonym-0.2.5 → sinonym-0.2.8}/sinonym/text_processing/text_normalizer.py +26 -3
  28. {sinonym-0.2.5 → sinonym-0.2.8}/sinonym/text_processing/text_preprocessor.py +60 -28
  29. sinonym-0.2.8/sinonym/timo/__init__.py +0 -0
  30. sinonym-0.2.8/sinonym/timo/config.yaml +12 -0
  31. sinonym-0.2.8/sinonym/timo/integration_test.py +69 -0
  32. sinonym-0.2.8/sinonym/timo/interface.py +347 -0
  33. sinonym-0.2.8/tests/_case_assertions.py +46 -0
  34. {sinonym-0.2.5 → sinonym-0.2.8}/tests/test_acl.py +23 -82
  35. {sinonym-0.2.5 → sinonym-0.2.8}/tests/test_all_chinese_inputs.py +7 -45
  36. {sinonym-0.2.5 → sinonym-0.2.8}/tests/test_basic_chinese_names.py +5 -37
  37. {sinonym-0.2.5 → sinonym-0.2.8}/tests/test_batch.py +289 -60
  38. sinonym-0.2.8/tests/test_check_test_status.py +237 -0
  39. sinonym-0.2.8/tests/test_compound_names.py +40 -0
  40. {sinonym-0.2.5 → sinonym-0.2.8}/tests/test_compound_surname_formats.py +6 -49
  41. sinonym-0.2.8/tests/test_middle_names.py +298 -0
  42. {sinonym-0.2.5 → sinonym-0.2.8}/tests/test_misc.py +4 -80
  43. {sinonym-0.2.5 → sinonym-0.2.8}/tests/test_mixed_production_cases.py +5 -45
  44. sinonym-0.2.8/tests/test_mixed_scripts.py +43 -0
  45. {sinonym-0.2.5 → sinonym-0.2.8}/tests/test_ml_japanese_detection.py +13 -62
  46. {sinonym-0.2.5 → sinonym-0.2.8}/tests/test_multiprocess_pool.py +28 -4
  47. {sinonym-0.2.5 → sinonym-0.2.8}/tests/test_name_formatting.py +5 -36
  48. sinonym-0.2.8/tests/test_name_order_routing_label_fixtures.py +74 -0
  49. sinonym-0.2.8/tests/test_name_order_routing_rules.py +879 -0
  50. {sinonym-0.2.5 → sinonym-0.2.8}/tests/test_non_chinese_rejection.py +7 -30
  51. {sinonym-0.2.5 → sinonym-0.2.8}/tests/test_regional_variants.py +5 -36
  52. sinonym-0.2.8/tests/test_regression_proposals.py +1215 -0
  53. sinonym-0.2.8/tests/test_timo_interface.py +223 -0
  54. sinonym-0.2.5/sinonym/services/batch_analysis.py +0 -755
  55. sinonym-0.2.5/tests/_fail_log.py +0 -24
  56. sinonym-0.2.5/tests/test_compound_names.py +0 -71
  57. sinonym-0.2.5/tests/test_middle_names.py +0 -378
  58. sinonym-0.2.5/tests/test_mixed_scripts.py +0 -74
  59. sinonym-0.2.5/tests/test_regression_proposals.py +0 -561
  60. {sinonym-0.2.5 → sinonym-0.2.8}/LICENSE +0 -0
  61. {sinonym-0.2.5 → sinonym-0.2.8}/sinonym/__init__.py +0 -0
  62. {sinonym-0.2.5 → sinonym-0.2.8}/sinonym/coretypes/config.py +0 -0
  63. {sinonym-0.2.5 → sinonym-0.2.8}/sinonym/data/README.md +0 -0
  64. {sinonym-0.2.5 → sinonym-0.2.8}/sinonym/data/acl_2025_authors.txt +0 -0
  65. {sinonym-0.2.5 → sinonym-0.2.8}/sinonym/data/chinese_japanese_classifier.joblib +0 -0
  66. {sinonym-0.2.5 → sinonym-0.2.8}/sinonym/data/chinese_japanese_classifier.skops +0 -0
  67. {sinonym-0.2.5 → sinonym-0.2.8}/sinonym/data/familyname_orcid.csv +0 -0
  68. {sinonym-0.2.5 → sinonym-0.2.8}/sinonym/data/givenname_orcid.csv +0 -0
  69. {sinonym-0.2.5 → sinonym-0.2.8}/sinonym/data/model_features.json +0 -0
  70. {sinonym-0.2.5 → sinonym-0.2.8}/sinonym/ml_model_components.py +0 -0
  71. {sinonym-0.2.5 → sinonym-0.2.8}/sinonym/patterns/__init__.py +0 -0
  72. {sinonym-0.2.5 → sinonym-0.2.8}/sinonym/patterns/compiled_patterns.py +0 -0
  73. {sinonym-0.2.5 → sinonym-0.2.8}/sinonym/patterns/regex_builders.py +0 -0
  74. {sinonym-0.2.5 → sinonym-0.2.8}/sinonym/resources.py +0 -0
  75. {sinonym-0.2.5 → sinonym-0.2.8}/sinonym/services/cache.py +0 -0
  76. {sinonym-0.2.5 → sinonym-0.2.8}/sinonym/text_processing/__init__.py +0 -0
  77. {sinonym-0.2.5 → sinonym-0.2.8}/sinonym/text_processing/compound_detector.py +0 -0
  78. {sinonym-0.2.5 → sinonym-0.2.8}/sinonym/utils/__init__.py +0 -0
  79. {sinonym-0.2.5 → sinonym-0.2.8}/sinonym/utils/string_manipulation.py +0 -0
  80. {sinonym-0.2.5 → sinonym-0.2.8}/sinonym/utils/thread_cache.py +0 -0
  81. {sinonym-0.2.5 → sinonym-0.2.8}/tests/conftest.py +0 -0
  82. {sinonym-0.2.5 → 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.5
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
@@ -21,6 +21,7 @@ Classifier: Topic :: Text Processing :: Linguistic
21
21
  Requires-Python: >=3.10
22
22
  Requires-Dist: joblib>=1.3
23
23
  Requires-Dist: numpy>=1.24
24
+ Requires-Dist: pydantic<2,>=1.10
24
25
  Requires-Dist: pypinyin>=0.44.0
25
26
  Requires-Dist: requests>=2.31
26
27
  Requires-Dist: scikit-learn<2,>=1.2
@@ -245,7 +246,11 @@ author_list = ["Zhang Wei", "Li Ming", "Wang Xiaoli", "Liu Jiaming", "Feng Cha"]
245
246
  batch_result = detector.analyze_name_batch(author_list)
246
247
  print(f"Format detected: {batch_result.format_pattern.dominant_format}")
247
248
  print(f"Confidence: {batch_result.format_pattern.confidence:.1%}")
248
- # 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%
249
254
 
250
255
  for i, result in enumerate(batch_result.results):
251
256
  if result.success:
@@ -257,7 +262,7 @@ unknown_format_list = ["Wei Zhang", "Ming Li", "Xiaoli Wang"]
257
262
  pattern = detector.detect_batch_format(unknown_format_list)
258
263
  if pattern.threshold_met:
259
264
  print(f"Consistent {pattern.dominant_format} formatting detected")
260
- 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")
261
266
  else:
262
267
  print("Mixed formatting detected - process individually")
263
268
 
@@ -281,7 +286,9 @@ When you call `normalize_name`, you get a `ParseResult` with helpful structured
281
286
  - `surname_tokens`, `given_tokens`: normalized, capitalized tokens used to form components
282
287
  - `middle_tokens`: trailing single-letter initials extracted from given name, if present
283
288
  - `order`: component order descriptor, typically `["given", "middle", "surname"]`
284
- - `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.
285
292
 
286
293
  Notes:
287
294
  - The tokens in `parsed` and `parsed_original_order` are the same normalized tokens; only the conceptual ordering differs via the `order` list.
@@ -294,8 +301,8 @@ res = detector.normalize_name("Li Wei")
294
301
  # res.result == "Wei Li"
295
302
  # res.parsed.order == ["given", "middle", "surname"]
296
303
  # res.parsed_original_order.order == ["surname", "given"]
297
- # res.parsed_original_order.given_name == "Li" # first in input
298
- # res.parsed_original_order.surname == "Wei" # last in input
304
+ # res.parsed_original_order.given_name == "Wei"
305
+ # res.parsed_original_order.surname == "Li"
299
306
 
300
307
  res = detector.normalize_name("Chi-Ying F. Huang")
301
308
  # res.result == "Chi-Ying F Huang"
@@ -317,7 +324,7 @@ When processing multiple names together, Sinonym:
317
324
 
318
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
319
326
  2. **Aggregates Evidence**: Uses frequency statistics across all names to build confidence in the detected pattern
320
- 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
321
328
  4. **Tracks Improvements**: Identifies which names benefit from batch context vs. individual processing
322
329
 
323
330
  ### Key Benefits
@@ -340,7 +347,10 @@ result = detector.analyze_name_batch([
340
347
  ])
341
348
  print(f"Format detected: {result.format_pattern.dominant_format}")
342
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%}")
343
352
  print(f"Improved names: {len(result.improvements)}")
353
+ print(result.name_order_evidence[0].selected_surname_position)
344
354
 
345
355
  # Quick format detection without full processing
346
356
  pattern = detector.detect_batch_format([
@@ -357,6 +367,37 @@ for result in results:
357
367
  print(f"Processed: {result.result}")
358
368
  ```
359
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
+
360
401
  ### Persistent Multi-Process Processing
361
402
 
362
403
  For high-throughput workloads, you can keep a persistent process pool alive and
@@ -376,7 +417,7 @@ def main():
376
417
  results_a = pool.normalize_names(names_a)
377
418
  results_b = pool.normalize_names(names_b)
378
419
 
379
- # One-off convenience wrapper (creates and closes a temporary pool)
420
+ # One-off compatibility wrapper with full batch-context semantics
380
421
  single_batch = detector.process_name_batch_multiprocess(names_a, max_workers=6, chunk_size=64)
381
422
  return results_a, results_b, single_batch
382
423
 
@@ -399,7 +440,9 @@ Batch processing requires a minimum of 2 names and works best with 5+ names for
399
440
 
400
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.
401
442
 
402
- **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.
403
446
 
404
447
  ### Batch Processing with Mixed Name Types
405
448
 
@@ -422,7 +465,7 @@ mixed_names = [
422
465
 
423
466
  result = detector.analyze_name_batch(mixed_names)
424
467
 
425
- # Format detection uses only the 8 Chinese names
468
+ # Format detection uses only the 8 Latin-only Chinese names
426
469
  # If 7 prefer GIVEN_FIRST vs 1 SURNAME_FIRST = 87.5% confidence
427
470
  # GIVEN_FIRST pattern is applied to Chinese names; non‑Chinese names return clear failures
428
471
 
@@ -448,8 +491,8 @@ for i, (name, result_obj) in enumerate(zip(mixed_names, result.results)):
448
491
 
449
492
  **Key Benefits:**
450
493
  - **Maintains input-output correspondence**: Results array matches input array length and order
451
- - **Robust format detection**: Only valid Chinese names contribute to pattern detection
452
- - **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
453
496
  - **Clear failure reporting**: Non-Chinese names are clearly marked as failed with error messages
454
497
 
455
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.5"
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"
@@ -27,6 +27,7 @@ keywords = ["chinese", "names", "nlp", "romanization", "pinyin"]
27
27
  dependencies = [
28
28
  "joblib>=1.3",
29
29
  "numpy>=1.24",
30
+ "pydantic>=1.10,<2",
30
31
  "pypinyin>=0.44.0",
31
32
  "requests>=2.31",
32
33
  "scikit-learn>=1.2,<2",
@@ -60,6 +61,8 @@ include = [
60
61
  include = [
61
62
  "sinonym",
62
63
  "tests",
64
+ "scripts/check_test_status.py",
65
+ "scripts/name_order_routing_rules.py",
63
66
  "README.md",
64
67
  "pyproject.toml",
65
68
  ]
@@ -79,7 +82,9 @@ select = ["E", "F", "W", "C90", "I", "N", "UP", "YTT", "S", "BLE", "FBT", "B", "
79
82
  ignore = ["S101", "T201"] # Allow assert statements and print statements
80
83
 
81
84
  [tool.ruff.lint.per-file-ignores]
82
- "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"]
83
88
 
84
89
  [tool.mypy]
85
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.