Context
PR #207 (#201 Phase 2b) added a Layer-C smoke test test_bpsd_round_trip that drives:
tot_init() # tot_api: g_initialized = True; eq's COMMON via tr_api_init -> CALL eq_init
eq_init() # eq_api: g_initialized = True ← REQUIRED, separate flag
eq_set_param("MODELG", 3.0)
eq_set_param_str("KNAMEQ", "eqdata-HT6M")
eq_run(1) # eq_load -> eq_bpsd_put
tr_check_bpsd_pull(ok) # ok = 1
The eq_init() call is required because eq_api maintains its own g_initialized flag (eq/eq_api.f90:70) separate from tot_api's (tot/tot_api.f90:88). Without the explicit eq_init(), eq_set_param/eq_run return EQ_ERR_NOT_INIT (2).
tot_api_init calls equnit::eq_init (the Fortran subroutine for COMMON setup) via tr_api_init's CALL eq_init, but it does NOT call eq_api_init (the C ABI wrapper that flips eq_api's g_initialized).
Why this is a foot-gun
A user driving the mono image through tot's C ABI is reasonably expected to think tot_init brings up everything — but eq-side calls via the C ABI still need their own eq_init. Discoverability of this is poor:
- Public docs say
tot_init brings up all sub-modules.
tot_api_init does call equnit's eq_init internally (for COMMON state).
- The C ABI export
eq_api_init is a separate function, internally idempotent but its g_initialized flag is independent.
For Phase 2c, when users actually use mono in production via TotPipeline, this dual-init contract becomes a footgun:
- A
TotPipeline.run_pipeline([("eq",...),("tr",...)]) would call wrappers, each of which calls its own _api_init. The mono routing PR (#NNN) makes them all load the same .so. Should each wrapper still flip its private g_initialized? Probably yes — that's the wrapper's contract.
- BUT a user writing direct ctypes code against the mono image (skipping TotPipeline) will hit this footgun.
Fix options
Option A: tot_api_init ALSO calls eq_api_init() after the existing tr_api_init(). Trivial 5-line change. Per-module g_initialized flags get flipped through the tot lifecycle. Subsequent direct calls to eq_set_param etc. don't need an extra eq_init() call.
Pros: cleanest user surface; mono direct-ctypes use case "just works"
Cons: deviates from current "tot_init initializes the COMMON state only, leaves C ABI flags untouched" pattern (if there is one)
Option B: Make eq_api's g_initialized track equnit_eq_init calls instead of being independent. I.e. any caller of equnit_eq_init flips eq_api's flag too. Requires a hook in equnit.f90.
Pros: less duplication of init code paths
Cons: invasive to existing Fortran (boundary concern per feedback_fortran_refactor_needs_lead_signoff.md)
Option C: Document the dual-init contract loudly in tot/tot_api.h and python/totlib/README.md. No code change.
Pros: zero risk
Cons: doesn't fix the footgun, just labels it
Recommendation
Option A is the right scope. Confirm with ats-fukuyama if uncertain, but adding a CALL eq_api_init() inside tot_api_init's init chain is additive and within the "k-yoshimi-led C ABI extension" boundary.
Effort
~30 min including local + CI verification.
References
Context
PR #207 (#201 Phase 2b) added a Layer-C smoke test
test_bpsd_round_tripthat drives:The
eq_init()call is required becauseeq_apimaintains its owng_initializedflag (eq/eq_api.f90:70) separate fromtot_api's (tot/tot_api.f90:88). Without the expliciteq_init(),eq_set_param/eq_runreturnEQ_ERR_NOT_INIT (2).tot_api_initcallsequnit::eq_init(the Fortran subroutine for COMMON setup) viatr_api_init'sCALL eq_init, but it does NOT calleq_api_init(the C ABI wrapper that flipseq_api'sg_initialized).Why this is a foot-gun
A user driving the mono image through tot's C ABI is reasonably expected to think
tot_initbrings up everything — but eq-side calls via the C ABI still need their owneq_init. Discoverability of this is poor:tot_initbrings up all sub-modules.tot_api_initdoes call equnit's eq_init internally (for COMMON state).eq_api_initis a separate function, internally idempotent but itsg_initializedflag is independent.For Phase 2c, when users actually use mono in production via
TotPipeline, this dual-init contract becomes a footgun:TotPipeline.run_pipeline([("eq",...),("tr",...)])would call wrappers, each of which calls its own_api_init. The mono routing PR (#NNN) makes them all load the same.so. Should each wrapper still flip its private g_initialized? Probably yes — that's the wrapper's contract.Fix options
Option A:
tot_api_initALSO callseq_api_init()after the existingtr_api_init(). Trivial 5-line change. Per-moduleg_initializedflags get flipped through the tot lifecycle. Subsequent direct calls toeq_set_parametc. don't need an extraeq_init()call.Pros: cleanest user surface; mono direct-ctypes use case "just works"
Cons: deviates from current "tot_init initializes the COMMON state only, leaves C ABI flags untouched" pattern (if there is one)
Option B: Make
eq_api'sg_initializedtrackequnit_eq_initcalls instead of being independent. I.e. any caller ofequnit_eq_initflips eq_api's flag too. Requires a hook in equnit.f90.Pros: less duplication of init code paths
Cons: invasive to existing Fortran (boundary concern per
feedback_fortran_refactor_needs_lead_signoff.md)Option C: Document the dual-init contract loudly in
tot/tot_api.handpython/totlib/README.md. No code change.Pros: zero risk
Cons: doesn't fix the footgun, just labels it
Recommendation
Option A is the right scope. Confirm with ats-fukuyama if uncertain, but adding a
CALL eq_api_init()insidetot_api_init's init chain is additive and within the "k-yoshimi-led C ABI extension" boundary.Effort
~30 min including local + CI verification.
References
docs/superpowers/specs/2026-05-18-l7b-ii-phase-2b-design.md§6 (Layer-C test design + dual-flag rationale)