factorforge-cds 3.2.6__tar.gz → 3.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 (100) hide show
  1. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/PKG-INFO +2 -2
  2. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/README.md +1 -1
  3. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/pyproject.toml +1 -1
  4. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/__init__.py +1 -1
  5. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/analysis/feasibility.py +5 -0
  6. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/analysis/metrics.py +4 -1
  7. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/engines/__init__.py +1 -1
  8. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/engines/profile/__init__.py +1 -1
  9. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/engines/profile/optimizer.py +7 -3
  10. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/engines/profile/pipeline.py +3 -1
  11. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/engines/profile/rules/reverse_translator.py +28 -12
  12. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/engines/profile/rules/rule_engine.py +8 -4
  13. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/engines/profile/scoring.py +104 -11
  14. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/engines/profile/utils.py +20 -2
  15. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge_cds.egg-info/PKG-INFO +2 -2
  16. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_benchmark_codon_table_metadata.py +32 -1
  17. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_registry_production_sync.py +76 -0
  18. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_worked_example.py +33 -1
  19. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/LICENSE +0 -0
  20. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/setup.cfg +0 -0
  21. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/__main__.py +0 -0
  22. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/analysis/__init__.py +0 -0
  23. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/cli/__init__.py +0 -0
  24. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/cli/legacy_cli.py +0 -0
  25. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/cli/main.py +0 -0
  26. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/core/interfaces/__init__.py +0 -0
  27. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/core/interfaces/exporter.py +0 -0
  28. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/core/interfaces/optimizer.py +0 -0
  29. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/core/interfaces/validator.py +0 -0
  30. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/data/nbenthamiana_codons.json +0 -0
  31. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/data/nbenthamiana_golden_set.json +0 -0
  32. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/data/ntabacum_codons.json +0 -0
  33. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/data/templates/high_expression.json +0 -0
  34. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/data/templates/standard_expression.json +0 -0
  35. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/data/wolffia_globosa_codons.json +0 -0
  36. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/database.py +0 -0
  37. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/engines/profile/codon_table_builder.py +0 -0
  38. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/engines/profile/construct_builder.py +0 -0
  39. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/engines/profile/exporter.py +0 -0
  40. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/engines/profile/rules/__init__.py +0 -0
  41. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/engines/profile/rules/domesticator.py +0 -0
  42. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/engines/profile/scoring_ml.py +0 -0
  43. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/engines/profile/validator.py +0 -0
  44. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/engines/registry.py +0 -0
  45. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/io/__init__.py +0 -0
  46. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/io/fasta.py +0 -0
  47. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/io/validation.py +0 -0
  48. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/protein_risk/__init__.py +0 -0
  49. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/protein_risk/annotate.py +0 -0
  50. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/protein_risk/kd_scale.py +0 -0
  51. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/protein_risk/risk_classifier.py +0 -0
  52. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/protein_risk/sp_predict.py +0 -0
  53. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/protein_risk/tm_predict.py +0 -0
  54. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/registry/__init__.py +0 -0
  55. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/registry/registry_loader.py +0 -0
  56. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/schemas/__init__.py +0 -0
  57. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/schemas/design_package.py +0 -0
  58. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/schemas/design_package.schema.json +0 -0
  59. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/utils/__init__.py +0 -0
  60. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/utils/construct_id.py +0 -0
  61. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/utils/exceptions.py +0 -0
  62. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/utils/restriction_sites.py +0 -0
  63. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/utils/sequence_validator.py +0 -0
  64. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/utils/validation.py +0 -0
  65. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/validation/__init__.py +0 -0
  66. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/validation/cli.py +0 -0
  67. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/validation/package_generator.py +0 -0
  68. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/validation_registry.py +0 -0
  69. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge/validation_report.py +0 -0
  70. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge_cds.egg-info/SOURCES.txt +0 -0
  71. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge_cds.egg-info/dependency_links.txt +0 -0
  72. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge_cds.egg-info/entry_points.txt +0 -0
  73. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge_cds.egg-info/requires.txt +0 -0
  74. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/src/factorforge_cds.egg-info/top_level.txt +0 -0
  75. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_baselines.py +0 -0
  76. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_benchmark_regression.py +0 -0
  77. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_benchmark_scoring.py +0 -0
  78. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_benchmark_smoke.py +0 -0
  79. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_cai.py +0 -0
  80. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_codon_table_manifest.py +0 -0
  81. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_database.py +0 -0
  82. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_design_package_schema.py +0 -0
  83. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_design_package_semantics.py +0 -0
  84. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_design_package_serialization.py +0 -0
  85. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_docs_consistency.py +0 -0
  86. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_fasta_io.py +0 -0
  87. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_gc_content.py +0 -0
  88. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_host_profile_metadata.py +0 -0
  89. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_iupac_validation.py +0 -0
  90. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_legacy_cli.py +0 -0
  91. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_no_raw_sequence_logging.py +0 -0
  92. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_openbio_missing_metric_contract.py +0 -0
  93. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_parameter_registry.py +0 -0
  94. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_protein_risk.py +0 -0
  95. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_restriction_sites.py +0 -0
  96. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_sequence_validator.py +0 -0
  97. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_translation_integrity.py +0 -0
  98. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_validation_contract_compat.py +0 -0
  99. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_validation_registry.py +0 -0
  100. {factorforge_cds-3.2.6 → factorforge_cds-3.2.8}/tests/test_validation_report.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: factorforge-cds
