You freeze every public port before generation starts. A remote model may fill the interior only. It does not invent new sockets on your system.
Cheap generation looks fast until a new export appears. That export becomes hidden coupling by the next review. You then chase a boundary you never actually approved.
Think of a dry dock before interior crews arrive. You lock every hull opening while the steel is still yours. A free remote worker is that interior crew, nothing more.
Your real constraint is not raw generation throughput. It is who may change the shape of the system. Shape lives in ports: routes, queue names, columns, flags.
If a model can add a route, it owns architecture. If it can only fill a function body, you still own shape. That split is the entire review.
A free generation host makes the split urgent. Latency varies and sessions drop without warning. You cannot let that plane define a public surface.
Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode is an open source coding assistant with free model access and a free server option. Use that plane as a fill worker. Keep every port on your side of the fence.
Data should move one way through four rooms. Room one is the port file you commit. Room two is a stub compiled from that file.
Room three is remote fill of stub bodies only. Room four is a local gate that rejects extra symbols. Nothing flows backward from room three into room one.
If the model wants a new field, it fails closed. You edit the port file yourself on purpose. Then you recompile the stub and rerun the fill.
Keep the port file boring enough to argue about. Architecture should look dull in version control. Excitement belongs inside the function body.
# ports.yaml
service: billing-preview
ports:
- name: QuotePreview
kind: function
input:
account_id: string
plan_id: string
output:
currency: string
cents: integer
errors:
- NotFound
- PlanFrozen
You treat this file as architecture, not comments. CI hashes it on every pull request. Humans change it. Models do not.
Now compile the port into a stub the model may fill. The compiler is local and deterministic. Remote fill never sees the yaml directly.
# compile_ports.py
import hashlib, pathlib, yaml
src = pathlib.Path("ports.yaml").read_text()
spec = yaml.safe_load(src)
digest = hashlib.sha256(src.encode()).hexdigest()[:12]
lines = [
f"# AUTO-STUB. PORTS {digest}. DO NOT ADD EXPORTS.",
"from typing import TypedDict",
"",
"class QuoteIn(TypedDict):",
" account_id: str",
" plan_id: str",
"",
"class QuoteOut(TypedDict):",
" currency: str",
" cents: int",
"",
"class NotFound(Exception): ...",
"class PlanFrozen(Exception): ...",
"",
"def quote_preview(inp: QuoteIn) -> QuoteOut:",
" raise NotImplementedError('fill-me')",
]
pathlib.Path("src/ports_stub.py").write_text("\n".join(lines) + "\n")
print(digest)
The stub is the socket in the hull. The model only replaces NotImplementedError. It cannot add a helper that other packages later import.
You send the stub, not the whole repository. You also send a hard rule in the fill prompt. Extra public names are a defect, not a gift.
Fill quote_preview in src/ports_stub.py.
Keep the module exports unchanged.
Do not add classes, constants, or routes.
Return a unified diff against the stub only.
If the port cannot be implemented, reply FAIL: reason.
Run that fill on a throwaway branch you can delete. Apply the diff inside a sandbox checkout. Then the gate runs before anyone talks about merge.
# gate_ports.py
import ast, hashlib, pathlib, sys
allowed = {"QuoteIn", "QuoteOut", "NotFound", "PlanFrozen", "quote_preview"}
src = pathlib.Path("src/ports_stub.py").read_text()
tree = ast.parse(src)
names = {
n.name for n in tree.body
if isinstance(n, (ast.FunctionDef, ast.ClassDef, ast.AsyncFunctionDef))
}
extra = names - allowed
missing = allowed - names
if extra or missing:
print("PORT GATE FAIL", extra, missing)
sys.exit(1)
ports = pathlib.Path("ports.yaml").read_text().encode()
want = hashlib.sha256(ports).hexdigest()[:12]
if f"PORTS {want}" not in src:
print("STALE STUB")
sys.exit(1)
print("PORT GATE OK")
This is not a style linter for polite code. It is an architecture fuse on the public surface. Blow it and the fill is scrap metal.
Add a contract test that never calls the model. The test pins shape, not clever billing math. A new key in the result is a failed review.
# test_quote_preview_contract.py
import pytest
from ports_stub import quote_preview, NotFound
def test_shape_on_happy_path():
out = quote_preview({"account_id": "a1", "plan_id": "p2"})
assert set(out) == {"currency", "cents"}
assert isinstance(out["cents"], int)
def test_unknown_account():
with pytest.raises(NotFound):
quote_preview({"account_id": "missing", "plan_id": "p2"})
You run the test after fill, not during brainstorming. You do not run it to see what happens. Surprise keys are leaks, not features.
Review the hops as ownership, not as a poster. The generation host can stall or return partial diffs. That hop owns delay, not schema.
The stub compiler can drift from ports.yaml after a rushed edit. That hop owns freshness of the socket. The fill can compile and still leak extra exports.
Notice the product domain stays off the free host. Quote math is not truth just because a model typed it. Your tests decide if the proposal is cargo or scrap.
If the free server disappears tomorrow, ports still compile. Stubs still typecheck on your laptop. You are blocked on fill, not on shape. That is the dock working.
The current gate checks names only, and names are a weak wall. A model can still stuff extra keys into a dict at runtime. Next change is a runtime shape wrap on every return.
def quote_preview(inp: QuoteIn) -> QuoteOut:
out = _impl(inp)
if set(out) != {"currency", "cents"}:
raise RuntimeError("port leak")
if not isinstance(out["cents"], int):
raise RuntimeError("port type leak")
return out
Second change is a mutation budget with git as the tape measure. One fill may touch one stub file, full stop. Any other path means architecture leaked through a side door.
python compile_ports.py
git add ports.yaml src/ports_stub.py
git commit -m "freeze quote preview port"
# apply the fill diff on a branch, then:
python gate_ports.py
pytest test_quote_preview_contract.py
changed=$(git diff --name-only)
echo "$changed"
test "$changed" = "src/ports_stub.py"
If that last test fails, you stop the review. You do not negotiate with the extra files. Extra files are hull cuts you did not approve.
Third change is a human-owned error taxonomy in yaml. FAIL: reason from a model is a note, not a type. You add errors only in ports.yaml, then recompile.
Skip this approach if you do not know the product boundary yet. Frozen ports punish exploration on purpose. A spike needs loose clay, not a welded hull.
Skip this if the interface itself is the experiment. Public API design stays human work. A fill plane cannot discover the right route list for you.
Skip this if you cannot run the gate locally. A remote-only workflow will smuggle ports back in. The whole review collapses without a local fuse.
The yaml file is not a full interface language. It does not model auth, pagination, or idempotency keys. Replace it with OpenAPI or Protobuf when preview ends.
The AST gate misses re-exports and monkey patches. A determined fill can still write through globals(). Pair the gate with a sandbox user and a read-only checkout.
Free remote fill is not a capacity plan either. Queue the jobs and cap concurrent fills. Architecture must survive a cold start of the worker.
This review also ignores runtime speed. A frozen port can still hide an N+1 query. You add probes after the shape is stable. Shape first. Speed later.
You can point the fill worker at any generation host you already trust enough to throw away. The host is interchangeable on purpose. The port file is not.
If you already keep a throwaway generation box, run gate_ports.py against its last diff. That one command tells you whether you still own shape.
Top comments (0)