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.
- nativegate-0.1.0/PKG-INFO +547 -0
- nativegate-0.1.0/README.md +514 -0
- nativegate-0.1.0/nativegate/__init__.py +1 -0
- nativegate-0.1.0/nativegate/__main__.py +4 -0
- nativegate-0.1.0/nativegate/buildinfo.py +344 -0
- nativegate-0.1.0/nativegate/cli.py +2007 -0
- nativegate-0.1.0/nativegate/config.py +991 -0
- nativegate-0.1.0/nativegate/declared_invariants.py +565 -0
- nativegate-0.1.0/nativegate/discovery.py +167 -0
- nativegate-0.1.0/nativegate/driverbuild.py +626 -0
- nativegate-0.1.0/nativegate/drivers/__init__.py +5 -0
- nativegate-0.1.0/nativegate/drivers/cpp.py +616 -0
- nativegate-0.1.0/nativegate/drivers/fortran.py +507 -0
- nativegate-0.1.0/nativegate/generators/__init__.py +0 -0
- nativegate-0.1.0/nativegate/generators/cmake_gen.py +101 -0
- nativegate-0.1.0/nativegate/generators/docker_gen.py +614 -0
- nativegate-0.1.0/nativegate/generators/error_gen.py +104 -0
- nativegate-0.1.0/nativegate/generators/f2py_gen.py +91 -0
- nativegate-0.1.0/nativegate/generators/gateway_gen.py +110 -0
- nativegate-0.1.0/nativegate/generators/golden_gen.py +50 -0
- nativegate-0.1.0/nativegate/generators/k8s_gen.py +212 -0
- nativegate-0.1.0/nativegate/generators/mcp_gen.py +281 -0
- nativegate-0.1.0/nativegate/generators/middleware_gen.py +717 -0
- nativegate-0.1.0/nativegate/generators/pybind_gen.py +406 -0
- nativegate-0.1.0/nativegate/generators/pyproject_gen.py +61 -0
- nativegate-0.1.0/nativegate/generators/python_pkg_gen.py +1164 -0
- nativegate-0.1.0/nativegate/generators/test_gen.py +160 -0
- nativegate-0.1.0/nativegate/golden.py +747 -0
- nativegate-0.1.0/nativegate/invariants.py +532 -0
- nativegate-0.1.0/nativegate/ir.py +789 -0
- nativegate-0.1.0/nativegate/lattice.py +350 -0
- nativegate-0.1.0/nativegate/locking.py +216 -0
- nativegate-0.1.0/nativegate/oracle.py +904 -0
- nativegate-0.1.0/nativegate/parsers/__init__.py +0 -0
- nativegate-0.1.0/nativegate/parsers/cpp.py +105 -0
- nativegate-0.1.0/nativegate/parsers/cpp_ast.py +1652 -0
- nativegate-0.1.0/nativegate/parsers/cpp_regex.py +812 -0
- nativegate-0.1.0/nativegate/parsers/fixed_form.py +868 -0
- nativegate-0.1.0/nativegate/parsers/fortran.py +157 -0
- nativegate-0.1.0/nativegate/parsers/fortran_fparser.py +1116 -0
- nativegate-0.1.0/nativegate/parsers/fortran_regex.py +686 -0
- nativegate-0.1.0/nativegate/preprocess.py +335 -0
- nativegate-0.1.0/nativegate/structural_invariants.py +762 -0
- nativegate-0.1.0/nativegate/suggest.py +208 -0
- nativegate-0.1.0/nativegate/templates/__init__.py +20 -0
- nativegate-0.1.0/nativegate/templates/golden_test_template.py +248 -0
- nativegate-0.1.0/nativegate/wire.py +438 -0
- nativegate-0.1.0/nativegate.egg-info/PKG-INFO +547 -0
- nativegate-0.1.0/nativegate.egg-info/SOURCES.txt +99 -0
- nativegate-0.1.0/nativegate.egg-info/dependency_links.txt +1 -0
- nativegate-0.1.0/nativegate.egg-info/entry_points.txt +3 -0
- nativegate-0.1.0/nativegate.egg-info/requires.txt +31 -0
- nativegate-0.1.0/nativegate.egg-info/top_level.txt +1 -0
- nativegate-0.1.0/pyproject.toml +99 -0
- nativegate-0.1.0/setup.cfg +4 -0
- nativegate-0.1.0/tests/test_buildinfo.py +256 -0
- nativegate-0.1.0/tests/test_cli_serve_verify.py +63 -0
- nativegate-0.1.0/tests/test_cli_suggest.py +42 -0
- nativegate-0.1.0/tests/test_cli_verify_aggregate.py +124 -0
- nativegate-0.1.0/tests/test_cmake_gen.py +118 -0
- nativegate-0.1.0/tests/test_cpp_ast.py +1325 -0
- nativegate-0.1.0/tests/test_cpp_driver.py +540 -0
- nativegate-0.1.0/tests/test_cpp_pipeline.py +51 -0
- nativegate-0.1.0/tests/test_declared_invariants.py +204 -0
- nativegate-0.1.0/tests/test_derived_types.py +219 -0
- nativegate-0.1.0/tests/test_docker_gen.py +411 -0
- nativegate-0.1.0/tests/test_driverbuild.py +558 -0
- nativegate-0.1.0/tests/test_endpoint_docstrings.py +178 -0
- nativegate-0.1.0/tests/test_fixed_form.py +223 -0
- nativegate-0.1.0/tests/test_fortran_doc_comments.py +340 -0
- nativegate-0.1.0/tests/test_fortran_driver.py +469 -0
- nativegate-0.1.0/tests/test_fortran_fparser.py +468 -0
- nativegate-0.1.0/tests/test_fortran_libraries.py +189 -0
- nativegate-0.1.0/tests/test_fortran_pipeline.py +643 -0
- nativegate-0.1.0/tests/test_gateway_gen.py +196 -0
- nativegate-0.1.0/tests/test_generated_python_is_valid.py +400 -0
- nativegate-0.1.0/tests/test_golden.py +858 -0
- nativegate-0.1.0/tests/test_invariants.py +330 -0
- nativegate-0.1.0/tests/test_ir_contract.py +405 -0
- nativegate-0.1.0/tests/test_k8s_gen.py +157 -0
- nativegate-0.1.0/tests/test_lattice.py +326 -0
- nativegate-0.1.0/tests/test_legacy_cpp.py +457 -0
- nativegate-0.1.0/tests/test_legacy_fortran.py +540 -0
- nativegate-0.1.0/tests/test_locking.py +109 -0
- nativegate-0.1.0/tests/test_mcp_gen.py +531 -0
- nativegate-0.1.0/tests/test_middleware_gen.py +697 -0
- nativegate-0.1.0/tests/test_multifile_cpp.py +209 -0
- nativegate-0.1.0/tests/test_native_oracle.py +545 -0
- nativegate-0.1.0/tests/test_native_state_contract.py +892 -0
- nativegate-0.1.0/tests/test_oracle.py +777 -0
- nativegate-0.1.0/tests/test_preprocess_and_config.py +376 -0
- nativegate-0.1.0/tests/test_preprocessed_fortran.py +255 -0
- nativegate-0.1.0/tests/test_pybind_gen.py +492 -0
- nativegate-0.1.0/tests/test_quickstart.py +180 -0
- nativegate-0.1.0/tests/test_scalar_ref.py +164 -0
- nativegate-0.1.0/tests/test_service_gen.py +1331 -0
- nativegate-0.1.0/tests/test_shared_libraries.py +94 -0
- nativegate-0.1.0/tests/test_structural_invariants.py +530 -0
- nativegate-0.1.0/tests/test_suggest.py +180 -0
- nativegate-0.1.0/tests/test_verification_config.py +514 -0
- 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.
|