3
- Version: 3.2.6
3
+ Version: 3.2.8
4
4
  Summary: FactorForge - open-source CDS design and pre-synthesis sequence review engine by Eijex.
5
5
  Author-email: Eijex <eijex.lab@gmail.com>
6
6
  License-Expression: AGPL-3.0-only
@@ -93,7 +93,7 @@ FactorForge outputs are **in-silico only** and have not been experimentally vali
93
93
  ## Citing
94
94
 
95
95
  ```
96
- FactorForge v3.2.6 (2026). Open-source constraint-based CDS design engine.
96
+ FactorForge v3.2.8 (2026). Open-source constraint-based CDS design engine.
97
97
  Eijex. https://github.com/eijex/factorforge-cds
98
98
  ```
99
99
 
@@ -60,7 +60,7 @@ FactorForge outputs are **in-silico only** and have not been experimentally vali
60
60
  ## Citing
61
61
 
62
62
  ```
63
- FactorForge v3.2.6 (2026). Open-source constraint-based CDS design engine.
63
+ FactorForge v3.2.8 (2026). Open-source constraint-based CDS design engine.
64
64
  Eijex. https://github.com/eijex/factorforge-cds
65
65
  ```
66
66
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "factorforge-cds"
7
- version = "3.2.6"
7
+ version = "3.2.8"
8
8
  description = "FactorForge - open-source CDS design and pre-synthesis sequence review engine by Eijex."
9
9
  readme = "README.md"
10
10
  license = "AGPL-3.0-only"
@@ -4,7 +4,7 @@ FactorForge - Codon Optimization Platform
4
4
  profile: constraint-aware rule/profile engine
5
5
  """
6
6
 
7
- __version__ = "3.2.6"
7
+ __version__ = "3.2.8"
8
8
  __author__ = "Eijex"
9
9
 
10
10
  # Auto-register engines (safe when running from source tree)
@@ -19,6 +19,11 @@ from factorforge.analysis.metrics import (
19
19
  # DEFAULT_CAI_TARGET=0.82 aligns with industry practice (>0.8) and is achievable.
20
20
  # Exported as named constants so tests/test_registry_production_sync.py can
21
21
  # strictly compare them against the registry (single source of truth).
22
+ #
23
+ # DEFAULT_GC_LOW/HIGH provisionally reverted from the Job 168/v3.3.0
24
+ # native-genome-composition anchor (40-47%, released as part of v3.2.7) back
25
+ # to the legacy engine-output-calibrated band, pending an MFE re-sensitivity +
26
+ # 2x2 factorial recheck. See scoring.py's GC_OPT_MIN/MAX comment.
22
27
  DEFAULT_CAI_TARGET: float = 0.82
23
28
  DEFAULT_GC_LOW: float = 55.0
24
29
  DEFAULT_GC_HIGH: float = 65.0
@@ -98,7 +98,10 @@ class CodonUsageTable:
98
98
 
99
99
 
100
100
  def _default_codon_table_path() -> Path:
101
- return get_data_path() / "nbenthamiana_codons.json"
101
+ # Job 168 / v3.3.0 (_analysis/025): production default switched from the
102
+ # legacy Kazusa/SGN-derived table to the NbeV1.1 LAB-strain high-confidence
103
+ # derived table. See data/reference/active_codon_reference.json.
104
+ return get_data_path() / "profiles" / "nbev11_cds_hc_derived_codons.json"
102
105
 
103
106
 
104
107
  def load_codon_usage_table(path: Path | None = None) -> CodonUsageTable:
@@ -13,7 +13,7 @@ def register_builtin_engines() -> None:
13
13
  "profile",
14
14
  RuleBasedOptimizer,
15
15
  metadata={
16
- "version": "3.2.6",
16
+ "version": "3.2.8",
17
17
  "engine_type": "profile_rule_based",
18
18
  "role": "stable_profile_engine",
19
19
  "stable": True,
@@ -5,7 +5,7 @@ Production system (2026)
5
5
  Plant-specific rule-based optimization
6
6
  """
7
7
 
8
- __version__ = "3.2.6"
8
+ __version__ = "3.2.8"
9
9
 
10
10
  from .optimizer import RuleBasedOptimizer
11
11
  from .pipeline import OptimizationPipeline
@@ -20,7 +20,7 @@ class RuleBasedOptimizer(OptimizerEngine):
20
20
  """Profile-based rule optimization engine."""
21
21
 
22
22
  name = "Profile-based"
23
- version = "3.2.6"
23
+ version = "3.2.8"
24
24
 
25
25
  def __init__(self, codon_table_path: str | None = None) -> None:
