-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontracts.py
More file actions
192 lines (163 loc) · 7.75 KB
/
Copy pathcontracts.py
File metadata and controls
192 lines (163 loc) · 7.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
"""API contracts as first-class artifacts.
The PLAN phase emits a `Contract` for any project where modules talk to each
other over HTTP (frontend↔backend). Both sides import from the same source —
the contract — instead of independently restating endpoint paths, request
shapes, and response shapes. This eliminates an entire class of bug we used
to ship: response-shape mismatches, validation-rule drift, frontend calling
a route that the backend never implemented.
The contract is intentionally simpler than full OpenAPI — the LLM has to
generate it, so the schema is a flat list of endpoints with stringly-typed
field names. It's enough to:
- drive the WIRING phase's route inventory (no regex needed)
- statically verify backend has every endpoint, frontend calls every endpoint
- feed module BUILD prompts so the frontend module sees the shape the
backend module is committed to (and vice versa)
Format on disk: `workspace/contracts.json`. JSON not YAML — no extra dep,
LLM emits it natively.
"""
from __future__ import annotations
import json
import os
import re
from dataclasses import dataclass, field, asdict
@dataclass
class Endpoint:
"""One HTTP endpoint in the contract.
`request` and `response` are dicts of {field_name: type_string}. Type
strings are informal ("string", "integer", "User", "array<Listing>"); the
contract is for alignment, not strict validation.
"""
name: str # "register", "place_bid"
method: str # "GET" | "POST" | "PUT" | "PATCH" | "DELETE"
path: str # "/api/auth/register"
module: str # owning backend module: "auth"
consumed_by: list[str] = field(default_factory=list) # frontend module(s)
request: dict = field(default_factory=dict) # {field: type_str}
response: dict = field(default_factory=dict) # {status_code: shape}
description: str = ""
def normalized_path(self) -> str:
"""Path with `<param>` placeholders normalized for matching."""
return re.sub(r"<[^>]+>", "<param>", self.path)
@dataclass
class Contract:
"""Full project contract: endpoints + named types."""
endpoints: list[Endpoint] = field(default_factory=list)
types: dict = field(default_factory=dict) # {TypeName: {field: type_str}}
def to_dict(self) -> dict:
return {
"endpoints": [asdict(e) for e in self.endpoints],
"types": self.types,
}
def to_json(self, indent: int = 2) -> str:
return json.dumps(self.to_dict(), indent=indent, sort_keys=False)
@classmethod
def from_dict(cls, data: dict | None) -> "Contract":
if not data:
return cls()
endpoints = []
for raw in data.get("endpoints", []) or []:
try:
endpoints.append(Endpoint(
name=raw.get("name", ""),
method=(raw.get("method") or "GET").upper(),
path=raw.get("path", ""),
module=raw.get("module", ""),
consumed_by=list(raw.get("consumed_by", []) or []),
request=dict(raw.get("request") or {}),
response=dict(raw.get("response") or {}),
description=raw.get("description", ""),
))
except Exception:
continue
return cls(endpoints=endpoints, types=dict(data.get("types") or {}))
@classmethod
def from_plan(cls, plan: dict) -> "Contract":
"""Pull contract out of a PLAN JSON dict (under the `contracts` key)."""
return cls.from_dict(plan.get("contracts"))
@classmethod
def load(cls, workspace: str) -> "Contract":
path = os.path.join(workspace, "contracts.json")
if not os.path.isfile(path):
return cls()
try:
with open(path) as f:
return cls.from_dict(json.load(f))
except Exception:
return cls()
def save(self, workspace: str) -> str:
path = os.path.join(workspace, "contracts.json")
with open(path, "w") as f:
f.write(self.to_json())
return path
def is_empty(self) -> bool:
return not self.endpoints and not self.types
# ── Filters ──────────────────────────────────────────────────────────
def endpoints_for_module(self, module_name: str) -> list[Endpoint]:
"""Endpoints implemented by `module_name` (backend side)."""
return [e for e in self.endpoints if e.module == module_name]
def endpoints_consumed_by(self, module_name: str) -> list[Endpoint]:
"""Endpoints `module_name` is allowed to call (frontend side)."""
return [e for e in self.endpoints if module_name in e.consumed_by]
# ── Prompt rendering ─────────────────────────────────────────────────
def render_for_module(self, module_name: str, *, kind: str = "auto") -> str:
"""Render a compact contract excerpt for inclusion in a module prompt.
`kind` is "backend" (show owned endpoints with full request/response),
"frontend" (show consumed endpoints with the same), or "auto" (both,
deduped).
"""
if self.is_empty():
return ""
owned = self.endpoints_for_module(module_name) if kind in ("backend", "auto") else []
consumed = self.endpoints_consumed_by(module_name) if kind in ("frontend", "auto") else []
seen: set[str] = set()
merged: list[Endpoint] = []
for e in (*owned, *consumed):
key = f"{e.method} {e.path}"
if key in seen:
continue
seen.add(key)
merged.append(e)
if not merged and not self.types:
return ""
lines = [
"## API Contract (authoritative — implement/consume EXACTLY these shapes)",
"",
]
for e in merged:
lines.append(f"### {e.method} {e.path} ({e.name})")
if e.description:
lines.append(f"_{e.description}_")
if e.request:
lines.append("Request body:")
for k, t in e.request.items():
lines.append(f" - {k}: {t}")
if e.response:
lines.append("Responses:")
for code, shape in e.response.items():
if isinstance(shape, dict):
shape_str = ", ".join(f"{k}: {v}" for k, v in shape.items())
else:
shape_str = str(shape)
lines.append(f" - {code}: {{{shape_str}}}")
lines.append("")
if self.types:
lines.append("### Named types")
for name, shape in self.types.items():
if isinstance(shape, dict):
shape_str = ", ".join(f"{k}: {v}" for k, v in shape.items())
else:
shape_str = str(shape)
lines.append(f" - {name}: {{{shape_str}}}")
return "\n".join(lines)
def render_full(self) -> str:
"""Render the entire contract (used in INTEGRATE / global prompts)."""
if self.is_empty():
return ""
lines = ["## Full API Contract", ""]
for e in self.endpoints:
consumed = f" (consumed by: {', '.join(e.consumed_by)})" if e.consumed_by else ""
lines.append(f"- {e.method} {e.path} → {e.module}{consumed}")
if self.types:
lines.append("")
lines.append("Types: " + ", ".join(self.types.keys()))
return "\n".join(lines)