nativegate 0.1.0__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 (101) hide show
  1. nativegate-0.1.0/PKG-INFO +547 -0
  2. nativegate-0.1.0/README.md +514 -0
  3. nativegate-0.1.0/nativegate/__init__.py +1 -0
  4. nativegate-0.1.0/nativegate/__main__.py +4 -0
  5. nativegate-0.1.0/nativegate/buildinfo.py +344 -0
  6. nativegate-0.1.0/nativegate/cli.py +2007 -0
  7. nativegate-0.1.0/nativegate/config.py +991 -0
  8. nativegate-0.1.0/nativegate/declared_invariants.py +565 -0
  9. nativegate-0.1.0/nativegate/discovery.py +167 -0
  10. nativegate-0.1.0/nativegate/driverbuild.py +626 -0
  11. nativegate-0.1.0/nativegate/drivers/__init__.py +5 -0
  12. nativegate-0.1.0/nativegate/drivers/cpp.py +616 -0
  13. nativegate-0.1.0/nativegate/drivers/fortran.py +507 -0
  14. nativegate-0.1.0/nativegate/generators/__init__.py +0 -0
  15. nativegate-0.1.0/nativegate/generators/cmake_gen.py +101 -0
  16. nativegate-0.1.0/nativegate/generators/docker_gen.py +614 -0
  17. nativegate-0.1.0/nativegate/generators/error_gen.py +104 -0
  18. nativegate-0.1.0/nativegate/generators/f2py_gen.py +91 -0
  19. nativegate-0.1.0/nativegate/generators/gateway_gen.py +110 -0
  20. nativegate-0.1.0/nativegate/generators/golden_gen.py +50 -0
  21. nativegate-0.1.0/nativegate/generators/k8s_gen.py +212 -0
  22. nativegate-0.1.0/nativegate/generators/mcp_gen.py +281 -0
  23. nativegate-0.1.0/nativegate/generators/middleware_gen.py +717 -0
  24. nativegate-0.1.0/nativegate/generators/pybind_gen.py +406 -0
  25. nativegate-0.1.0/nativegate/generators/pyproject_gen.py +61 -0
  26. nativegate-0.1.0/nativegate/generators/python_pkg_gen.py +1164 -0
  27. nativegate-0.1.0/nativegate/generators/test_gen.py +160 -0
  28. nativegate-0.1.0/nativegate/golden.py +747 -0
  29. nativegate-0.1.0/nativegate/invariants.py +532 -0
  30. nativegate-0.1.0/nativegate/ir.py +789 -0
  31. nativegate-0.1.0/nativegate/lattice.py +350 -0
  32. nativegate-0.1.0/nativegate/locking.py +216 -0
  33. nativegate-0.1.0/nativegate/oracle.py +904 -0
  34. nativegate-0.1.0/nativegate/parsers/__init__.py +0 -0
  35. nativegate-0.1.0/nativegate/parsers/cpp.py +105 -0
  36. nativegate-0.1.0/nativegate/parsers/cpp_ast.py +1652 -0
  37. nativegate-0.1.0/nativegate/parsers/cpp_regex.py +812 -0
  38. nativegate-0.1.0/nativegate/parsers/fixed_form.py +868 -0
  39. nativegate-0.1.0/nativegate/parsers/fortran.py +157 -0
  40. nativegate-0.1.0/nativegate/parsers/fortran_fparser.py +1116 -0
  41. nativegate-0.1.0/nativegate/parsers/fortran_regex.py +686 -0
  42. nativegate-0.1.0/nativegate/preprocess.py +335 -0
  43. nativegate-0.1.0/nativegate/structural_invariants.py +762 -0
  44. nativegate-0.1.0/nativegate/suggest.py +208 -0
  45. nativegate-0.1.0/nativegate/templates/__init__.py +20 -0
  46. nativegate-0.1.0/nativegate/templates/golden_test_template.py +248 -0
  47. nativegate-0.1.0/nativegate/wire.py +438 -0
  48. nativegate-0.1.0/nativegate.egg-info/PKG-INFO +547 -0
  49. nativegate-0.1.0/nativegate.egg-info/SOURCES.txt +99 -0
  50. nativegate-0.1.0/nativegate.egg-info/dependency_links.txt +1 -0
  51. nativegate-0.1.0/nativegate.egg-info/entry_points.txt +3 -0
  52. nativegate-0.1.0/nativegate.egg-info/requires.txt +31 -0
  53. nativegate-0.1.0/nativegate.egg-info/top_level.txt +1 -0
  54. nativegate-0.1.0/pyproject.toml +99 -0
  55. nativegate-0.1.0/setup.cfg +4 -0
  56. nativegate-0.1.0/tests/test_buildinfo.py +256 -0
  57. nativegate-0.1.0/tests/test_cli_serve_verify.py +63 -0
  58. nativegate-0.1.0/tests/test_cli_suggest.py +42 -0
  59. nativegate-0.1.0/tests/test_cli_verify_aggregate.py +124 -0
  60. nativegate-0.1.0/tests/test_cmake_gen.py +118 -0
  61. nativegate-0.1.0/tests/test_cpp_ast.py +1325 -0
  62. nativegate-0.1.0/tests/test_cpp_driver.py +540 -0
  63. nativegate-0.1.0/tests/test_cpp_pipeline.py +51 -0
  64. nativegate-0.1.0/tests/test_declared_invariants.py +204 -0
  65. nativegate-0.1.0/tests/test_derived_types.py +219 -0
  66. nativegate-0.1.0/tests/test_docker_gen.py +411 -0
  67. nativegate-0.1.0/tests/test_driverbuild.py +558 -0
  68. nativegate-0.1.0/tests/test_endpoint_docstrings.py +178 -0
  69. nativegate-0.1.0/tests/test_fixed_form.py +223 -0
  70. nativegate-0.1.0/tests/test_fortran_doc_comments.py +340 -0
  71. nativegate-0.1.0/tests/test_fortran_driver.py +469 -0
  72. nativegate-0.1.0/tests/test_fortran_fparser.py +468 -0
  73. nativegate-0.1.0/tests/test_fortran_libraries.py +189 -0
  74. nativegate-0.1.0/tests/test_fortran_pipeline.py +643 -0
  75. nativegate-0.1.0/tests/test_gateway_gen.py +196 -0
  76. nativegate-0.1.0/tests/test_generated_python_is_valid.py +400 -0
  77. nativegate-0.1.0/tests/test_golden.py +858 -0
  78. nativegate-0.1.0/tests/test_invariants.py +330 -0
  79. nativegate-0.1.0/tests/test_ir_contract.py +405 -0
  80. nativegate-0.1.0/tests/test_k8s_gen.py +157 -0
  81. nativegate-0.1.0/tests/test_lattice.py +326 -0
  82. nativegate-0.1.0/tests/test_legacy_cpp.py +457 -0
  83. nativegate-0.1.0/tests/test_legacy_fortran.py +540 -0
  84. nativegate-0.1.0/tests/test_locking.py +109 -0
  85. nativegate-0.1.0/tests/test_mcp_gen.py +531 -0
  86. nativegate-0.1.0/tests/test_middleware_gen.py +697 -0
  87. nativegate-0.1.0/tests/test_multifile_cpp.py +209 -0
  88. nativegate-0.1.0/tests/test_native_oracle.py +545 -0
  89. nativegate-0.1.0/tests/test_native_state_contract.py +892 -0
  90. nativegate-0.1.0/tests/test_oracle.py +777 -0
  91. nativegate-0.1.0/tests/test_preprocess_and_config.py +376 -0
  92. nativegate-0.1.0/tests/test_preprocessed_fortran.py +255 -0
  93. nativegate-0.1.0/tests/test_pybind_gen.py +492 -0
  94. nativegate-0.1.0/tests/test_quickstart.py +180 -0
  95. nativegate-0.1.0/tests/test_scalar_ref.py +164 -0
  96. nativegate-0.1.0/tests/test_service_gen.py +1331 -0
  97. nativegate-0.1.0/tests/test_shared_libraries.py +94 -0
  98. nativegate-0.1.0/tests/test_structural_invariants.py +530 -0
  99. nativegate-0.1.0/tests/test_suggest.py +180 -0
  100. nativegate-0.1.0/tests/test_verification_config.py +514 -0
  101. nativegate-0.1.0/tests/test_wire.py +448 -0