26
26
  """
@@ -122,12 +122,16 @@ class RuleBasedOptimizer(OptimizerEngine):
122
122
  cai = translator.calculate_cai(optimized_dna)
123
123
  gc = translator.calculate_gc_content(optimized_dna)
124
124
  score = calculate_composite_score(
125
- cai=cai, gc=gc, sequence=optimized_dna, profile=profile_value
125
+ cai=cai, gc=gc, sequence=optimized_dna, profile=profile_value, host=host
126
126
  )
127
127
  candidates = [{"sequence": optimized_dna, "cai": cai, "gc": gc, "score": score}]
128
128
  else:
129
+ translate_kwargs = {
130
+ k: v for k, v in kwargs.items()
131
+ if k not in ("scan_mode", "scan_include", "scan_exclude")
132
+ }
129
133
  candidates = translator.generate_candidates(
130
- processed_seq, profile=opt_profile, n=1, seed=seed
134
+ processed_seq, profile=opt_profile, n=1, seed=seed, **translate_kwargs
131
135
  )
132
136
  if not candidates:
133
137
  raise ValueError("No candidates generated for input sequence.")
@@ -192,7 +192,8 @@ class OptimizationPipeline:
192
192
  cai = translator.calculate_cai(optimized_dna)
193
193
  gc = translator.calculate_gc_content(optimized_dna)
194
194
  score = calculate_composite_score(
195
- cai=cai, gc=gc, sequence=optimized_dna, profile=effective_profile
195
+ cai=cai, gc=gc, sequence=optimized_dna, profile=effective_profile,
196
+ host=effective_host,
196
197
  )
197
198
  candidate_metrics = {"cai": cai, "gc": gc, "score": score}
198
199
  else:
@@ -244,6 +245,7 @@ class OptimizationPipeline:
244
245
  gc=candidate_metrics["gc"],
245
246
  sequence=optimized_dna,
246
247
  profile=effective_profile,
248
+ host=effective_host,
247
249
  )
248
250
  logger.info(
249
251
  f"Dinucleotide reduction [{dinu_fix['mode']}]: "
@@ -15,12 +15,16 @@ from enum import Enum
15
15
  from pathlib import Path
16
16
  from typing import Any, cast
17
17
 
18
- from factorforge.engines.profile.scoring import GC_OPT_MID, calculate_composite_score
18
+ from factorforge.engines.profile.scoring import (
19
+ calculate_composite_score,
20
+ resolve_host_gc_range,
21
+ )
19
22
  from factorforge.engines.profile.utils import (
20
23
  build_aa_to_codons_map,
21
24
  calculate_gc,
22
25
  get_data_path,
23
26
  load_golden_set,
27
+ resolve_host_codon_table_path,
24
28
  )
25
29
  from factorforge.utils.exceptions import EmptyCandidateError
26
30
 
@@ -66,7 +70,7 @@ class ReverseTranslator:
66
70
  if codon_table_path is None:
67
71
  # Use centralized data path management
68
72
  data_dir = get_data_path()
69
- codon_table_path = data_dir / f"{host}_codons.json"
73
+ codon_table_path = resolve_host_codon_table_path(host, data_dir)
70
74
 
71
75
  self.codon_table: dict[str, Any] = self._load_codon_table(codon_table_path)
72
76
  self.aa_to_codons: dict[str, list[tuple[str, float]]] = self._build_aa_to_codons_map()
@@ -370,10 +374,16 @@ class ReverseTranslator:
370
374
  Balanced profile: CAI first, GC balanced
371
375
 
372
376
  - Preferred codon ratio: 70%
373
- - Target GC: 55-65% (internal benchmark: avg output 60.1%)
377
+ - Target GC: host composition band, resolved via
378
+ resolve_host_gc_range() (engines/profile/scoring.py
379
+ GC_OPT_MIN/MAX). nbenthamiana currently uses the legacy 55-65%
380
+ band; the native genome-composition anchor (40-47%, _analysis/025)
381
+ was provisionally reverted on 2026-06-29 pending an MFE
382
+ re-sensitivity + 2x2 factorial recheck.
374
383
  """
375
- target_gc_min = kwargs.get("target_gc_min", 55)
376
- target_gc_max = kwargs.get("target_gc_max", 65)
384
+ _host_gc_min, _host_gc_max = resolve_host_gc_range(self.host)
385
+ target_gc_min = kwargs.get("target_gc_min", _host_gc_min)
386
+ target_gc_max = kwargs.get("target_gc_max", _host_gc_max)
377
387
  preferred_ratio = kwargs.get("preferred_ratio", 0.7)
378
388
  max_attempts = kwargs.get("max_gc_attempts", 10)
379
389
  if max_attempts < 1:
@@ -443,19 +453,19 @@ class ReverseTranslator:
443
453
  """GC-Target profile: drive global GC toward a configurable target.
444
454
 
445
455
  Targets the caller-supplied ``target_gc`` if provided, otherwise the
446
- host-profile GC midpoint (GC_OPT_MID = 60% for N. benthamiana). To target
447
- a lower GC (e.g. for specific vector requirements), pass target_gc explicitly.
456
+ active host's composition midpoint (resolve_host_gc_range(self.host);
457
+ 43.5% for nbenthamiana since v3.3.0 / _analysis/025, 60.0% for other
458
+ hosts pending their own host-specific analysis). To target a different
459
+ GC (e.g. for specific vector requirements), pass target_gc explicitly.
448
460
 
449
461
  - GC constraint first
450
462
  - CAI may be sacrificed
451
463
  - Balance local window GC (50 bp)
452
-
453
- TODO: GC_OPT_MID is currently a single N. benthamiana-calibrated constant.
454
- When per-host GC profiles are added, source the default from the active host.
455
464
  """
456
465
  target_gc = kwargs.get("target_gc")
457
466
  if target_gc is None:
458
- target_gc = GC_OPT_MID
467
+ _host_gc_min, _host_gc_max = resolve_host_gc_range(self.host)
468
+ target_gc = (_host_gc_min + _host_gc_max) / 2
459
469
 
460
470
  dna_seq: list[str] = []
461
471
 
@@ -507,9 +517,14 @@ class ReverseTranslator:
507
517
  raise ValueError("max_attempts must be >= 1")
508
518
  last_seq = ""
509
519
 
520
+ balanced_kwargs = {
521
+ k: v for k, v in kwargs.items() if k in ("target_gc_min", "target_gc_max", "max_gc_attempts")
522
+ }
523
+ balanced_kwargs["preferred_ratio"] = 0.6
524
+
510
525
  for attempt in range(max_attempts):
511
526
  # Start with Balanced strategy
512
- dna_seq = self._balanced_translate(protein_seq, preferred_ratio=0.6)
527
+ dna_seq = self._balanced_translate(protein_seq, **balanced_kwargs)
513
528
  last_seq = dna_seq
514
529
 
515
530
  # Check restriction sites (forward + reverse complement)
@@ -710,6 +725,7 @@ class ReverseTranslator:
710
725
  gc=gc,
711
726
  sequence=dna_seq,
712
727
  profile=profile.value,
728
+ host=self.host,
713
729
  **kwargs,
714
730
  )
715
731
  return {"sequence": dna_seq, "cai": cai, "gc": gc, "score": score}
@@ -358,10 +358,14 @@ class RuleEngine:
358
358
 
359
359
  This is a LOCAL synthesis/extreme-window guard (default 25-75% over a
360
360
  50 bp window), NOT the global GC target. Global GC is governed separately
361
- by the scoring band (GC_OPT_MIN/MAX, ~55-65%) and the API/DP gc_min/gc_max
362
- constraints. The wide 25-75% band intentionally flags only synthesis-hostile
363
- local windows; narrowing it toward the global optimum would raise false
364
- positives against the engine's own output distribution (internal benchmark: 55-71%).
361
+ by the scoring band (GC_OPT_MIN/MAX, currently 55-65% for nbenthamiana
362
+ — the native genome-composition anchor, ~40-47%, _analysis/025, was
363
+ provisionally reverted on 2026-06-29 pending an MFE re-sensitivity +
364
+ 2x2 factorial recheck) and the API/DP gc_min/gc_max constraints. The
365
+ wide 25-75% local band is an independent synthesis/hairpin-risk guard
366
+ (registry `gc_extreme_local_window`), not derived from or coupled to
367
+ the global GC target — narrowing it is a separate decision from the
368
+ global band retarget and is out of scope here.
365
369
 
366
370
  Args:
367
371
  seq: DNA sequence
@@ -17,11 +17,42 @@ logger = logging.getLogger(__name__)
17
17
  # high-expression codon table which exhibits 3rd-position GC bias.
18
18
  # These constants define the acceptable band — sequences within [GC_OPT_MIN, GC_OPT_MAX]
19
19
  # receive full GC score; outside the band the score decays linearly.
20
+ #
21
+ # Job 168/v3.3.0 (released as part of v3.2.7, see CHANGELOG) moved this band to
22
+ # 40-47% (native genome-composition anchor, _analysis/025). Provisionally
23
+ # reverted here pending an MFE re-sensitivity + 2x2 factorial recheck
24
+ # (_analysis, scope TBD as of 2026-06-29) — see eijex-workspace
25
+ # _version/factorforge-version-sequencing-plan.md. Not a rejection of the
26
+ # 40-47% anchor, just not yet re-confirmed as the production default.
20
27
  GC_OPT_MIN = 55.0
21
28
  GC_OPT_MAX = 65.0
22
29
  GC_OPT_MID = 60.0 # kept for gc_target point-scoring and viral_delivery centering
23
30
  GC_DECAY_WIDTH = 20.0 # percentage points outside band before score reaches 0.0
24
31
 
32
+ # Host-isolation fix (Job 168/v3.3.0, kept): ntabacum/BY-2 must not silently
33
+ # inherit whatever band nbenthamiana uses. Both currently resolve to the same
34
+ # GC_RANGE_DEFAULT band pending the nbenthamiana re-check above.
35
+ GC_RANGE_DEFAULT: tuple[float, float] = (55.0, 65.0)
36
+ GC_RANGES_BY_HOST: dict[str, tuple[float, float]] = {
37
+ "nbenthamiana": (GC_OPT_MIN, GC_OPT_MAX),
38
+ }
39
+
40
+
41
+ def resolve_host_gc_range(host: str | None) -> tuple[float, float]:
42
+ """Resolve the (gc_min, gc_max) composition band for a host.
43
+
44
+ nbenthamiana uses the _analysis/025 native genome-composition anchor.
45
+ Any other host (including unknown ones) keeps the pre-v3.3.0 global
46
+ default band until it gets its own host-specific analysis.
47
+ """
48
+ return GC_RANGES_BY_HOST.get(host or "nbenthamiana", GC_RANGE_DEFAULT)
49
+
50
+
51
+ def resolve_host_gc_mid(host: str | None) -> float:
52
+ """Resolve the GC band midpoint for a host (see resolve_host_gc_range)."""
53
+ gc_min, gc_max = resolve_host_gc_range(host)
54
+ return (gc_min + gc_max) / 2
55
+
25
56
  # ViennaRNA availability cache