@@ -0,0 +1,547 @@
1
+ Metadata-Version: 2.4
2
+ Name: nativegate
3
+ Version: 0.1.0
4
+ Summary: Make legacy C++/C/Fortran AI-ready and microservice-ready: expose native code as an MCP server, REST API and container
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: click>=8.1
8
+ Requires-Dist: pyyaml>=6.0
9
+ Requires-Dist: rich>=13.7
10
+ Provides-Extra: clang
11
+ Requires-Dist: libclang>=16; extra == "clang"
12
+ Provides-Extra: fparser
13
+ Requires-Dist: fparser<0.3,>=0.2; extra == "fparser"
14
+ Provides-Extra: docs
15
+ Requires-Dist: mkdocs>=1.6; extra == "docs"
16
+ Requires-Dist: mkdocs-material>=9.5; extra == "docs"
17
+ Provides-Extra: build
18
+ Requires-Dist: scikit-build-core>=0.10; extra == "build"
19
+ Requires-Dist: pybind11>=3.0; extra == "build"
20
+ Requires-Dist: numpy>=1.26; extra == "build"
21
+ Requires-Dist: fastapi>=0.110; extra == "build"
22
+ Requires-Dist: uvicorn>=0.29; extra == "build"
23
+ Requires-Dist: meson>=1.1; extra == "build"
24
+ Requires-Dist: ninja>=1.11; extra == "build"
25
+ Provides-Extra: test
26
+ Requires-Dist: pytest>=8.0; extra == "test"
27
+ Requires-Dist: httpx>=0.27; extra == "test"
28
+ Requires-Dist: setuptools>=68; extra == "test"
29
+ Requires-Dist: build>=1.0; extra == "test"
30
+ Requires-Dist: wheel>=0.42; extra == "test"
31
+ Requires-Dist: fastmcp>=3.0; extra == "test"
32
+ Requires-Dist: anyio>=4.0; extra == "test"
33
+
34
+ # nativegate — make legacy C++ and Fortran AI-ready and microservice-ready
35
+
36
+ Expose existing C++ and Fortran code to Python as deployable microservices —
37
+ without hand-writing bindings, and without touching the numerics.
38
+
39
+ Point it at a header or a Fortran source and it generates the pybind11/f2py
40
+ bindings, the CMake build, an installable Python package, a FastAPI service,
41
+ smoke tests, a numerical regression baseline, and a Dockerfile.
42
+
43
+ ```bash
44
+ ngate quickstart libraries/geometry/geometry.hpp --name demo --build
45
+ pip install services/demo/dist/*.whl
46
+ ```
47
+
48
+ ```python
49
+ from demo import Geometry
50
+
51
+ Geometry().hypotenuse(3.0, 4.0) # 5.0
52
+ ```
53
+
54
+ ...plus a FastAPI service over the same binding:
55
+
56
+ ```bash
57
+ ngate serve demo
58
+ curl -X POST "http://localhost:8000/hypotenuse?a=3&b=4"
59
+ ```
60
+
61
+ Built for re-hosting the kind of code that runs an engineering business:
62
+ 1990s C++ over F77, fixed-form decks with COMMON blocks and INCLUDE files,
63
+ PVT correlations nobody wants to rewrite and nobody can afford to get wrong.
64
+ The worked example of that in this repo is `services/petro_api`, generated
65
+ from the Fortran decks in `libraries/petro/`.
66
+
67
+ ## Install
68
+
69
+ ```bash
70
+ cd tools/nativegate
71
+ ./scripts/bootstrap.sh # creates .venv, installs everything
72
+ ```
73
+
74
+ Or piecemeal, into an environment of your own:
75
+
76
+ ```bash
77
+ pip install -e ".[clang,build,test,docs]"
78
+ ```
79
+
80
+ | Extra | What it adds |
81
+ |---|---|
82
+ | `clang` | libclang — the C++ AST parser. Without it, C++ falls back to a much weaker regex reader |
83
+ | `build` | scikit-build-core, pybind11, numpy, fastapi, uvicorn — needed to build generated services |
84
+ | `test` | pytest, httpx |
85
+ | `docs` | mkdocs + material |
86
+
87
+ Python 3.10+. `ngate build` also needs a system toolchain that pip cannot
88
+ install: `cmake`, `ninja`, a C++ compiler, and `gfortran` for Fortran services.
89
+
90
+ ### Global install (`ngate` on your PATH everywhere)
91
+
92
+ To get the `ngate` command in every shell/project without activating a
93
+ venv, install this checkout editable with [pipx](https://pipx.pypa.io):
94
+
95
+ ```bash
96
+ python3 -m pip install --user pipx
97
+ python3 -m pipx ensurepath # adds ~/.local/bin to PATH, restart your shell after
98
+ pipx install -e tools/nativegate # run from the repo root
99
+ ```
100
+
101
+ `-e` keeps it editable, so changes under `nativegate/` take effect immediately
102
+ — no reinstall needed. Verify with `ngate --version` from any directory.
103
+
104
+ If `pyproject.toml`'s `dependencies` list changes (e.g. a new package is
105
+ added), `pipx install -e` won't pick it up on its own — the venv pipx made
106
+ already exists. Re-run the install to rebuild it:
107
+
108
+ ```bash
109
+ pipx reinstall nativegate
110
+ ```
111
+
112
+ ## Using it in another project
113
+
114
+ nativegate is an ordinary pip package with a console entry point. Nothing in it
115
+ refers to this repo: `services/` and `libraries/` resolve against your
116
+ **current working directory**, so "use it elsewhere" means install it into
117
+ that project's environment and run it from that project's root.
118
+
119
+ ```bash
120
+ cd /path/to/other-project
121
+ python3 -m venv .venv
122
+ .venv/bin/pip install "nativegate[clang,build,test] @ git+https://github.com/ravikings/nativegate.git@<sha>#subdirectory=tools/nativegate"
123
+
124
+ .venv/bin/ngate quickstart src/mylib.hpp --build
125
+ ```
126
+
127
+ `quickstart` scaffolds `services/mylib` itself — no separate `init` needed
128
+ unless you also want the empty `libraries/`/`infrastructure/` directories.
129
+ `--build` compiles the wheel in the same run. Progress prints as a live
130
+ checklist (scaffold → copy source → generate bindings → build):
131
+
132
+ ```
133
+ ✔ Scaffold service
134
+ ✔ Copy native source
135
+ ✔ Generate bindings & package
136
+ ✔ Build wheel
137
+
138
+ Done — services/mylib is ready.
139
+ ```
140
+
141
+ Pin `@<sha>` (or a tag). This is a code generator: an unpinned upgrade can
142
+ change the bindings under a service that was working, and you want that to be
143
+ a deliberate commit rather than a surprise on someone's laptop.
144
+
145
+ Installing from a local checkout works the same way, and is what you want
146
+ while changing the tool itself:
147
+
148
+ ```bash
149
+ pip install -e "/path/to/haliburtion/tools/nativegate[clang,build,test]"
150
+ ```
151
+
152
+ Three things worth knowing before rolling it out to a team:
153
+
154
+ - **Extras are not optional in practice.** Without `clang` the C++ front end
155
+ silently falls back to the regex reader and your macro-defined and
156
+ `#include`d symbols quietly vanish — set `parser: clang` in
157
+ `nativegate.yaml` so a mis-provisioned machine fails loudly instead. Without
158
+ `build`, `ngate build` cannot run; without `test`, `ngate test`
159
+ cannot.
160
+ - **Run from the project root.** `libraries:` and `include_paths:` in
161
+ `nativegate.yaml` resolve from there too.
162
+ - **Generated services do not depend on nativegate.** `services/<name>/` is a
163
+ self-contained wheel plus a FastAPI app — hand it to a team that has never
164
+ installed the generator and it still builds.
165
+
166
+ ## Use it as an agent skill
167
+
168
+ The tool ships a Claude Code skill at `tools/nativegate/skill/SKILL.md`, so a
169
+ coding agent reaches for `ngate` instead of hand-writing a binding. This
170
+ repo already exposes it — `.claude/skills/nativegate` is a symlink to that
171
+ directory, so editing `SKILL.md` updates the installed skill with no copy step.
172
+
173
+ To install it in another project:
174
+
175
+ ```bash
176
+ mkdir -p .claude/skills
177
+ cp -r /path/to/nativegate/tools/nativegate/skill .claude/skills/nativegate
178
+ # or, to track the tool: ln -s /path/to/nativegate/tools/nativegate/skill .claude/skills/nativegate
179
+ ```
180
+
181
+ The skill covers what the agent has no way to infer from the CLI's `--help`:
182
+ that `suggest` exists so it should not read headers itself to pick a starting
183
+ file, that a `regex reader` line means the parse is missing symbols and must
184
+ not be trusted, that skipped-binding reasons should be relayed rather than
185
+ summarized, and that generated files are rewritten every run so hand-edits are
186
+ lost. It also carries the production-readiness gap list, so the agent raises
187
+ the process-wide call lock and the single-tenant boundary unprompted rather
188
+ than handing over a service as if it were ready for anything.
189
+
190
+ ### Why an agent should use it: the token argument
191
+
192
+ For a human the saving is hours. For an agent it is context, and context is
193
+ the binding constraint on whether the task finishes at all.
194
+
195
+ Hand-writing the binding for one 20-method C++ class means *emitting* the
196
+ pybind11 translation unit, `CMakeLists.txt`, `pyproject.toml`, the package
197
+ `__init__`, a FastAPI app and a test module — on the order of 500 lines, call
198
+ it 7–10k output tokens if every line lands correctly on the first attempt.
199
+ They do not. Each compile or link error costs another round of reading the
200
+ error, re-reading the file, and re-emitting a corrected version; three or four
201
+ rounds is normal for template-adjacent code, and `include/`-vs-`src/` layout
202
+ and undefined-symbol-at-import failures are exactly the kind of thing that
203
+ burns several. A realistic figure is **20–40k tokens per file**, most of it
204
+ output, and all of it resident in context afterwards — crowding out the actual
205
+ engineering question the agent was asked.
206
+
207
+ The same work through nativegate is one command and a four-line checklist:
208
+ roughly **300–500 tokens**, and the generated files never enter context
209
+ because the agent has no reason to read them. Those are estimates from the
210
+ size of the generated artifacts, not a measured benchmark — but the ratio is
211
+ not a close call, and it is structural rather than a matter of prompting the
212
+ agent better.
213
+
214
+ Three effects beyond the raw count:
215
+
216
+ - **The repair loop disappears, not just shrinks.** The generator emits code
217
+ that already compiles for the constructs it accepts, and refuses the ones it
218
+ cannot bind *with a reason*. The agent never enters the read-error →
219
+ re-emit → rebuild cycle that dominates hand-written binding cost, and never
220
+ spends a round discovering that a header does not compile.
221
+ - **`golden verify` is a verification signal the agent cannot fake.** Left to
222
+ itself, an agent asked whether a re-hosted library still works writes tests
223
+ from its own reading of the source — which validates its interpretation, not
224
+ the numbers. `golden record` / `golden verify` gives a real pass/fail on the
225
+ answers for a couple of hundred tokens.
226
+ - **Correctness stops depending on how much context is left.** A hand-written
227
+ binding degrades as the window fills — later methods get less attention than
228
+ earlier ones. Generated output is identical at the start of a session and at
229
+ 80% context.
230
+
231
+ ## Which file do I point it at?
232
+
233
+ For a codebase you did not write, `suggest` parses every header it can find
234
+ and ranks them by how cleanly they would bind — preferring self-contained
235
+ files, because a good first service is one that drags nothing else in:
236
+
237
+ ```
238
+ $ ngate suggest libraries/petro/cpp
239
+
240
+ file binds skipped notes
241
+ ~ include/Units.hpp 11 fn, 1 class, 9 method 1 Units.cpp
242
+ ~ include/WellModel.hpp 1 class, 21 method WellModel.cpp; needs FortranBridge.hpp
243
+ ~ include/FluidModel.hpp 1 class, 18 method, 1 struct FluidModel.cpp; needs FortranBridge.hpp
244
+ ~ include/Simulator.hpp 1 class, 24 method 10 Simulator.cpp; needs DeckReader.hpp, FluidModel.hpp +2
245
+ ✘ include/FortranBridge.hpp 2 fn, 3 struct 26 no .cpp found
246
+
247
+ Start with libraries/petro/cpp/include/Units.hpp:
248
+
249
+ ngate quickstart libraries/petro/cpp/include/Units.hpp --name units --build
250
+ ```
251
+
252
+ `✔` binds everything it declares and needs no other header; `~` binds more
253
+ than it skips; `✘` binds nothing usable. Dependencies outrank skip count in
254
+ the ranking — one skipped array method costs you that method, while one
255
+ `#include` of another subsystem costs you that subsystem's whole build.
256
+
257
+ If the header line says *regex reader* rather than *clang AST*, install the
258
+ parser first (`pip install "nativegate[clang]"`) and re-run: the fallback
259
+ reader silently misses free functions and anything behind a macro, so the
260
+ ranking will be scored on an incomplete picture.
261
+
262
+ ## The loop
263
+
264
+ Run everything from the repo root — commands resolve `services/<name>/`
265
+ relative to your working directory.
266
+
267
+ ```bash
268
+ ngate init # optional: empty libraries/, infrastructure/ dirs
269
+ ngate suggest src/ # which file should I start with?
270
+ ngate quickstart src/pvt.hpp --build # scaffold + expose + generate + build, one shot
271
+ ngate inspect src/pvt.hpp # what the parser sees, before generating
272
+ ngate generate pvt # re-run codegen after editing nativegate.yaml
273
+ ngate build pvt # pip wheel . -> scikit-build-core -> CMake
274
+ ngate test pvt # the generated pytest suite
275
+ ngate golden record pvt # pin the numbers (commit golden.json)
276
+ ngate golden verify pvt # prove a rebuild returns the same answers
277
+ ngate lock pvt # pin deps by version + SHA-256
278
+ ngate docker pvt --build # multi-stage image, non-root, healthcheck
279
+ ngate k8s pvt # -> infrastructure/kubernetes/pvt.yaml
280
+ ngate gateway platform-api --service pvt
281
+ ```
282
+
283
+ Every generated service and gateway also serves an [MCP endpoint](docs/mcp.md)
284
+ at `/mcp`, so the same native routines are callable as tools by Claude or any
285
+ other MCP client. It is the same routes — FastMCP derives one tool per route
286
+ from the router's OpenAPI schema — so the tool surface cannot drift from the
287
+ HTTP surface, and the COMMON-block lock still applies.
288
+
289
+ ## What it parses
290
+
291
+ **C++** — via Clang (libclang), the same front end that compiles the code.
292
+ The preprocessor runs, so `#include`d types, `#define`d names, `#ifdef`
293
+ branches and macro-mangled `extern "C"` bridge declarations resolve. Handles
294
+ classes, structs, overloads, public inheritance, constructors, typedefs and
295
+ `using` aliases, enums, namespaces, `std::string`, static methods, abstract
296
+ classes, `= default` / `= delete`. Also: `std::vector<T>` of scalars,
297
+ `const char*` inputs, operators bound as Python special methods (`__add__`,
298
+ `__eq__`, `__getitem__`, …), and a raw `T*` paired with a length argument
299
+ bound as a numpy buffer — including on class methods.
300
+
301
+ Refused *with a reason*, never silently: templates (a typedef does not help),
302
+ a non-const `std::vector<T>&` (pybind11 would discard what you write into it),
303
+ a non-const `char*` (an output buffer with no length convention), a raw
304
+ pointer with no length argument to pair it with, pointer returns of class
305
+ types (ownership), and types only forward-declared. A header that does not
306
+ compile is refused outright rather than half-bound — clang recovers from an
307
+ unknown type by pretending it was `int`, and binding that produces a service
308
+ whose signatures disagree with the C++ it calls.
309
+
310
+ `extern "C"` functions whose bare `T*` arguments are single scalars passed by
311
+ reference (the Fortran-linkage convention) can be opted in per function with
312
+ `clang.scalar_ref_functions`. It is opt-in and never inferred, because an
313
+ array whose extent lives in a COMMON block or PARAMETER looks identical in a C
314
+ prototype. It applies to free functions under the clang backend only.
315
+
316
+ **Fortran** — you name the routines you want, and a large legacy deck costs
317
+ only what you expose. Fixed-form F77 and free-form F90+, INCLUDE expansion,
318
+ kind-parameter resolution (`real(dp)` → `real(8)`), inferred argument intent,
319
+ module-nested routines, array arguments, and `.F90`/`.F` sources run through
320
+ `gfortran -cpp` first.
321
+
322
+ Two newer Fortran features are narrower than they sound, so they are worth
323
+ stating precisely. **Derived types** are bound by generating a flattening
324
+ shim, but only for a *subroutine*, in *free-form* source, parsed by the
325
+ *fparser2* backend, where the routine is inside a module, the type is defined
326
+ in the same file, and every component is a scalar `real`/`integer`/`logical`.
327
+ **CHARACTER outputs** are bound only when the declaration fixes the length
328
+ (`CHARACTER*80`); an assumed-length output is demoted to an input and
329
+ reported, because f2py builds it and then silently returns an empty string.
330
+ That screening currently runs on the fixed-form path only.
331
+
332
+ Everything the parser recognises but cannot bind is reported at `generate`
333
+ time with the reason — the failure mode that costs the most time is a header
334
+ with thirty methods producing a module with twenty-six and nothing saying
335
+ which four are missing.
336
+
337
+ ## Verification
338
+
339
+ For re-hosted engineering code, "it builds and imports" is not the acceptance
340
+ criterion. Three gates sit beside each other, each answering a different
341
+ question that the others structurally cannot:
342
+
343
+ | | asks | compares against | catches |
344
+ |---|---|---|---|
345
+ | `golden.json` | did it change? | its own past | compiler/flag drift, a regenerate picking a different overload, a refactor of the native source |
346
+ | `oracle` | is it faithful? | the legacy binary | transposed arrays, wrong intent, narrowing, units, argument-order swaps, a wrong first recording |
347
+ | `invariants.json` | is it possible? | mathematics | wrong away from the sample point, NaN at domain edges, hidden process-global state |
348
+
349
+ Run every layer at once, in the order CI runs them — oracle first (a
350
+ faithful binding is a precondition for the other two meaning anything), then
351
+ golden, then invariants — with each layer reported by name so one layer's
352
+ failure never masks another's:
353
+
354
+ ```bash
355
+ ngate verify pvt
356
+ # oracle: passed (9 covered, 0 skipped)
357
+ # golden: passed
358
+ # invariants: passed
359
+ ```
360
+
361
+ ### Layer 1 — golden.json: did the answer change?
362
+
363
+ ```bash
364
+ ngate golden record pvt # services/pvt/golden.json — commit this
365
+ ngate golden verify pvt # 4 entry point(s) unchanged (0 not covered).
366
+ ```
367
+
368
+ `golden.json` records every bound entry point, the inputs it was called with,
369
+ and the answer. Edit the inputs to values your engineers recognise (a real
370
+ pressure, a real API gravity); they survive future re-records. The generated
371
+ `tests/test_golden.py` replays the same calls inside the service's own suite,
372
+ so CI catches drift with no extra wiring. Changing a conversion constant from
373
+ `14.5037738` to `14.5038` fails the check — a change no compiler, type
374
+ checker or unit test would flag.
375
+
376
+ Its limit is structural: golden only asserts about the one input tuple it
377
+ recorded, and only proves the binding is *unchanged*, never that the first
378
+ recording was *right*.
379
+
380
+ ### Layer 2 — oracle: is the binding faithful to the legacy code?
381
+
382
+ ```bash
383
+ ngate oracle check pvt
384
+ # 9 covered, 0 skipped
385
+ ```
386
+
387
+ `oracle check` generates a driver **in the original language** from
388
+ `golden.json`'s recorded entries (never from a fresh sample plan), compiles
389
+ only the driver translation unit, links it against the extension's own built
390
+ objects (never a second compilation of the library sources), runs both
391
+ paths in the same build, and compares every observable value **bitwise** —
392
+ 16 hex digits of IEEE-754, no tolerance, because the same machine code
393
+ produced both sides. It needs a compiler; it needs no committed file, and
394
+ regenerates the driver on every run so a stale one can never pass. A failing
395
+ comparison is classified (argument order, transposition, `float32`
396
+ narrowing, a missing in-place output, ...) so the fix is obvious from the
397
+ message rather than from a difference alone.
398
+
399
+ ### Layer 3 — invariants.json: is it possible?
400
+
401
+ ```bash
402
+ ngate invariants verify pvt
403
+ # 6 function(s) checked, 1 uncovered (services/pvt/invariants.json):
404
+ # solution_gor: pass (6 propert(y/ies), 33 point(s))
405
+ # [uncovered] tubing_bhp: no range declared for parameter(s) rate
406
+ ```
407
+
408
+ Declare what a function's answer must satisfy in `nativegate.yaml`:
409
+
410
+ ```yaml
411
+ state:
412
+ setup: [pvt_set_fluid] # replayed before every property evaluation
413
+ mutating: [pvt_set_fluid] # this routine's job IS to change state
414
+ error_flag: last_error
415
+
416
+ invariants:
417
+ solution_gor:
418
+ - bounds: {min: 0.0}
419
+ - monotone: {in: pressure, direction: nondecreasing}
420
+
421
+ ranges:
422
+ pressure: [14.7, 10000.0] # swept, 33 points, inclusive
423
+ ```
424
+
425
+ `invariants verify` checks two kinds of property over a fixed, declared
426
+ lattice (a per-parameter sweep, never random, never a cartesian explosion):
427
+ **structural** properties (`finite`, `total`, `no_error_flag`, `idempotent`,
428
+ `order_independent` — derived from the IR plus the `state:` declaration, no
429
+ domain knowledge needed) and **declared** properties (`bounds`, `monotone`,
430
+ `sum_to_one` — a closed, non-`eval`'d vocabulary authored by a human). A
431
+ failing declared property reports the first failing lattice point and
432
+ bisects to the tightest bracket it can prove. A swept parameter with no
433
+ declared range is not skipped silently — it is recorded under `uncovered`,
434
+ and **a run whose `checked` block comes out empty is a hard failure**, never
435
+ a quiet pass: an invariants file that checks nothing must not look like one
436
+ that does.
437
+
438
+ ## Layout
439
+
440
+ ```
441
+ nativegate/
442
+ cli.py the CLI (click)
443
+ ir.py language-neutral IR — the seam every layer meets at
444
+ config.py nativegate.yaml
445
+ golden.py numerical regression harness
446
+ discovery.py language/dialect detection
447
+ preprocess.py INCLUDE expansion and gfortran -cpp for .F90/.F
448
+ suggest.py ranks candidate sources
449
+ locking.py dependency pinning for requirements.lock
450
+ parsers/
451
+ cpp.py front door: picks a C++ backend, reports which ran
452
+ cpp_ast.py Clang AST parser (primary)
453
+ cpp_regex.py token/brace reader (fallback, no libclang needed)
454
+ fortran.py front door: picks a Fortran backend
455
+ fortran_fparser.py fparser2 parse tree (primary)
456
+ fortran_regex.py targeted per-routine reader (fallback)
457
+ fixed_form.py F77 fixed-form helpers (IMPLICIT, CHARACTER lengths)
458
+ generators/ pybind11, f2py, CMake, Python package, FastAPI router,
459
+ middleware, error handling, tests, golden test,
460
+ Dockerfile, gateway, MCP server, Kubernetes, pyproject
461
+ ```
462
+
463
+ Parsers normalise source into `ir.ModuleIR`; generators consume it without
464
+ knowing which language it came from. Both C++ backends emit the identical IR,
465
+ which is why swapping the regex reader for a real compiler front end changed
466
+ nothing below `parsers/`. The Fortran side has since gone the same way: the
467
+ fparser2 backend replaced the regex reader as the default, and the regex
468
+ reader is still there for machines without the package.
469
+
470
+ ## Developing
471
+
472
+ ```bash
473
+ .venv/bin/python -m pytest tests -q # 694 tests
474
+ NATIVEGATE_CPP_PARSER=regex .venv/bin/python -m pytest -q # C++ fallback backend
475
+ NATIVEGATE_FORTRAN_PARSER=regex .venv/bin/python -m pytest -q # Fortran fallback backend
476
+ .venv/bin/python -m mkdocs serve # docs at :8000
477
+ ```
478
+
479
+ The suite runs green under both C++ backends — that is the contract that lets
480
+ either one be selected. Beyond unit tests, both language paths are verified by
481
+ really building them: generated CMake fed to actual `cmake`/`clang++`/
482
+ `gfortran`, the resulting extension imported, the FastAPI service exercised
483
+ over HTTP. Two real bugs (f2py module nesting, the missing-`.cpp` link
484
+ failure) were found that way and not by inspection.
485
+
486
+ ## Docs
487
+
488
+ `docs/`, or `mkdocs serve`:
489
+
490
+ - [Getting started](docs/getting-started.md) — C++ end to end
491
+ - [Exposing C++](docs/cpp-guide.md) / [Exposing Fortran](docs/fortran-guide.md)
492
+ - [Numerical regression](docs/golden-values.md)
493
+ - [Troubleshooting](docs/troubleshooting.md)
494
+ - [nativegate.yaml reference](docs/configuration.md) · [CLI reference](docs/cli-reference.md)
495
+ - [Architecture](docs/architecture.md) — the IR, the parser seam, and what is
496
+ generated versus what is yours
497
+ - [Deployment topologies](docs/deployment-topologies.md) — one image per
498
+ service, or one composed gateway
499
+ - [MCP endpoint](docs/mcp.md) — calling the native routines as tools from an
500
+ LLM client
501
+
502
+ ## Before you ship
503
+
504
+ **Honest positioning: this is usable today for internal, single-tenant
505
+ deployments, where a team is putting an HTTP or Python interface on legacy
506
+ code it already trusts. It is not ready for internet-facing or multi-tenant
507
+ use.**
508
+
509
+ The compile-and-serve path is real and verified. Generated services do now
510
+ carry API-key auth (`api.auth: api_key`), rate limiting, a request size cap,
511
+ request IDs and access logging, an exception handler, `/healthz` and `/readyz`
512
+ with SIGTERM draining, and Kubernetes manifests via `ngate k8s`.
513
+
514
+ What remains, and why it is not just a to-do item:
515
+
516
+ - **Fortran endpoints are serialised process-wide.** Each holds one lock
517
+ across the native call, because COMMON blocks are process-global storage and
518
+ two concurrent requests that configure different states would otherwise read
519
+ back each other's numbers silently. Throughput scales with processes, not
520
+ threads — do not plan capacity as if these endpoints were concurrent. C++
521
+ services are not locked (a fresh instance per request covers the usual
522
+ case), which also means a C++ library holding file-scope static state has no
523
+ protection here and the parser cannot detect that it needs any.
524
+ - **Stateful libraries get a session-shaped API** — "configure, then read".
525
+ That is fine when one tenant owns the process and wrong when it does not.
526
+ - **`MAX_ARRAY_ITEMS` is a memory guard, not a bounds check.** Emitted for
527
+ services that take array arguments. The IR records that a parameter is an
528
+ array, not the extent the routine actually declares, so one configurable cap
529
+ (`NATIVEGATE_MAX_ARRAY_ITEMS`, default 65536) stands in for all of them. A
530
+ real extent check waits on the fparser2 front end.
531
+ - **A segfault still takes the worker down.** No sandbox, no per-call timeout,
532
+ no per-call isolation.
533
+ - No CI/CD templates; `infrastructure/docker/` is an empty skeleton.
534
+
535
+ Read [Is this production-ready?](docs/production-readiness.md) and
536
+ [DEFECTS.md](DEFECTS.md) before a paying workload touches this. Those are
537
+ honest gap lists, not a sales pitch.
538
+
539
+ ### Refusals are part of the contract
540
+
541
+ When the parser recognises a symbol but will not bind it, that is a decision,
542
+ not a gap it forgot to fill: a non-const `char*` output buffer has no length,
543
+ a pointer return has no ownership, a derived-type Fortran result has no
544
+ mapping. Each refusal is printed at `generate` time with its reason, and the
545
+ generated service publishes them at `GET /_unexposed` so they stay visible to
546
+ whoever is calling the API. An empty mapping there means this build refused
547
+ nothing; a 404 means the service predates the route and cannot tell you.