26
57
  _vienna_available: bool | None = None
27
58
 
@@ -45,7 +76,12 @@ class ScoringConfig:
45
76
  use_mfe: bool = True
46
77
 
47
78
  def __post_init__(self) -> None:
48
- """Normalize weights to sum to 1.0."""
79
+ """Validate inputs, then normalize weights to sum to 1.0."""
80
+ for field_name in ("w_cai", "w_gc", "w_mfe", "w_dinuc", "w_syncodonlm"):
81
+ if getattr(self, field_name) < 0:
82
+ raise ValueError(f"{field_name} must be >= 0, got {getattr(self, field_name)}")
83
+ if self.gc_min > self.gc_max:
84
+ raise ValueError(f"gc_min ({self.gc_min}) must be <= gc_max ({self.gc_max})")
49
85
  self._normalize()
50
86
 
51
87
  def _normalize(self) -> None:
@@ -110,6 +146,20 @@ def _check_vienna_available() -> bool:
110
146
  return _vienna_available
111
147
 
112
148
 
149
+ # 170-fix: ViennaRNA's RNA.fold() uses Zuker's MFE algorithm, O(n^3) time /
150
+ # O(n^2) memory — there was previously no length guard anywhere in the
151
+ # calculate_mfe() call chain, so a single request at/under the existing
152
+ # public API length limits (5000aa/15000bp) could pin a CPU core for minutes
153
+ # (algorithmic-complexity DoS, CWE-407; confirmed via faulthandler stack
154
+ # traces + an isolated RNA.fold() timing curve: 1000nt ~2.2s, 2000nt ~9.8s,
155
+ # 3000nt ~24.6s). calculate_mfe() is called twice per optimize() call
156
+ # (once for scoring, once independently for compute_mfe_evidence()
157
+ # provenance), so the real per-request cost is ~2x this curve. 1000nt
158
+ # (~333aa) keeps worst-case cost to roughly 4-5s even with that doubling,
159
+ # while still covering most realistic single-protein CDS design requests.
160
+ MFE_MAX_SEQUENCE_LENGTH = 1000
161
+
162
+
113
163
  def calculate_mfe(sequence: str) -> float | None:
114
164
  """
115
165
  Calculate minimum free energy (MFE) using ViennaRNA.
@@ -118,11 +168,22 @@ def calculate_mfe(sequence: str) -> float | None:
118
168
  sequence: DNA or RNA sequence.
119
169
 
120
170
  Returns:
121
- MFE in kcal/mol, or None if ViennaRNA is not available.
171
+ MFE in kcal/mol, or None if ViennaRNA is not available or the
172
+ sequence exceeds MFE_MAX_SEQUENCE_LENGTH.
122
173
  """
123
174
  if not _check_vienna_available():
124
175
  return None
125
176
 
177
+ if len(sequence) > MFE_MAX_SEQUENCE_LENGTH:
178
+ logger.warning(
179
+ "Sequence length (%d nt) exceeds MFE_MAX_SEQUENCE_LENGTH (%d nt); "
180
+ "skipping global MFE calculation to avoid an unbounded ViennaRNA "
181
+ "RNA.fold() runtime (O(n^3)). MFE scoring falls back to a neutral, "
182
+ "zero-weighted contribution for this candidate (170-fix).",
183
+ len(sequence), MFE_MAX_SEQUENCE_LENGTH,
184
+ )
185
+ return None
186
+
126
187
  try:
127
188
  import RNA
128
189
 
@@ -139,24 +200,34 @@ def normalize_mfe(mfe: float, seq_length: int) -> float:
139
200
  """
140
201
  Normalize MFE to 0-1 range where 1 = no structure (favorable).
141
202
 
142
- Uses empirical scaling: MFE per nucleotide typically ranges from
143
- -0.5 to 0.0 kcal/mol/nt for mRNA coding sequences.
203
+ Clamp range calibrated empirically in analysis 011
204
+ (eijex-workspace/_analysis/2026-06-26/011-mfe-clamp-calibration):
205
+ measured MFE/nt across 135 FactorForge outputs (N. benthamiana +
206
+ BY-2 hosts, 5 profiles) ranged -0.4064 to -0.1338 (combined 5th/95th
207
+ percentile -0.3839/-0.1760). The range below widens that empirical
208
+ percentile by a margin on both sides to avoid saturating ~10% of
209
+ future sequences at the score floor/ceiling.
144
210
 
145
211
  Args:
146
212
  mfe: Minimum free energy in kcal/mol.
147
213
  seq_length: Sequence length in nucleotides.
148
214
 
149
215
  Returns:
150
- Normalized MFE score (0-1, higher = less structured = better for translation).
216
+ Normalized MFE score (0-1) under this computational normalization,
217
+ where a higher value represents a less negative whole-CDS MFE/nt.
218
+ This is a Tier-0 computational heuristic only (see
219
+ eijex-validationHub/docs/CLAIM_EVIDENCE_BENCHMARK_MODEL.md §4) — no
220
+ biological interpretation (translation efficiency, mRNA stability,
221
+ or expression outcome) is implied or validated.
151
222
  """
152
223
  if seq_length == 0:
153
224
  return 0.5
154
225
 
155
226
  mfe_per_nt = mfe / seq_length
156
- # Clamp to expected range [-0.5, 0.0]
157
- clamped = max(-0.5, min(0.0, mfe_per_nt))
158
- # Map to [0, 1] where 0.0 kcal/mol/nt → 1.0 and -0.5 → 0.0
159
- return 1.0 + (clamped / 0.5)
227
+ # Clamp to empirically calibrated range [-0.40, -0.15] (analysis 011)
228
+ clamped = max(-0.40, min(-0.15, mfe_per_nt))
229
+ # Map to [0, 1] where -0.15 kcal/mol/nt → 1.0 and -0.40 → 0.0
230
+ return (clamped + 0.40) / 0.25
160
231
 
161
232
 
162
233
  def gc_band_score(
@@ -210,6 +281,7 @@ def calculate_composite_score(
210
281
  sequence: str | None = None,
211
282
  config: ScoringConfig | None = None,
212
283
  profile: str | None = None,
284
+ host: str | None = None,
213
285
  **kwargs: Any,
214
286
  ) -> float:
215
287
  """Calculate multidimensional composite score.
@@ -238,7 +310,12 @@ def calculate_composite_score(
238
310
  profile_name = (profile or "balanced").lower()
239
311
  config = PROFILE_SCORING_CONFIGS.get(profile_name)
240
312
  if config is None:
241
- config = PROFILE_SCORING_CONFIGS["balanced"]
313
+ supported = ", ".join(sorted(PROFILE_SCORING_CONFIGS))
314
+ raise ValueError(
315
+ f"Unknown profile: {profile_name}. Supported profiles: {supported}"
316
+ )
317
+ if "target_gc" in kwargs and profile_name != "gc_target":
318
+ raise ValueError("target_gc is only valid for the gc_target profile")
242
319
 
243
320
  # Component 1: CAI (already 0-1)
244
321
  cai_score = max(0.0, min(1.0, cai))
@@ -250,7 +327,15 @@ def calculate_composite_score(
250
327
  tgt = float(kwargs["target_gc"])
251
328
  gc_score = gc_band_score(gc, tgt - 5.0, tgt + 5.0, config.gc_decay_width)
252
329
  else:
253
- gc_score = gc_band_score(gc, config.gc_min, config.gc_max, config.gc_decay_width)
330
+ gc_min, gc_max = config.gc_min, config.gc_max
331
+ # Profiles that haven't customized the band (still on the
332
+ # GC_OPT_MIN/MAX default) follow the active host's composition band.
333
+ # Profiles with an explicit band override (e.g. viral_delivery) keep
334
+ # it regardless of host — that override is profile-specific, not a
335
+ # host default.
336
+ if (gc_min, gc_max) == (GC_OPT_MIN, GC_OPT_MAX):
337
+ gc_min, gc_max = resolve_host_gc_range(host)
338
+ gc_score = gc_band_score(gc, gc_min, gc_max, config.gc_decay_width)
254
339
 
255
340
  # Component 3: MFE (optional)
256
341
  mfe_score = 0.5 # neutral default
@@ -336,6 +421,14 @@ def compute_mfe_evidence(
336
421
  reason = "MFE was not computed because no sequence was provided."
337
422
  elif not _check_vienna_available():
338
423
  reason = "MFE was not computed because ViennaRNA is unavailable in this environment."
424
+ elif len(sequence) > MFE_MAX_SEQUENCE_LENGTH:
425
+ # 170-fix: distinguish a deliberate length-based skip from an actual
426
+ # fold failure — the generic "computation failed" message below would
427
+ # otherwise mislead a caller into thinking something is broken.
428
+ reason = (
429
+ f"MFE was skipped because the sequence ({len(sequence)} nt) exceeds "
430
+ f"the {MFE_MAX_SEQUENCE_LENGTH} nt limit for global MFE folding."
431
+ )
339
432
  else:
340
433
  mfe_value = calculate_mfe(sequence)
341
434
  if mfe_value is None:
@@ -115,6 +115,22 @@ def calculate_dinucleotide_ratio(sequence: str, dinucleotide: str = "CG") -> flo
115
115
  return observed / expected
116
116
 
117
117
 
118
+ # Job 168 / v3.3.0 (_analysis/025) introduced a host -> production-default
119
+ # codon table file override mechanism and pointed nbenthamiana at the NbeV1.1
120
+ # LAB-strain derived table (released as part of v3.2.7). Provisionally
121
+ # reverted to empty (falls back to the legacy {host}_codons.json convention)
122
+ # pending an MFE re-sensitivity + 2x2 factorial recheck. The NbeV1.1 table
123
+ # remains on disk and selectable; see data/reference/active_codon_reference.json.
124
+ _HOST_CODON_TABLE_OVERRIDES: dict[str, str] = {}
125
+
126
+
127
+ def resolve_host_codon_table_path(host: str, codon_tables_dir: Path) -> Path:
128
+ """Resolve the production-default codon table file path for a host."""
129
+ override = _HOST_CODON_TABLE_OVERRIDES.get(host)
130
+ filename = override or f"{host}_codons.json"
131
+ return codon_tables_dir / filename
132
+
133
+
118
134
  def load_codon_table(organism: str, codon_tables_dir: Path) -> dict[str, Any]:
119
135
  """Load codon usage table for organism.
120
136
 
@@ -128,8 +144,10 @@ def load_codon_table(organism: str, codon_tables_dir: Path) -> dict[str, Any]:
128
144
  Raises:
129
145
  FileNotFoundError: If codon table file not found.
130
146
  """
131
- filename = organism if organism.endswith(".json") else f"{organism}_codons.json"
132
- codon_table_path = codon_tables_dir / filename
147
+ if organism.endswith(".json"):
148
+ codon_table_path = codon_tables_dir / organism
149
+ else:
150
+ codon_table_path = resolve_host_codon_table_path(organism, codon_tables_dir)
133
151
 
134
152
  with open(codon_table_path, "r", encoding="utf-8") as handle:
135
153
  return cast(dict[str, Any], json.load(handle))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: factorforge-cds
3
- Version: 3.2.6
3
+ Version: 3.2.8
4
4
  Summary: FactorForge - open-source CDS design and pre-synthesis sequence review engine by Eijex.
5
5
  Author-email: Eijex <eijex.lab@gmail.com>
6
6
  License-Expression: AGPL-3.0-only
@@ -93,7 +93,7 @@ FactorForge outputs are **in-silico only** and have not been experimentally vali
93
93
  ## Citing
94
94
 
95
95
  ```
96
- FactorForge v3.2.6 (2026). Open-source constraint-based CDS design engine.
96
+ FactorForge v3.2.8 (2026). Open-source constraint-based CDS design engine.
97
97
  Eijex. https://github.com/eijex/factorforge-cds
98
98
  ```
99
99
 
@@ -110,7 +110,38 @@ def test_smoke_summary_contains_codon_table_fields(tmp_path):
110
110
  ]:
111
111
  assert field in data, f"Missing codon table field in smoke summary JSON: {field}"
112
112
 
113
- assert data["codon_table_id"] == "nbenthamiana_legacy_kazusa_sgn_v101"
113
+ # No --codon-table-path override ⇒ codon_table_id must reflect whichever
114
+ # asset is actually the current production default (Job 168 / v3.3.0,
115
+ # _analysis/025 — see data/reference/active_codon_reference.json), not a
116
+ # hardcoded legacy literal.
117
+ active_ref = json.loads(
118
+ (ROOT / "data" / "reference" / "active_codon_reference.json").read_text(encoding="utf-8")
119
+ )
120
+ assert data["codon_table_id"] == active_ref["active_codon_table_id"]
121
+
122
+
123
+ def test_smoke_summary_contains_vienna_rna_active(tmp_path):
124
+ """Every future run must record whether ViennaRNA (and thus MFE-weighted
125
+ candidate selection) was active, per analysis 011's provenance-gap finding."""
126
+ from benchmarks.run_benchmark import run
127
+
128
+ out_csv = tmp_path / "results.csv"
129
+ out_md = tmp_path / "summary.md"
130
+ run(
131
+ dataset="synthetic",
132
+ mode="smoke",
133
+ out_csv=out_csv,
134
+ out_md=out_md,
135
+ proteins_fasta=ROOT / "tests" / "fixtures" / "small_proteins.fasta",
136
+ native_fasta=ROOT / "tests" / "fixtures" / "small_native_cds.fasta",
137
+ )
138
+ summary_json = tmp_path / "benchmark_summary.json"
139
+ data = json.loads(summary_json.read_text(encoding="utf-8"))
140
+ assert isinstance(data.get("vienna_rna_active"), bool), (
141
+ "vienna_rna_active must be a bool recording whether ViennaRNA was "
142
+ "available (and therefore could affect MFE-weighted candidate "
143
+ "selection) during this benchmark run"
144
+ )
114
145
 
115
146
 
116
147
  # --- raw FASTA / genome files must not be committed ---
@@ -74,3 +74,79 @@ def test_aa_identity_policy_sync():
74
74
  # registry says identity must be 1.0 — passing case confirms
75
75
  ok = validate_cds_output("MKT", "ATGAAAACC")
76
76
  assert ok["passed"] and ok["aa_identity"] == registry_val
77
+
78
+
79
+ # ── codon_reference source-of-truth sync (Job 168 / v3.3.0, _analysis/025) ────
80
+
81
+ def test_codon_reference_active_sync_with_active_reference_file():
82
+ """registry's codon_reference.active block must match
83
+ data/reference/active_codon_reference.json (the file run_benchmark.py
84
+ reads at runtime) — both describe "what is the current production
85
+ default", and must not drift apart."""
86
+ import json
87
+ from pathlib import Path
88
+
89
+ root = Path(__file__).resolve().parents[1]
90
+ registry_active = _resolve("codon_reference.active")
91
+ active_ref = json.loads(
92
+ (root / "data" / "reference" / "active_codon_reference.json").read_text(encoding="utf-8")
93
+ )
94
+ assert registry_active["id"] == active_ref["active_codon_table_id"]
95
+ assert registry_active["asset_type"] == active_ref["active_asset_type"]
96
+ assert registry_active["codon_reference_contract_version"] == active_ref[
97
+ "codon_reference_contract_version"
98
+ ]
99
+ # active_codon_reference.json doesn't carry sha256 directly — cross-check
100
+ # via the file it points at instead.
101
+ import hashlib
102
+ table_path = root / active_ref["file"]
103
+ assert registry_active["sha256"] == hashlib.sha256(table_path.read_bytes()).hexdigest()
104
+
105
+
106
+ def test_codon_reference_active_sync_with_legacy_manifest():
107
+ """registry's codon_reference.active block must match the schema-conformant
108
+ legacy (v1) manifest file's facts (asset_type, sha256, source_status) — v1
109
+ is the production default again as of the v3.2.7 GC-band/codon-reference
110
+ revert, pending an MFE re-sensitivity + 2x2 factorial recheck of v2."""
111
+ import json
112
+ from pathlib import Path
113
+
114
+ registry_active = _resolve("codon_reference.active")
115
+ manifest = json.loads(
116
+ (Path(__file__).resolve().parents[1] / "data" / "reference"
117
+ / "codon_table_manifest.json").read_text(encoding="utf-8")
118
+ )
119
+ assert registry_active["id"] == manifest["codon_table_id"]
120
+ assert registry_active["sha256"] == manifest["sha256"]
121
+ assert registry_active["asset_type"] == manifest["asset_type"]
122
+ assert registry_active["source_status"] == manifest["source_status"]
123
+
124
+
125
+ def test_codon_reference_candidate_sync_with_v2_manifest():
126
+ """registry's codon_reference.candidate block (the provisionally
127
+ un-promoted v2 asset) must stay in sync with its own manifest file even
128
+ while not active."""
129
+ import json
130
+ from pathlib import Path
131
+
132
+ registry_candidate = _resolve("codon_reference.candidate")
133
+ manifest = json.loads(
134
+ (Path(__file__).resolve().parents[1] / "data" / "reference"
135
+ / "codon_table_manifest_nbev11_hc_v2.json").read_text(encoding="utf-8")
136
+ )
137
+ assert registry_candidate["id"] == manifest["codon_table_id"]
138
+ assert registry_candidate["sha256"] == manifest["sha256"]
139
+ assert registry_candidate["asset_type"] == manifest["asset_type"]
140
+ assert registry_candidate["source_status"] == manifest["source_status"]
141
+
142
+
143
+ def test_codon_reference_active_table_sha256_matches_production_default():
144
+ """The sha256 recorded for the active codon_reference must match the
145
+ actual file the production engine resolves to by default."""
146
+ from factorforge.engines.profile.utils import get_data_path, resolve_host_codon_table_path
147
+ import hashlib
148
+
149
+ registry_active = _resolve("codon_reference.active")
150
+ resolved_path = resolve_host_codon_table_path("nbenthamiana", get_data_path())
151
+ actual_sha256 = hashlib.sha256(resolved_path.read_bytes()).hexdigest()
152
+ assert registry_active["sha256"] == actual_sha256
@@ -61,7 +61,14 @@ def test_sfgfp_sequence_no_x_residues():
61
61
  # ---------------------------------------------------------------------------
62
62
 
63
63
  def test_run_example_deterministic():
64
- """run_example.py (no --freeze) must exit 0 when frozen outputs exist."""
64
+ """run_example.py (no --freeze) must exit 0 when frozen outputs exist.
65
+
66
+ Job 168 / v3.3.0 (_analysis/025): this script is explicitly pinned to the
67
+ legacy v1 codon reference (GC 55-65%) regardless of the engine's current
68
+ production default, so this reproducibility check stays valid forever.
69
+ See run_example_v2_smoke.py / test_run_example_v2_smoke_succeeds for the
70
+ current-default (v2) path.
71
+ """
65
72
  result = subprocess.run(
66
73
  [sys.executable, str(EXAMPLE_DIR / "run_example.py")],
67
74
  capture_output=True,
@@ -74,6 +81,31 @@ def test_run_example_deterministic():
74
81
  assert "OK" in result.stdout, "expected 'OK' in output"
75
82
 
76
83
 
84
+ def test_run_example_v2_smoke_succeeds():
85
+ """run_example_v2_smoke.py (tracks whatever the current production
86
+ default actually is) must exit 0 and report a provenance ID that matches
87
+ data/reference/active_codon_reference.json — not hardcoded to v2, since
88
+ the default was provisionally reverted to v1 on 2026-06-29 pending an MFE
89
+ re-sensitivity + 2x2 factorial recheck (Job 168 / v3.3.0, _analysis/025).
90
+
91
+ No frozen-output comparison — this only checks that the current-default
92
+ path runs end-to-end and reports correct provenance.
93
+ """
94
+ result = subprocess.run(
95
+ [sys.executable, str(EXAMPLE_DIR / "run_example_v2_smoke.py")],
96
+ capture_output=True,
97
+ text=True,
98
+ cwd=str(ROOT),
99
+ )
100
+ assert result.returncode == 0, (
101
+ f"run_example_v2_smoke.py exited {result.returncode}\n"
102
+ f"stdout:{result.stdout}\nstderr:{result.stderr}"
103
+ )
104
+ assert "OK" in result.stdout
105
+ active_ref = _load(ROOT / "data" / "reference" / "active_codon_reference.json")
106
+ assert active_ref["active_codon_table_id"] in result.stdout
107
+
108
+
77
109
  # ---------------------------------------------------------------------------
78
110
  # TC3 — design_package.json claim boundary
79
111
  # ---------------------------------------------------------------------------
File without changes